Короче говоря, не проектируйте свое программное обеспечение для повторного использования, потому что конечному пользователю нет дела до возможности повторного использования ваших функций. Вместо этого, инженер для понимания дизайна - легко ли мой код для кого-то другого или мое будущее забывчивое я для понимания? - и гибкость дизайна- когда мне неизбежно придется исправлять ошибки, добавлять функции или иным образом изменять функциональность, насколько мой код будет противостоять изменениям? Единственное, о чем заботится ваш клиент, - это как быстро вы сможете ответить, когда она сообщит об ошибке или попросит внести изменения. Когда вы задаете эти вопросы о своем дизайне, это, как правило, приводит к тому, что код можно использовать повторно, но этот подход позволяет вам сосредоточиться на том, чтобы избежать реальных проблем, с которыми вы столкнетесь в течение жизни этого кода, чтобы вы могли лучше обслуживать конечного пользователя, а не преследовать высокий, непрактичный «инженерные» идеалы, чтобы радовать шею бороды.
Для чего-то столь же простого, как пример, который вы предоставили, ваша первоначальная реализация хороша из-за ее малости, но этот простой дизайн станет трудным для понимания и ломким, если вы попытаетесь вложить слишком много функциональной гибкости (в отличие от гибкости дизайна) в одна процедура. Ниже мое объяснение моего предпочтительного подхода к проектированию сложных систем для понимания и гибкости, которое, я надеюсь, продемонстрирует, что я имею в виду под ними. Я бы не использовал эту стратегию для чего-то, что можно было бы написать менее чем в 20 строках за одну процедуру, потому что что-то настолько маленькое, что уже соответствует моим критериям понятности и гибкости.
Объекты, а не процедуры
Вместо того чтобы использовать классы, такие как модули старой школы, с набором подпрограмм, которые вы вызываете для выполнения того, что должно делать ваше программное обеспечение, рассмотрите возможность моделирования домена как объектов, которые взаимодействуют и взаимодействуют для выполнения поставленной задачи. Методы в объектно-ориентированной парадигме были изначально созданы для того, чтобы быть сигналами между объектами, чтобы они Object1
могли сказать, Object2
что делать, что бы то ни было, и, возможно, получить ответный сигнал. Это связано с тем, что объектно-ориентированная парадигма по своей сути предназначена для моделирования ваших доменных объектов и их взаимодействий, а не для причудливого способа организации тех же самых старых функций и процедур императивной парадигмы. В случае сvoid destroyBaghdad
Например, вместо того, чтобы пытаться написать не зависящий от контекста универсальный метод для обработки разрушения Багдада или любой другой вещи (которая может быстро стать сложной, трудной для понимания и ломкой), каждая вещь, которая может быть уничтожена, должна отвечать за понимание того, как уничтожить себя. Например, у вас есть интерфейс, который описывает поведение вещей, которые могут быть уничтожены:
interface Destroyable {
void destroy();
}
Тогда у вас есть город, который реализует этот интерфейс:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Ничто из того, что требует уничтожения экземпляра City
, никогда не будет заботиться о том, как это происходит, поэтому нет никаких оснований для того, чтобы этот код существовал где-либо за пределами City::destroy
, и, действительно, глубокое знание внутренней работы City
вне себя было бы тесной связью, которая уменьшает гибкость, так как вы должны учитывать эти внешние элементы, если вам когда-либо потребуется изменить поведение City
. Это истинная цель инкапсуляции. Думайте об этом, как будто у каждого объекта есть свой собственный API, который должен позволять вам делать с ним все, что вам нужно, чтобы вы могли беспокоиться о выполнении ваших запросов.
Делегация, а не «Контроль»
Теперь, является ли ваш реализующий класс City
или Baghdad
зависит от того, насколько универсальным оказывается процесс разрушения города. По всей вероятности, City
воля будет состоять из более мелких частей, которые нужно будет уничтожить по отдельности, чтобы выполнить полное разрушение города, поэтому в этом случае каждая из этих частей также будет реализована Destroyable
, и каждая из них получит указание City
уничтожить Сами же так же кто-то извне просил себя City
уничтожить.
interface Part extends Destroyable {
...part-specific methods
}
class Building implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class Street implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class City implements Destroyable {
public List<Part> parts() {...}
@Override
public void destroy() {
parts().forEach(Destroyable::destroy);
}
}
Если вы хотите по-настоящему сойти с ума и реализовать идею о Bomb
том, что объект отбрасывается на локацию и уничтожает все в пределах определенного радиуса, это может выглядеть примерно так:
class Bomb {
private final Integer radius;
public Bomb(final Integer radius) {
this.radius = radius;
}
public void drop(final Grid grid, final Coordinate target) {
new ObjectsByRadius(
grid,
target,
this.radius
).forEach(Destroyable::destroy);
}
}
ObjectsByRadius
представляет набор объектов, который рассчитывается для Bomb
входных данных, потому что Bomb
не имеет значения, как выполняется этот расчет, если он может работать с объектами. Кстати, это можно многократно использовать, но главная цель - изолировать вычисления от процессов отбрасывания Bomb
и разрушения объектов, чтобы вы могли понять каждую часть и то, как они сочетаются друг с другом, и изменить поведение отдельной части, не изменяя весь алгоритм ,
Взаимодействия, а не алгоритмы
Вместо того, чтобы пытаться угадать правильное количество параметров для сложного алгоритма, имеет смысл моделировать процесс как набор взаимодействующих объектов, каждый из которых имеет чрезвычайно узкие роли, поскольку это даст вам возможность моделировать сложность вашего процесс через взаимодействия между этими четко определенными, простыми для понимания и почти неизменными объектами. Если все сделано правильно, это делает даже некоторые из самых сложных модификаций такими же тривиальными, как реализация одного или двух интерфейсов и переработка тех объектов, которые создаются в вашем main()
методе.
Я бы дал вам кое-что для вашего оригинального примера, но я, честно говоря, не могу понять, что значит «печатать… Дневные сбережения». Что я могу сказать об этой категории проблем, так это то, что каждый раз, когда вы выполняете вычисление, результат которого можно отформатировать несколькими способами, мой предпочтительный способ разбить это так:
interface Result {
String print();
}
class Caclulation {
private final Parameter paramater1;
private final Parameter parameter2;
public Calculation(final Parameter parameter1, final Parameter parameter2) {
this.parameter1 = parameter1;
this.parameter2 = parameter2;
}
public Result calculate() {
...calculate the result
}
}
class FormattedResult {
private final Result result;
public FormattedResult(final Result result) {
this.result = result;
}
@Override
public String print() {
...interact with this.result to format it and return the formatted String
}
}
Поскольку в вашем примере используются классы из библиотеки Java, которые не поддерживают этот дизайн, вы можете просто использовать API ZonedDateTime
напрямую. Идея здесь заключается в том, что каждый расчет заключен в свой собственный объект. Он не делает никаких предположений о том, сколько раз он должен выполняться или как он должен отформатировать результат. Это исключительно связано с выполнением простейшей формы расчета. Это облегчает понимание и гибкость в изменениях. Точно так же, Result
он связан исключительно с инкапсуляцией результата вычисления, а FormattedResult
исключительно с взаимодействием с ним Result
для его форматирования в соответствии с определенными нами правилами. Таким образом,мы можем найти идеальное количество аргументов для каждого из наших методов, поскольку каждый из них имеет четко определенную задачу . Также гораздо проще изменить движение вперед, если интерфейсы не меняются (что они вряд ли будут делать, если вы должным образом минимизировали ответственность ваших объектов). Нашmain()
метод может выглядеть так:
class App {
public static void main(String[] args) {
final List<Set<Paramater>> parameters = ...instantiated from args
parameters.forEach(set -> {
System.out.println(
new FormattedResult(
new Calculation(
set.get(0),
set.get(1)
).calculate()
).print()
);
});
}
}
Фактически, объектно-ориентированное программирование было придумано специально как решение проблемы сложности / гибкости парадигмы императива, потому что просто нет хорошего ответа (что каждый может договориться или прийти независимо, так или иначе), как оптимально указать императивные функции и процедуры в рамках идиомы.