GoF Factories: чи можна Абстрактну фабрику замінити набором Фабричних методів

Привіт! Мене звати Олександр, я Senior C++ Engineer в компанії GlobalLogic. Мій загальний комерційний досвід роботи на позиції інженера-програміста складає близько 9 років. Крім основної діяльності розробника я понад чотири роки викладаю курси «Алгоритми та структури даних» та «GoF Design Patterns» в межах програми GlobalLogic Education.

У цій статті я хочу розказати про GoF-фабрики, а саме — про патерни Factory Method (Фабричний метод) та Abstract Factory (Абстрактна фабрика), а також подивитися на них із дещо незвичного ракурсу, відповівши на питання: «Чи можна Абстрактну фабрику замінити набором Фабричних методів?».

Коротко про GOF

У 1994 році Еріх Ґамма (Erich Gamma), Річард Гелм (Richard Helm), Ральф Джонсон (Ralph Johnson) та Джон Вліссідес (John Vlissides) опублікували книгу «Design Patterns: Elements of Reusable Object-Oriented Software» («Патерни проєктування: елементи повторно використовуваного об’єктноорієнтованого програмного забезпечення»). Ці патерни надають перевірені рішення для розповсюджених задач проєктування програмного забезпечення та допомагають зробити систему більш модульною, гнучкою та підтримуваною. У книзі описано 23 патерни, які було розділено на три групи: породжувальні, структурні та поведінкові.

Знаходимо Фабрики

Фабричний метод та Абстрактна фабрика належать до групи породжувальних патернів.

Загалом, породжувальні патерни — це група патернів проєктування, які розв’язують проблеми створення об’єктів. Їхніми ключовими особливостями є:

  • інкапсуляція створення об’єктів;
  • забезпечення гнучкості та розширюваності;
  • зменшення зв’язності між клієнтським кодом та конкретними класами;
  • повторне використання коду.

Фабричний метод та Абстрактна фабрика на прикладі Civilization-like стратегії

Далі пропоную розглянути реалізації Фабричного методу та Абстрактної фабрики на цікавому прикладі. Уявімо, що ми розробляємо Civilization-like стратегію*. Гравець керує розвитком своєї віртуальної цивілізації від доісторичних часів до сучасності та майбутнього, взаємодіючи з іншими суперниками, використовуючи різноманітні стратегії та тактики для перемоги (військова, культурна, наукова тощо). Від епохи до епохи розвитку зазнають (звичайно, із внутрішньо ігровими умовностями) бойові юніти, культурні та наукові будівлі тощо.

Забігаючи наперед, код, представлений у статті, написано мовою С++. Однак я опускатиму деякі формальні та специфічні моменти для того, щоб зробити його простішим і зрозумілим для розробників на інших об’єктно-орієнтованих мовах програмування. Пам’ятайте, приклад будь-якого патерну — це не готове рішення для вашої конкретної задачі. Це відображення принципів та ідей, які потрібно кастомізувати під свою потребу.

Будемо поступово розширювати гру, розв’язуючи задачу саме створення об’єктів в системі. Почнемо з мінімалістичного функціоналу. Для початку, є лише 2 ери: Давні часи (Ancient) та Середньовіччя (Medieval). Також представлено 2 типи бойових юнітів відповідно: Лучник (Archer) та Арбалетник (Crossbowman).

Тобто середньовічні арбалетники приходять на заміну «застарілим» прадавнім лучникам. (Нагадую про ігрові умовності та деяке нехтування історичною правдивістю у контексті комп’ютерної гри).

Мінімалістична діаграма класів виглядатиме наступним чином:

У контексті цієї діаграми нас цікавить те, що кожен юніт має поліморфний метод attack(), що визначається у конкретному класі-нащадку. Для того, щоб надалі не плутатися з ерами та відповідністю юнітів, пропоную перейменувати клас Archer в AncientUnit та Crossbowman в MedievalUnit:

Мінімалістична імплементація цих класів виглядатиме наступним чином:

class Unit
{
public:
    virtual ~Unit() {};
    virtual void attack() = 0;
};

class AncientUnit : public Unit
{
public:
    virtual void attack() override
    {
        std::cout << "Ancient unit (Archer) attacks" << std::endl;
    }
	
};

class MedievalUnit : public Unit
{
public:
    virtual void attack() override
    {
        std::cout << "Medieval unit (Crossbowman) attacks" << std::endl;  
    }
};

Без використання будь-якого патерна процес створення юнітів виглядатиме так:

З різних місць клієнтського коду відбувається створення об’єктів бойових юнітів. Відповідно логіка створення цих об’єктів, що охоплює прийняття рішення про потрібний тип юніта, «розмазана» по клієнтському коду та, з великою ймовірністю, дублюється. Знаходження баги в одному такому місці або оновлення вимог до бізнес-логіки найімовірніше призведе до змін в аналогічних місцях у різних модулях програми. А десь внести зміни можна просто забути...

Фабричний метод

