Я працюю програмістом вже багато років, протягом яких, як це не дивно, я весь час щось програмую. І ось яку цікаву річ я помітив: у коді, написаному мною місяць тому, завжди хочеться щось трохи поправити. У код піврічної давності хочеться поміняти дуже багато, а код, написаний два-три роки тому, перетворює мене в емо: хочеться заплакати і померти. У цій статті я опишу два підходи. Завдяки першому архітектура програми виходить заплутаною, а супровід - невиправдано дорогим, а другий - це принцип KISS.
Отже, уявімо собі, що є два програміста. Один з них розумний, прочитав купу статей на Хабре, знає каталог GoF напам'ять, а Фаулера - в обличчя. Інший же робить все просто. Першого зватимуть, наприклад, Борис Н. а другого - Маркус П. Само собою, імена вигадані, і всі збіги з реальними людьми і програмістами випадкові.
Отже, до них обох приходить проектний менеджер (якщо у вашій всесвіту PM не ходить сам до програмістам, назвіть його якось інакше, наприклад BA або lead. Суті це не змінить) і каже:
- Хлопці, нам потрібно, щоб робився хліб.
Саме так, «робився», без уточнення способу виробництва.
Як же надійдуть наші програмісти?
Борис створює свою першу абстракцію - клас Product, від нього він успадковує клас Bread, а інстанціірует екземпляри цього класу фабричний метод класу ProductFactory - createProduct ().
Маркус робить приблизно те ж. Він створює клас Bread і клас Manager з промисловим способом createBread ().
Поки різниця мінімальна. Проектний менеджер, трохи глибше розібравшись (це йому так тільки здається, так) в потребах замовника, приходить вдруге і каже:
- Нам потрібно, щоб хліб не просто робився, а випікався в грубці.
А відразу не можна було сказати, що хліб печеться не в вакуумі, а в грубці? Ну ладно, що ж роблять програмісти?
Борис перейменовує клас ProductFactory в Oven, і виділяє абстракцію - AbstractOven. Щоб було зовсім красиво, він метод createProduct () перейменовує в bakeProduct (). Тим самим Борис вперше здійснив рефакторинг, застосувавши «виділення абстракції», а так само реалізував шаблон «абстрактна фабрика» точь-в-точь як він описаний в літературі. Молодець, Борис.
А ось Маркус нічого не робить. З його точки зору все і так добре. Ну може бути, варто злегка змінить реалізацію createBread ().
Фаза місяця змінюється, і менеджер в третій раз приходить до програмістам. Він говорить:
- Нам потрібно, щоб грубки були різних видів.
Що ж, справедливо.
Борис, радісно потираючи руки, створює три спадкоємця AbstractOven - ElectricOven, MicrowaveOven і GasOven. А клас Oven він видаляє за непотрібністю.
Маркус теж вносить зміни в програму. Він додає в метод createBread цілочисельний параметр ovenType.
У четвертий раз приходить до програмістам менеджер. Він тільки що прочитав одну з книг серії «Я пізнаю світ». Інтерференція нової інформації і PMBoK дала несподіваний результат. Менеджер каже:
- Нам потрібно, щоб газова піч не могла піч без газу.
Борис абсолютно безпідставно вважає, що джерело газу може бути тільки один. А для таких випадків завжди є наш улюблений шаблон. Він створює поодинці GasSourceSingleton, а для зменшення зв'язність впроваджує його через інтерфейс GasSource в GasOven. Ура, він застосував впровадження залежності через сетер!
Скромний від природи Маркус створює речовий приватне поле gasLevel в класі Manager. Природно, доведеться трохи змінити логіку методу createBread, але що поробиш!
Але ось пару днів по тому менеджер приходить в п'ятий раз, і, сито облизуючись, вимовляє:
- Нам потрібно, щоб грубки могли випікати ще й пиріжки (окремо - з м'ясом, окремо - з капустою), і торти.
Програмісти теж хочуть їсти, тому беруться за роботу.
Борис вже починає щось таке відчувати, але зупинитися вже не може. Як грубка дізнається, що саме їй потрібно готувати? Очевидно ж - їй потрібен кухар. І Борис, не довго (а може і довго) думаючи, створює клас Cook. У нього буде метод для приготування, який бере на вхід абстрактну піч - cook (owen: AbstractOwen): Product. Адже це логічно - кухар бере піч, і з її допомогою готує. Потім Борис створює ще кілька спадкоємців класу Product - Cake і Pasty, а від Pasty успадковує MeatPasty і CabbagePasty. А потім для кожного типу продукту створює окремого кухаря - BreadCook, PastyCook і CakeCook.
Начебто ще нормально, але часу на це пішло набагато більше, ніж у Маркуса, який просто додав ще один цілочисельний параметр до методу createBread - breadType.
У шостий раз приходить менеджер. До речі, те, що він зараз попросить - це не вимога замовника, це його власна ініціатива. Але ж про це ніхто не дізнається, чи не так?
- Нам потрібно, щоб хліб, пиріжки і торти випікалися за різними рецептами.
«Хм», - вимовляє Борис і згадує про шаблон «будівельник» (разом з «вільним інтерфейсом». Звичайно ж). Він створює клас Recipe, а до нього - будівельник RecipeBuilder. Рецепт він впроваджує (РАПТОМ!) В грубку за допомогою сетера setRecipe (recipe: Recipe).
А Маркус (ви не повірите) додає ще один цілочисельний параметр в createBread - recipe.
Найцікавіше, як завжди, відбувається далеко від комп'ютерів. А саме: менеджер вперше після початку розробки зустрічається з замовником і нарешті розуміє, навіщо йому потрібна була піч. Він (менеджер) в сьомий раз приходить до програмістам і каже:
- Нам потрібно, щоб в печі можна було обпалювати цеглу.
Для Бориса це остання зустріч з менеджером, але все ж він з останніх сил вносить зміни в архітектуру. Він виділяє абстрактний клас AbstractHeatingSmth - абстрактне нагріває щось. Для нього він створює фабрику HeatingFactory. Від AbstractHeatingSmth він успадковує ProductOven і Furance. В останнього є фабричний метод makeBrick, що створює екземпляр об'єкта Brick. Але нічого не працює. Читачеві пропонується самостійно знайти помилку в архітектурі.
У Маркуса теж не все так гладко. Йому доводиться створити вже третій (!) За рахунком клас. Він називає його Brick, і додає в свій Manager метод makeBrick.
Звичайно, можна заперечити, що у Маркуса всередині методу createBread твориться ад та Ізраїль. і це насправді так. Але за допомогою шаблону «шаблонний метод» безлад цілком можна структурувати. А в достатку фабрик і абстракцій розібратися, ну, трохи складніше.
Висновки, які я хочу зробити, напевно, трохи передбачувані.
Підхід Бориса хороший тим, що практично кожну частину системи можна ізолювати і покрити тестами. Але часу на створення такої кількості класів піде непристойно багато, і кожна зміна вимог обернеться каскадним зміною коду. Спроба ж зробити архітектуру гнучкою, передбачивши побажання замовника, зазвичай провалюється - архітектура гнеться зовсім не в тому місці. Адже, як відомо «світ не просто дивніше, ніж ми собі уявляємо, -
він дивніше, ніж ми можемо собі уявити ». І, отримавши черговий change request, програміст переконується в цьому як ніхто інший.
Підхід Маркуса, скінчено, не дозволяє використовувати модульне тестування, але зате він дає результат набагато швидше, і зміни даються меншою кров'ю. Цей підхід - той самий швидкий старт, якого так хочуть стартаперів всіх мастей. І, як не дивно, в такому коді дійсно легше розібратися, тому що він простіше.
А переписати все заново, якщо що, - це завжди встигну.