Коротше кажучи, не розробляйте програмне забезпечення для повторного використання, оскільки жоден кінцевий користувач не піклується про те, чи можна повторно використовувати ваші функції. Натомість, інженер із зрозумілості дизайну - чи легко мій код зрозуміти комусь іншому чи моєму майбутньому забудькущому самому? - та гнучкість дизайну- коли мені неминуче доведеться виправляти помилки, додавати функції чи іншим чином змінювати функціональність, наскільки мій код буде протистояти змінам? Єдине, про що піклується ваш клієнт, - це як швидко ви зможете реагувати, коли вона повідомляє про помилку або просить змінити. Задаючи ці питання щодо вашого дизайну, мимоволі, як правило, призводить до повторного використання коду, але такий підхід дозволяє зосередитись на тому, щоб уникнути реальних проблем, з якими ви зіткнетесь протягом життя цього коду, щоб ви могли краще обслуговувати кінцевого користувача, а не переслідувати високих, непрактичних "інженерні" ідеали для задоволення шиї.
Щось таке просте, як приклад, який ви подали, ваша початкова реалізація чудова через те, наскільки вона невелика, але ця прямолінійна конструкція стане важкою для розуміння та крихкою, якщо ви спробуєте заглушити занадто велику функціональну гнучкість (на відміну від гнучкості дизайну) на одна процедура. Нижче моє пояснення мого переважного підходу до розробки складних систем для зрозумілості та гнучкості, які, сподіваюся, продемонструють, що я маю на увазі під ними. Я б не використовував цю стратегію для того, що можна було написати в менш ніж 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()
);
});
}
}
Власне, об'єктно-орієнтоване програмування було винайдено спеціально як рішення проблеми складності / гнучкості парадигми імперативу, оскільки просто немає хорошої відповіді (щоб кожен міг погодитись або дійти самостійно, як би не було), як оптимально вкажіть імперативні функції та процедури в фразі.