Дану проблему легко вирішує простий патерн Фабричний метод. Створимо клас UnitProvider, статичний метод createUnit якого на основі вхідних параметрів (в нашому випадку — значення епохи) повертатиме створений об’єкт відповідного бойового юніта.

enum class Epoch : int
{	
    ANCIENT,
    MEDIEVAL
};

class UnitProvider
{
public:
    static Unit* createUnit(const Epoch _epoch)
    {
        Unit* result_ptr = nullptr;
        
        switch (_epoch)
        {
            case Epoch::ANCIENT:
                result_ptr = new AncientUnit();
                break;
            case Epoch::MEDIEVAL:
                result_ptr = new MedievalUnit();
                break;
            default:
                /* Some decision here */
                break;
        }
        
        return result_ptr;
    }
};

Клас Game виступає у ролі клієнтського коду:

class Game
{
public:
    // Симуляція бурхливого розвитку до Середньовіччя
    void simulateDevelopment()
    {
        m_CurrentEpoch = Epoch::MEDIEVAL;
    }
 
    void simulateWar()
    {
        // Делегуємо створення юніта для поточної епохи Фабричному методу
        Unit* warrior_ptr = UnitProvider::createUnit(m_CurrentEpoch);
        warrior_ptr->attack();
        delete warrior_ptr;
    }
    
private:
    // Стартуємо в Давні часи
    Epoch m_CurrentEpoch{Epoch::ANCIENT};
};

Ну і метод main, для повноти картини:

int main()
{
    Game game;
    
    game.simulateWar();    
    game.simulateDevelopment();
    game.simulateWar();
    
    return 0;
}

Вивід на консоль виглядає наступним чином:

Рішення про те, об’єкт якого саме класу-нащадка Unit створити, інкапсульоване в UnitProvider::createUnit.

Як проміжний висновок можна сказати, що Фабричний метод на основі вхідного параметра(-ів) приймає рішення про створення об’єкта певного класа-нащадка по вказівнику на базовий клас.

Абстрактна фабрика

Але ж не війною єдиною, треба і про культуру подумати. Додамо в нашу гру споруди, які приноситимуть очки культури (їх можна буде витратити на певні ігрові бонуси). Для Давніх часів це буде Монумент, що приноситиме 1 очко культури, а для Середньовіччя — Амфітеатр з двома очками. Відповідно, мінімалістично маємо базовий клас CulturalBuilding з інтерфейсом getCulturalPoints. Знову ж таки, щоб запобігти плутанині, дочірні класи назвемо AncientCulturalBuilding та MedievalCulturalBuilding.

class CulturalBuilding
{
public:
    virtual ~CulturalBuilding() {}
    virtual int getCulturalPoints() = 0;
};

class AncientCulturalBuilding : public CulturalBuilding
{
public:
    virtual int getCulturalPoints() override
    {
        std::cout << "Ancient cultural building (Monument) gives 1 cultural point" << std::endl; 
        return 1;
    }
};

class MedievalCulturalBuilding : public CulturalBuilding
{
public:
    virtual int getCulturalPoints() override
    {
        std::cout << "Medieval cultural building (Amphitheater) gives 2 cultural points" << std::endl;
        return 2;
    }
};

На даному етапі можна сказати, що в нас є два сімейства сутностей: Давньої епохи (Лучник, Монумент) і Середньовіччя (Арбалетчик, Амфітеатр).

Вводячи поняття «сімейство сутностей» я плавно підходжу до обґрунтування подальшого використання патерну Абстрактна фабрика: він надає інтерфейс для створення сімейства взаємопов’язаних або взаємозалежних сутностей (об’єктів). Основна задача патерну та сама: інкапсулювати логіку створення об’єктів. В цьому випадку інкапсуляція відбуватиметься вже не всередині методу, а в ієрархії класів-фабрик.

Пропоную спочатку розглянути діаграму класів.

IAbstractFactory надає інтерфейс для створення бойових юнітів та культурних будівель. Класи-нащадки AncientFactory та MedievalFactory є реальними фабриками, кожна з яких повинна реалізувати методи createUnit та createCulturalBuilding. І тут все просто. AncientFactory створюватиме та повертатиме сімейство об’єктів Давньої епохи, а MedievalFactory — Середньовіччя.

class IAbstractFactory
{
public:
    virtual ~IAbstractFactory() {}
    virtual Unit* createUnit() = 0;
    virtual CulturalBuilding* createCulturalBuilding() = 0;
};

class AncientFactory : public IAbstractFactory
{
public:
    virtual Unit* createUnit() override 
    {
        return new AncientUnit();
    }
	
    virtual CulturalBuilding* createCulturalBuilding()  override
    {
        return new AncientCulturalBuilding();
    }
};

class MedievalFactory : public IAbstractFactory
{
public:
    virtual Unit* createUnit() override 
    {
        return new MedievalUnit();
    }
	
    virtual CulturalBuilding* createCulturalBuilding()  override
    {
        return new MedievalCulturalBuilding();
    }
};

Змін зазнає клас Game, що виступає як клієнтський код. Атрибут m_Factory_ptr триматиме вказівник на фабрику, за допомогою якої будуть створюватись об’єкти. Також зникає потреба в enum Epoch.

class Game
{
public:
    Game()
    {
        // Стартуємо в Давні часи
        m_Factory_ptr = new AncientFactory();
    }
	
    // Симуляція бурхливої діяльності
    void simulateUserActions()
    {
        Unit* unit_ptr = m_Factory_ptr->createUnit();
        unit_ptr->attack(); // Воюємо юнітом
		
        CulturalBuilding* culturalBuilding_ptr = m_Factory_ptr->createCulturalBuilding();
        culturalBuilding_ptr->getCulturalPoints(); // Забираємо очки культури
	 
        delete unit_ptr;
        delete culturalBuilding_ptr;
    }
	

    void simulateDevelopment()
    {
        delete m_Factory_ptr;
        // Розвиток до Середньовіччя – просто заміняємо фабрику
        m_Factory_ptr = new MedievalFactory();
    }

private:	
    IAbstractFactory* m_Factory_ptr = nullptr;
};

Ну і метод main:

int main()
{
    Game game;
    game.simulateUserActions();
    game.simulateDevelopment();
    std::cout << std::endl;    
    game.simulateUserActions();
	
    return 0;
}

Вивід на консоль виглядає наступним чином:

Все, що потрібно для переходу від однієї ери до іншої (так, в нашій версії гри відбувається повний перехід до іншої ери усіма внутрішньо ігровими сутностями) — це заміна об’єкта фабрики, на яку вказує m_Factory_ptr.

Даний приклад є доволі простим з погляду бізнес-логіки: заміна фабрики відбувається ультимативно на об’єкт класу MedievalFactory в методі simulateDevelopment. У складнішому випадку часто створюють Фабричний метод, який на основі вхідних параметрів повертає об’єкт конкретної фабрики, яка, своєю чергою, створюватиме об’єкти із певного сімейства.

Набір фабричних методів

Дивлячись на доволі непросту конструкцію патерна Абстрактна фабрика, логічно приходить думка: «А навіщо так складно? Можна ж було просто додати ще один Фабричний метод у попередній варіант реалізації, що набагато простіше».

І насправді таке рішення абсолютно має право на життя. Пропоную його розглянути.

Повертаємо назад приватний атрибут m_CurrentEpoch в клас Game; клас UnitProvider тепер варто перейменувати у щось на кшталт EpochObjectsProvider:

class EpochObjectsProvider
{
public:
    static Unit* createUnit(const Epoch _epochType)
    {
        Unit* result_ptr = nullptr;
		
        switch (_epochType)
        {
            case Epoch::ANCIENT:
                result_ptr = new AncientUnit();
                break;
            case Epoch::MEDIEVAL:
                result_ptr = new MedievalUnit();
                break;		
            default:
                /* ... */
                break;		
        }
        return result_ptr;
    }
	
    static CulturalBuilding* createCulturalBuilding(const Epoch _epochType)
    {
        CulturalBuilding* result_ptr = nullptr;
		
        switch (_epochType)
        {
            case Epoch::ANCIENT:
                result_ptr = new AncientCulturalBuilding();
                break;
            case Epoch::MEDIEVAL:
                result_ptr = new MedievalCulturalBuilding();
                break;	
            default:
                /* ... */
                break;		
        }	
        return result_ptr;
    }
};

Клас Game:

class Game
{
public:
    void simulateDevelopment()
    {
        m_currentEpoch = Epoch::MEDIEVAL;
    }
    
    void simulateUserActions()
    {
        Unit* warrior_ptr = EpochObjectsProvider::createUnit(m_currentEpoch);
        warrior_ptr->attack();
        delete warrior_ptr;
        
        CulturalBuilding* culturalBuilding_ptr = EpochObjectsProvider::createCulturalBuilding(m_currentEpoch);
        culturalBuilding_ptr->getCulturalPoints();
        delete culturalBuilding_ptr;
    }
    
private:
    Epoch m_currentEpoch{Epoch::ANCIENT};
};

Метод main залишаємо без змін. Запускаємо і бачимо результат на консолі абсолютно ідентичний попередньому.

Таким чином, ми щойно побачили, що патерн Абстрактна фабрика можна замінити набором Фабричних методів. Також цей підхід виглядає набагато простішим для розуміння, особливо для новачків. То чи можна на цьому ставити крапку і написати гарний висновок? Відповідь: «Ні!».

Перш ніж рухатися далі, необхідно розглянути один з принципів SOLID, а саме, принцип відкритості/закритості (англ. OCP — Open/Closed principle).

Принцип відкритості/закритості

Тут неможливо не згадати про Роберта Мартіна — американського інженера та програміста зі стажем більш як 50 років. Він також відомий під псевдонімом Uncle Bob. Трохи відходячи від теми, дуже раджу подивитися його виступи на Youtube по Clean code.

Картинки по запросу "robert martin"

Роберт Мартін є одним із засновників руху за гнучку розробку (Agile) та одним з авторів Маніфесту гнучкої розробки програмного забезпечення. Також, що нас найбільше цікавить у контексті даної статті, він зібрав п’ять принципів розробки ПЗ, відомі як SOLID. Один з них — принцип відкритості/закритості: «Програмні сутності (класи, модулі, функції тощо) повинні бути відкритими до розширення та закритими до змін». Іншими словами, потрібно додавати новий функціонал таким чином, щоб не змінювати вже написаний код.

Якщо коротко, реалізується даний принцип через наслідування та поліморфізм. Йому потрібно слідувати для того, щоб зменшити витрати (як часові, так і фінансові) на тестування (якщо код не змінювався, його не потрібно тестувати заново) і зменшити ймовірність внесення багів (баги будуть лише в новому коді: старий, відтестований залишається без змін). Звичайно, це ідеальна картина. В реальному житті потрібно прагнути мінімізувати зміни в уже написаному коді.

Абстрактна фабрика чи набір Фабричних методів

Аналізуючи бізнес-задачу, яку ми розглядаємо, можна чітко побачити дві осі змін. По-перше, ми можемо захотіти додати нові ери (наприклад, Сучасність тощо). Іншими словами, це буде нове сімейство сутностей: сучасний воїн та сучасна культурна будівля. Другим напрямом можливого розвитку проєкту є додавання нових об’єктів у кожне з уже визначених сімейств. Наприклад, додаємо Наукову будівлю, яка приноситиме очки науки для розвитку цивілізації. Такою будівлею в Давні часи може бути Бібліотека, а в Середньовіччі — Університет (знову ж таки, прошу не зважати на історичний аспект, при реалізації будемо просто використовувати назви AncientScienceBuilding та MedievalScienceBuilding).

Почнемо із додавання Сучасної ери (тобто нового сімейства сутностей). Відповідно, нам потрібен клас ModernUnit, що наслідується від Unit, та ModernCulturalBuilding, що є нащадком CulturalBuilding.

У першому сценарії подивимося, що буде, якщо ми обрали архітектуру на основі Абстрактної фабрики. Усі зміни відносно попередньої версії на діаграмі класів позначатиму червоним кольором.

Бачимо, що додано нові класи бізнес-об’єктів і фабрику. Важливо зазначити, що не внесено жодних змін в інші класи. Так, в нашій системі зміни ще мають відбутися у методі Game::simulateDevelopment, але вони мінімальні та локалізовані.

А тепер розглянемо другий сценарій: ми обрали підхід із використанням набору Фабричних методів.

Клас EpochObjectProvider зазнає значних змін. Усі його методи повинні бути змінені, щоб обробляти нове значення Epoch::MODERN. У даному прикладі таких методів лише два, але в реальному проєкті їх буде набагато більше. Відповідно, грубо порушуємо принцип відкритості/закритості. Для кращого розуміння наведу оновлений код для методу createUnit:

static Unit* createUnit(const Epoch _epochType)
{
    Unit* result_ptr = nullptr;
		
    switch (_epochType)
    {
        case Epoch::ANCIENT:
            result_ptr = new AncientUnit();
            break;
        case Epoch::MEDIEVAL:
            result_ptr = new MedievalUnit();
            break;
        case Epoch::MODERN:
            result_ptr = new ModernUnit();
            break;
        default:
            /* ... */
            break;		
    }	
    return result_ptr;
}

Можна зробити проміжний висновок. Якщо ми плануємо додавати/видаляти сімейства сутностей, то як архітектурне рішення необхідно використати Абстрактну фабрику. Вибір підходу на основі набору Фабричних методів буде некоректним через порушення принципу відкритості/закритості з усіма наслідками, що випливають з цього.

Тепер пропоную повернутися до початкового варіанту із двома ерами та двома сутностями в кожній і розглянути інший можливий випадок. У кожну із наявних ер потрібно додати відповідну Наукову будівлю.

Почнемо із рішення на основі Абстрактної фабрики.

Поточна архітектура передбачає додавання нового абстрактного методу createScienceBuilding до інтерфейсу IAbstractFactory. Але що відбудеться далі? Усі класи-нащадки мають імплементувати цей метод. Так, в цьому випадку зміни зазнають два класи (MedievalFactory та AncientFactory). Але розуміємо, що в реальному проєкті їх буде набагато більше. Таким чином, будь-яка вимога на додавання/видалення елементу сімейства призводить до змін в усіх нащадках IAbstractFactory.

Альтернативним варіантом, очевидно, є використання рішення на основі набору Фабричних методів.

На перший погляд може здатися, що картина аналогічна до попередньої. Ми додали метод createScienceBuilding у клас EpochObjectProvider. Але ця зміна не тягне за собою низки наступних. Так, ми внесли зміни у раніше написаний клас. Але вона єдина (на відміну від попереднього рішення зі змінами у всіх нащадках IAbstractFactory).

Отже, якщо передбачається активне додавання/видалення сутностей всередині сімейств (без додавання/видалення самих сімейств), то як архітектурне рішення необхідно використати набір Фабричних методів.

Висновок

Залишається сформулювати загальний висновок. Чи можна замість достатньо складного патерну Абстрактна фабрика використати простіший підхід на основі набору Фабричних методів? Проста відповідь: «Так, Абстрактну фабрику можна перетворити на набір Фабричних методів». Інше питання полягає у доцільності. Треба розуміти, що за будь-якого сценарію в нас є поняття сімейств та сутностей, які їх наповнюють. Обираючи між двома підходами важливо вдало спрогнозувати, що переважно залишатиметься сталим, а що зазнаватиме постійних змін.

Якщо сутності залишаються сталими та передбачається активне додавання/видалення сімейств, необхідно обрати Абстрактну фабрику.

Якщо сімейства залишаються сталими та передбачається активне додавання/видалення сутностей в них, правильним вибором буде набір Фабричних методів.

А що робити, якщо активно змінюються і сімейства, і сутності в них? Напишіть свої відповіді в коментарях, а я з часом спробую розкрити це питання у новій статті.

Література

У першу чергу потрібно посилатися на першоджерело патернів проєктування ПЗ: книгу, згадану на початку статті: Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides «Design Patterns: Elements of Reusable Object-Oriented Software». Однак, оскільки вона була написана ще в кінці попереднього тисячоліття, деякі підходи та ідеї, описані в книзі, можуть здатися застарілими та неактуальними.

Зі свого боку можу порадити дві книги на тему, які прочитав сам:

Подяка

У 2015 році я навчався на магістратурі НУ «Львівська Політехніка» кафедри САПР, де Загарюк Роман Вікторович неймовірно цікаво та доступно розкрив нам тему GoF-патернів. Після іспиту він підписав та подарував мені книгу «Head First Design Patterns» (див. Література).

C:\Users\o.basalkevych\Downloads\IMG_2763.JPG

Ця досить нетипова та неочікувана подія посприяла моєму розвитку як ІТ-спеціаліста, ментора та викладача, за що я йому дуже вдячний.

* У статті згадується комп’ютерна гра «Цивілізація» Сіда Меєра (Sid Meier’s Civilization). Ілюстрації бойових юнітів та культурних будівель взято із Sid Meier’s Civilization 5.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

👍ПодобаєтьсяСподобалось14
До обраногоВ обраному12
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Так начебто депенденсі інжекшин вже усюди ліплять замість отіх фабрик з методами, ні?
Якщо так, то нахіба треба взагалі голову забивати цією дурнею?

Цей комент чудово показує повне нерозуміння теми

В мелких проектах — «фабрики» для инстанциирования излишни. В крупных проектах, инстанциированием занимается DI.

В общем, у этих паттерноф GOF — теперь нет ниши. Да и паттерны с таким количеством наследования (вместо композиции) — просто воняют анти-паттерновостью.

P.S. Требование «O» (из SOLID) в проектах встречается часто. К тому же, если некая реализация уже написана и покрыта тестами — как-то некошерно запихивать туда дополнительные фичи, чтобы затем уже нaписанные тесты переписывать/расширять.

Но вот, к примеру, требование «L» встречается не так уж часто — и напрасно, т.к. нарушения нередки.

Ще один комент, який не розуміє для чого потрібні фабрики і чому це не заміна для DI, а DI не заміна для фабрик =)

Нам на 122-ой такое разговаривали на лекции, шё в некоторых случаях действительно можно использовать набор фабричных методов вместо Абстрактной фабрики, это не всегда будет оптимальным решением. Выбор между этими паттернами зависит от конкретных требований вашего проекта и архитектуры системы. Абстрактная фабрика обеспечивает более высокую гибкость и организованность при работе с семействами связанных объектов, тогда как фабричный метод может быть проще и более подходящим для создания отдельных объектов.

Что бы это понять, надо иметь инженерное мышление, а это только 12-х, на гуманитарных про это наверное не рассказывают...

Робити окремі гілки в абстракції для опису епохи вважаю неправильним, що буде якщо нам потрібно буде розвиватись через 10 епох ? Таке називається комбінаторний вибух. Абстрактна фабрика тут гарно підходить, але не можна включати епоху в ієрархію. Є патерн для таких ситуацій, називається Bridge.

Чуть подкину на тему SOLID и буквы O.
У меня 20 лет опыта коммерческой разработки и 28 лет некоммерческой.
Помню первый раз когда на собесах начали активно спрашивать про SOLID, это был 2019 год — тоесть у меня было 15 лет опыта разработки.
Сначало на собесах говорил, что знать не знаю что такое SOLID и знать не хочу. Такой ответ их растраивал, потому-что почему-то для них было очень важно «это». Но то был горячий рынок и вообщем, кое-как это сходило.
Далее я проходил собесы в 20 и 21, опять же о SOLID спрашивали в основном укр компании. Как то я попал в контору на собес, где работал ранее Sergey Teplyakov, там его прямо почитали как бога, он писал добротный блог, который мне ранее попадался и оказывается он также опубликовал книгу о GoF и SOLID, я ее распечатал, книга очень неплохая. Она позволила мне понять такую вещь, что не имеет смысла писать код реализующий GoF, а мы скорее смотрим на свой написанный код и видим есть ли частичная схожесть с GoF. Но SOLID продолжал казаться, какой-то чушью, особенно Open Closed Principle. Сколько статей про это прочитано.
И вот когда пришлось проходить собесы в 24, на этом ужасном рынке, я пытался найти рационализацию и кажеться нашел в этом какой-то смысл. Тут надо наверное углубиться в такую вещь, о которой редко думают, и о чем писал Joel Spolsky
www.joelonsoftware.com/2002/05/06/five-worlds
Есть разные виды кода, как одноразовый например, которому явно не нужен не SOLID, ни OOP. Мы с Вами в основном пишем enterprise код, которому принцип Open/Closed практически не нужен вообще. Другое дело, если Вы пишите library, которым пользуються другие люди, оно версионно, какая-нибудь json библиотека с миллиардом закачиваний, для которой важна backward compatibility. Или язык программирования, вспомним пример с python 2 и 3, изменения в которых привели к торможения обновления на 10+ лет. Любой публичный метод там появивщийся, ты не поменяешь его сигнатуру, вообщем это целый геморой. Удаление некоторых классов, может занимать 5 лет и 5 мажорных версий, как например случилось с бинарной сериализацией в .NET.
Но зачем 95% компаний, которые никогда не пишут библиотеки, постоянно спрашивают про O, еще и у всех свои мнения на этот счет. Думаю проблема в курсах, которые наобучали сотню тысяч программистов с 2015 по 2020, создали целое поколение, которые теперь вроде как синьоры, и не очень хотят прислушиваться к здравому смыслу от старших товарищей.

На моей памаяти, где то с 2016 года начали, на собесах, этот solid спрашивать.

Солід і раніше питали, роках в 2009-2010 точно зустрічав, це було таке питання із зірочкою, але раніше всіх цікавила буква L, тому що більшість коду в ті часи була написана в стилі як в цій статті

Не до конца понял, код как в этой статье и буква L.
Так, а что именно они пытались проверить в интервью?

Знання для чого потрібен поліморфізм. Вище є: створюється як Unit, атакує як Unit — значить це Unit.

Ну... як на мене проблема ООП (особливо Simula like OOP якщо брати за основу C++, C#, Java) полягає у тому, що, по-перше, воно інтуїтивно дуже пасує нашому світогляду і тому дуже привабливе. І, по-друге, воно досить не гнучке, та складне, та часто еволюціонує в погані архітектурні рішення. Саме через це люди намагаються її врятувати та придумують різні методології, на кшталт SOLID, щоб можна було пояснити, чому сталося не так як гадалося: ви порушили такі ось принципи. Так, це легко казати постфактум, от тільки цікаве питання: якщо ми дотримуємося методології, чи може виникнути ситуація, що нам треба буде або переписати проект, або порушити принципи?

Бібліотеки більше підходять під ООП, як на мене, з двох причин. По-перше, зазвичай нам більше знайома галузь, тому меньше несподіванок. По-друге, ми більше керуємо правилами гри, можемо щось не робити, якщо це зовсім не лягає на архітектуру.

Особисти мені ближче все ж таки погляд на світ через ADT, він більше математичний.

Да основная проблема с SOLID как по мне, что принципы не калькулируемы никак, ну кроме принципа I, кое как.

через ADT

Да, мне нравиться идти путем обычного структурного программирования и потом перевода кода в некую «архитектуру», если она виднееться там.

Мабуть ООП, патерни проєктування та SOLID виникли як намагання дати раду велетенським важко керованим монолітам, які було породжено процедурним програмуванням.

Ну... патерни програмування, ООП вийшо зі світа Java більше, де процедурного програмування ніколи не було. SOLID це намагання дати раду ООП.

Взагалі не сходиться по часу. Коли було продедурне програмування, не було інтернету. Коли з’явився інтернет, не було процедурного програмування. Тому якщо брати монолітні сервери, то це точно не процедурне програмування.

Взагалі, якщо брати процедурне програмування, то це мова Сі, та часто філософія Unix систем. А це командний рядок та різні правила на кшталт:

Чим менше — тим краще.
Кожна програма має добре виконувати лише одну роботу.
Створіть прототип якомога швидше.
Виберіть портативність, а не ефективність.
Зберігайте дані в плоских текстових файлах .
Використовуйте переваги програмного забезпечення на свою користь.
Використовуйте скрипти середовища, щоб збільшити портативність.
Уникайте неконтрольованих інтерфейсів користувача.
Зробіть кожну програму фільтром .

Тобто знову мова не йде про моноліти. grep, cat, sed, find це все окремі незалежні програми.

А взагалі ООП досить швидко перемогло процедурне програмування. Як тільки вийшов Borland C++ 3.1 та Turbo Pascal 5.5, то усі швидко почали писати лише ООП, я був перший. Тому що ООП набагато ближче до інтуїтивного сприйняття світу. Я кинувся переписувати свої процедурні програми і... шашкову програму у процедурному стилі я писав тиждень. Коли я її пробував переписати в ООП, з класами колір, фігура, шашка, дамка, поле, ... то мучився більше місяця, та закинув. Навтіь тоді, коли перевід вдавався, то чіасто втрачалася швидкодія. Але навіть тоді я не придав цьому багато значення, переоцінка прийшла трохи пізніше.

Тому я не бачу жодного впливу процедурного програмування на SOLID.

Відкрию вам секрет, що ООП та імплементація його у мовах програмування, виникли ще задовго до жави.
Просто в жаві, а потім у шарпі воно набуло «класичного» вигляду.

Просто в жаві, а потім у шарпі воно набуло «класичного» вигляду.

Цей «класичний» вид я називаю Simula-like OOP, бо ООП на таблицях віртуальних функцій, скоріше за усе, виникло саме там. Але то час мейнфреймів, за якими особисто я не працював, тому я більше розглядаю то, що називалося персоналками. Ну а там панували Turbo C, Turbo Pascal, FoxPro, Clipper, ... можливо Quic Basic у школярів. 1991 — 1993 це вихід ООП версій. Так, Turbo Pascal був перший та вийшов ще у 1989, якщо брати C++, то Watcom з’явився у 1993, Turbo C++ це 1991, але... Все ж таки набуло популярності більше Borland C++ 3.1, хоча реалізація C++ навіть там була досить обмежена.

Якщо брати саме маси, а масовим програмування стало на персоналках, то у 1993 усі писали процедурно, а в ~1995 усі перейшли на ООП, тим паче що з’явилося Turbo Vision, Delphi, це відбулося досить швидно та ніхто не чиплявся.

Зворотна сумісність важлива далеко не тільки в бібліотеках. Системи не мають бути поломаними тільки, тому що розробникам так захотілось.

Ну і плюс якщо ви робите реліз зі зворотною сумісністю набагато зрозуміліше опрацьовувати міграцію, наприклад якщо в вас асинк обробка(наприклад, черга на вході).

Зворотна сумісність важлива далеко не тільки в бібліотеках.

Можно представить сервис с публичным API, конечно, оно не должно меняться/ломаться просто так.

Системи не мають бути поломаними тільки, тому що розробникам так захотілось.

Что это за разработчики, которые хотят ломать системы?)

Ну і плюс якщо ви робите реліз зі зворотною сумісністю набагато зрозуміліше опрацьовувати міграцію, наприклад якщо в вас асинк обробка(наприклад, черга на вході).

Если у Вас есть очередь и ее надо обработать, возможно в неком старом стиле, ну да прийдеться временно держать старый и новый код. Хотя это не значит что старый надо было прямо не трогать, возможно разница между старым и новым 1% Lines of Code, и проще поставить пару if.

Зворотна сумісність важлива далеко не тільки в бібліотеках.

Зворотня сумісність виникає там, де існують важливі клієнти, яких ми не можемо дропнути. Якщо всі клієнти під нашим контролем, то ми можемо їх оновити та забути, що воно колись було не так.

Но зачем 95% компаний, которые никогда не пишут библиотеки, постоянно спрашивают про O, еще и у всех свои мнения на этот счет.

кек, аутсорс, пишу С++ & C++/CLI бібліотеки.

Взагалі, раціональна думка, але знову ж таки, це якщо ми в продукті. Інакше — в аутстафі, аутсорсі — ніколи не вгадаєш, коли буде той момент, що саме потрібне буде те «О», або навіть TDD тощо, бо там горизонт планування для виконавця надто короткий.

що саме потрібне буде те «О»

В каком смысле «потрібне»? Когда дядя сверху или Вы сами запретите себе менять написанный код класса?

Есть разные виды кода, как одноразовый например, которому явно не нужен не SOLID, ни OOP. Мы с Вами в основном пишем enterprise код, которому принцип Open/Closed практически не нужен вообще.

Тоже к этому пришел спустя пару лет разработки;) В веб энтерпрайзе основной упор на архитектуру в целом, иерархию зависимостей и IoC. Даже не вспомню когда я последний раз видел слово «Abstract» в коде.

Накину трохи на вентилятор заради холівару, можливо в тред підтягнуться діди С++ розробки, фанати CppCon, адепти why oop fails та інші (вибачте ті кого пропустив, але всеодно вам раді).
Речі в статті застаріли настільки що на інтервью вже не стидно казати що ти це забув, дехто навіть каже що ніколи цього не знав (не працює якщо ви джун), і саме така відповідь додає балів (грубо, але заради холівару).
Для «настільки застарілого коду» вкину доволі застарілу але не менш епічну статейку harmful.cat-v.org/...​d_Programming_GCAP_09.pdf вона трохи не в тему, але в часи коли писали такий код, це був гарний аргумент. Зараз кужуть що потужностей сучасних процессорів більш ніж достатньо для будь якої гри, але ж це зовсім іншій і більш сучасний підхід.

Чудова стаття, дуже доступно пояснено.

Зразу відчувається наявність нормальної освіти.
Піценосцям не зрозуміти)

Ну це стаття для естетов :)
Дуже годно написано

В принципі так, абстрактна фабрика для сімейств та ієрархій

А що робити, якщо активно змінюються і сімейства, і сутності в них?

Та не городити патерни такі й не ускладнювати життя поки не устаканиться бо це сповільнює внесення змін. Тяпляп і якщо не дають все зарефакторити потім, краще звільнятись і наступний хай колупає код папєрєдніків. Якщо дають то вимагати кепку архітектора

Не знаю що гірше от якщо чесно, колупатись в тяпляп чи у відлизаній архітектурі з триповерховими рівнями патернів. Складні проекти це повільне царство так чи інак

По суті це те саме просто трохи строгіше типи використовуються бо отаке

static Unit* createUnit(const Epoch _epochType)

просто замінюється поліморфізмом

Для мене от завжди було великим питанням про особливі бенефіти абстрактних фабрик. Я їх не бачу думаю краще акуратний набір фабричних методів (хай навіть їх сотні) з передбачуваними іменами вирішує проблему не гірше але не настільки жорстко зав’язує все на типи й ієрархію класів

Ну короч, it depends мабуть

Цікава стаття.
В новому С++ можна variadic template заюзати, буде ще цікавіше.
Для зручності запхнем усе в 1 хедер.

GameFactory.h

enum class WarriorId : int
{
ANCIENT,
MEDIEVAL
};

enum class CultureBuildingId : int
{
ANCIENT,
MEDIEVAL
};

class Unit
{
public:
virtual void showName() = 0;
};

class Warrior : public Unit
{
public:
virtual void attack() = 0;
};

class AnctientArcher : public Warrior
{
public:
void attack() override {}
void showName() override { std::cout << “AnctientArcher” << std::endl; }
};

class MedievalArcher : public Warrior
{
public:
void attack() override {}
void showName() override { std::cout << “MedievalArcher” << std::endl; }
};

class CultureBuilding : public Unit
{
public:
virtual void build() = 0;
};

class AnctientBuilding : public CultureBuilding
{
public:
void build() override {}
void showName() override { std::cout << “AnctientBuilding” << std::endl; }
};

class MedievalBuilding : public CultureBuilding
{
public:
void build() override {}
void showName() override { std::cout << “MedievalBuilding” << std::endl; }
};

class WarriorFactory
{
public:
static Unit* create(WarriorId warriorId)
{
Unit* unit = nullptr;
switch (warriorId)
{
case WarriorId::ANCIENT:
unit = new AnctientArcher();
break;
case WarriorId::MEDIEVAL:
unit = new MedievalArcher();
break;
default:
break;
}

return unit;
}
};

class CultureBuildingFactory
{
public:
static Unit* create(CultureBuildingId warriorId)
{
Unit* unit = nullptr;
switch (warriorId)
{
case CultureBuildingId::ANCIENT:
unit = new AnctientBuilding();
break;
case CultureBuildingId::MEDIEVAL:
unit = new MedievalBuilding();
break;
default:
break;
}

return unit;
}
};

template
class GameFactory: public Factories...
{
public:
using Factories::create...;
};

В цьому хедері, буде сидіти конкретна спеціалізація темлейту (можна декілька створити)

GameFactoryImp.h
#include <GameFactory.h> using GameFactoryImpl=GameFactory<CultureBuildingFactory, WarriorFactory>;

Далі просто юзаемо GameFactoryImpl, викликаємо і не про що не паримось

main.cpp

#include

....

GameFactoryImpl::create(CultureBuildingId::ANCIENT)->showName();
GameFactoryImpl::create(WarriorId::ANCIENT)->showName();
....

Так, ну я б ще shared_ptr заюзав замість голих вказівників.

Це добре в комп’ютерній грі, коли ми керуємо правилами гри та можемо не додавати правила, які не вписуються в архїтектуру. В житті... в житті у бізнес лозіці може з’явитися епоха, яка поєднує Ancient та Medieval, а у чому-сь буде унікальною. А потім з’ясується, що кожен юніт взашалі живе у своїй епохі. А кожне рішення, чи зробити нову епоху різновидом Andient, чи Medieval, чи ввести новий клас, вони цвяхами забивають певні гнучкості.

Підписатись на коментарі