Architecture Best Practices

Software Design Principles (Частина 1)

Глибоке вивчення фундаментальних принципів проектування програмного забезпечення в C#: вступ, Single Responsibility Principle (SRP) та Open/Closed Principle (OCP)

Software Design Principles: Вступ та SOLID (SRP, OCP)

Вступ та Контекст

Проблема "Спагетті-Коду"

Уявіть проєкт, який почався як елегантне рішення певної задачі. Проходить час, додаються нові вимоги, фіксяться баги, приходять нові розробники. Код розростається, класи стають "всезнаючими монстрами", методи перетворюються на сотні рядків заплутаної логіки, а зміна в одному місці ламає функціональність у зовсім іншому. Це і є технічний борг (Technical Debt) — наслідок ігнорування принципів проектування.

Технічний борг — це концепція, яка описує довгострокові витрати на підтримку коду, який був написаний швидко або без дотримання best practices. Подібно до фінансового боргу, технічний борг накопичує "відсотки" у вигляді збільшеного часу на розробку нових функцій та виправлення помилок.

Історична Еволюція

Розвиток підходів до проектування програмного забезпечення пройшов довгий шлях:

Процедурне Програмування (1950-1970-ті)

Код організовувався як набір процедур та функцій. Дані та логіка існували окремо, що призводило до складності у підтриманні великих систем.

Об'єктно-Орієнтоване Програмування (1980-1990-ті)

ООП об'єднало дані та поведінку в об'єкти. Це був революційний крок, але виникла нова проблема: як правильно проектувати класи та їх взаємодії?

Формування Принципів (2000-ні)

Індустрія усвідомила необхідність формалізованих підходів. Robert C. Martin (також відомий як "Uncle Bob") сформулював SOLID принципи, які стали фундаментом сучасної розробки.

Сучасність (2010-і та далі)

Принципи проектування інтегровані в практики Clean Architecture, Domain-Driven Design (DDD), та agile-методології.

Мета: Чому Потрібні Принципи Проектування?

Принципи проектування — це не абстрактні академічні концепції. Вони вирішують конкретні проблеми:

ПроблемаРішення через принципи
Складність підтримкиКод стає модульним та зрозумілим
Висока зв'язаність (Coupling)Класи стають незалежними один від одного
Низька згуртованість (Cohesion)Кожен клас має чітку, єдину відповідальність
Ламка архітектураЗміни в одному місці не впливають на інші
Складність тестуванняКомпоненти легко ізолюються для unit-тестів
Дублювання кодуПовторне використання через абстракції
Архітектурна якість (Architectural Quality) вимірюється трьома ключовими характеристиками:
  • Maintainability (Супроводжуваність): Наскільки легко вносити зміни та виправляти помилки
  • Scalability (Масштабованість): Чи може система рости без повного переписування
  • Testability (Тестованість): Наскільки легко писати автоматизовані тести

Prerequisites

Перед вивченням цього матеріалу ви повинні бути знайомі з:


Фундаментальні Концепції

Що таке Software Design Principles?

Software Design Principles (Принципи Проектування Програмного Забезпечення) — це набір фундаментальних правил та рекомендацій, які допомагають розробникам створювати код, що є:

  • Зрозумілим: Інші розробники (і ви через півроку) можуть швидко розібратися в коді
  • Модульним: Компоненти можна легко замінювати або переробляти
  • Розширюваним: Нові функції додаються без зламу існуючого функціоналу
  • Стійким до змін: Зміни вимог не вимагають переписування всієї системи

Зв'язок між Принципами та Шаблонами Проектування

Важливо розуміти різницю:

Loading diagram...
graph LR
    A[Design Principles<br/>Принципи] -->|Втілюються через| B[Design Patterns<br/>Шаблони]
    B -->|Реалізуються в| C[Code<br/>Код]

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#f59e0b,stroke:#b45309,color:#ffffff
    style C fill:#64748b,stroke:#334155,color:#ffffff

Принципи — це ЩО потрібно досягти (наприклад, "клас повинен мати одну відповідальність").
Шаблони — це ЯК це досягти (наприклад, "використай Strategy pattern для інкапсуляції алгоритмів").

Архітектурна Якість

Три стовпи якісної архітектури:

Maintainability

Супроводжуваність — можливість вносити зміни швидко та безпечно. Вимірюється:

  • Часом на додавання нової функції
  • Кількістю місць, які потрібно змінити для одного bug-fix
  • Складністю розуміння коду новим розробником

Scalability

Масштабованість — можливість системи обробляти зростаюче навантаження або складність. Включає:

  • Додавання нових модулів без переробки існуючих
  • Горизонтальне та вертикальне масштабування
  • Розподілення відповідальностей між компонентами

Testability

Тестованість — можливість ізольовано тестувати компоненти. Досягається через:

  • Низьку зв'язаність (Loose Coupling)
  • Dependency Injection
  • Чіткі інтерфейси та контракти

Мета SOLID Принципів

SOLID — це акронім п'яти базових принципів об'єктно-орієнтованого проектування:

ЛітераПринципСуть
SSingle Responsibility PrincipleКлас має одну причину для зміни
OOpen/Closed PrincipleВідкритий для розширення, закритий для модифікації
LLiskov Substitution PrincipleПідкласи замінюють базові класи без зламу логіки
IInterface Segregation PrincipleКлієнти не залежать від невикористовуваних методів
DDependency Inversion PrincipleЗалежність від абстракцій, а не конкретних реалізацій
SOLID принципи працюють разом як система. Дотримання одного принципу часто полегшує дотримання інших. Наприклад, SRP спрощує застосування OCP, а DIP робить можливим ISP.

Single Responsibility Principle (SRP)

Визначення та Формулювання

Single Responsibility Principle: Клас повинен мати одну, і тільки одну, причину для зміни.

— Robert C. Martin

Альтернативне формулювання: Клас повинен відповідати лише за одну частину функціональності системи, і ця відповідальність має бути повністю інкапсульована класом.

"Одна Причина для Зміни"

Що таке "причина для зміни"? Це зміна вимог до системи, яка примушує нас модифікувати клас.

Loading diagram...
graph TD
    A[Вимога змінилася] --> B{Скільки класів<br/>потребують змін?}
    B -->|Один клас| C[✓ Дотримання SRP]
    B -->|Декілька класів| D[✗ Порушення SRP]

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style C fill:#10b981,stroke:#059669,color:#ffffff
    style D fill:#ef4444,stroke:#dc2626,color:#ffffff
    style B fill:#f59e0b,stroke:#b45309,color:#ffffff

Приклад: Якщо зміна формату логування вимагає модифікації класу UserService, це означає, що UserService має дві відповідальності: логіку користувача + логування.

Порушення SRP: Anti-Pattern

Розглянемо класичний приклад класу, який порушує SRP:

UserService.cs
public class UserService
{
    // Відповідальність 1: Робота з базою даних
    public void SaveUser(User user)
    {
        using var connection = new SqlConnection("connection_string");
        connection.Open();
        // SQL логіка збереження
    }

    // Відповідальність 2: Валідація
    public bool ValidateUser(User user)
    {
        if (string.IsNullOrEmpty(user.Email)) return false;
        if (!user.Email.Contains("@")) return false;
        return true;
    }

    // Відповідальність 3: Відправка email
    public void SendWelcomeEmail(User user)
    {
        var smtpClient = new SmtpClient("smtp.example.com");
        // Логіка відправки email
    }

    // Відповідальність 4: Логування
    public void LogUserAction(string action)
    {
        File.AppendAllText("log.txt", $"{DateTime.Now}: {action}\n");
    }
}
Проблеми цього класу:
  1. Зміна формату логування → клас потребує змін
  2. Зміна SMTP сервера → клас потребує змін
  3. Зміна правил валідації → клас потребує змін
  4. Зміна структури БД → клас потребує змін
Це чотири причини для зміни замість однієї!

Рефакторинг до SRP

Розділимо відповідальності на окремі класи:

/// <summary>
/// Відповідальність: Робота з базою даних користувачів
/// </summary>
public class UserRepository
{
    private readonly string _connectionString;

    public UserRepository(string connectionString)
    {
        _connectionString = connectionString;
    }

    public void Save(User user)
    {
        using var connection = new SqlConnection(_connectionString);
        connection.Open();

        using var command = new SqlCommand(
            "INSERT INTO Users (Id, Email, Name) VALUES (@Id, @Email, @Name)",
            connection
        );

        command.Parameters.AddWithValue("@Id", user.Id);
        command.Parameters.AddWithValue("@Email", user.Email);
        command.Parameters.AddWithValue("@Name", user.Name);

        command.ExecuteNonQuery();
    }

    public User? GetById(Guid id)
    {
        // Логіка отримання користувача
        return null; // Для прикладу
    }
}

Візуалізація Рефакторингу

Loading diagram...
classDiagram
    class UserService {
        -UserRepository repository
        -UserValidator validator
        -EmailService emailService
        -Logger logger
        +RegisterUser(user) Result
    }

    class UserRepository {
        -string connectionString
        +Save(user) void
        +GetById(id) User
    }

    class UserValidator {
        +Validate(user) ValidationResult
    }

    class EmailService {
        -SmtpClient smtpClient
        +SendWelcomeEmail(user) void
    }

    class Logger {
        -string logFilePath
        +Log(message) void
        +LogInfo(message) void
        +LogWarning(message) void
        +LogError(message) void
    }

    UserService --> UserRepository : використовує
    UserService --> UserValidator : використовує
    UserService --> EmailService : використовує
    UserService --> Logger : використовує

    style UserService fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style UserRepository fill:#64748b,stroke:#334155,color:#ffffff
    style UserValidator fill:#64748b,stroke:#334155,color:#ffffff
    style EmailService fill:#64748b,stroke:#334155,color:#ffffff
    style Logger fill:#64748b,stroke:#334155,color:#ffffff

Переваги Дотримання SRP

КритерійДо SRPПісля SRP
Розуміння кодуСкладно: клас робить "все"Легко: кожен клас має чітку мету
ТестуванняВажко: потрібно мокати всі залежності одразуПросто: тестуємо кожну відповідальність окремо
ЗміниРизиковано: зміна в одному місці може зламати іншеБезпечно: зміни локалізовані
Повторне використанняНеможливо: логіка переплетенаМожливо: класи можна використовувати в інших контекстах

Best Practices

Як визначити, що клас має одну відповідальність?
  1. Тест опису: Чи можете ви описати, що робить клас, одним реченням без "і"/"або"?
- ✓ "UserRepository зберігає та отримує користувачів з бази даних"
- ✗ "UserService зберігає користувачів і валідує їх і відправляє email і логує події"
2. Тест змін: Чи можете ви назвати лише одну причину, чому цей клас може змінитися?
- ✓ "Logger змінюється, якщо змінюється формат або місце зберігання логів"
- ✗ "UserService змінюється, якщо змінюється БД, або валідація, або email"
3. Тест відповідальності: Чи можна виділити відповідальності класу в окремі абстракції?

Troubleshooting: Типові Помилки


Open/Closed Principle (OCP)

Визначення та Формулювання

Open/Closed Principle: Програмні сутності (класи, модулі, функції) повинні бути відкриті для розширення, але закриті для модифікації.

— Bertrand Meyer

Сучасна інтерпретація (Robert C. Martin): Ви повинні мати можливість додавати нову функціональність без зміни існуючого коду.

Loading diagram...
graph LR
    A[Нова вимога] --> B{Як додати<br/>функціональність?}
    B -->|Зміна існуючого коду| C[✗ Порушення OCP<br/>Ризик зламу]
    B -->|Створення нового класу| D[✓ Дотримання OCP<br/>Безпечно]

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style C fill:#ef4444,stroke:#dc2626,color:#ffffff
    style D fill:#10b981,stroke:#059669,color:#ffffff
    style B fill:#f59e0b,stroke:#b45309,color:#ffffff

"Відкритий для Розширення, Закритий для Модифікації"

Дв компоненти цього принципу:

  1. Відкритий для розширення: Можливість додавати нову поведінку при зміні вимог
  2. Закритий для модифікації: Існуючий код залишається недоторканим

Стратегія Реалізації через Абстракції

Ключ до дотримання OCP — програмування через інтерфейси та абстракції:

  • Залежність від інтерфейсів замість конкретних класів
  • Використання спадкування для розширення функціональності
  • Застосування Design Patterns (Strategy, Template Method, Decorator)

Приклад: Порушення OCP

Розглянемо систему обробки платежів:

PaymentProcessor.cs
public class PaymentProcessor
{
    public void ProcessPayment(Order order, string paymentMethod)
    {
        if (paymentMethod == "CreditCard")
        {
            Console.WriteLine("Обробка платежу кредитною карткою");
            // Логіка для кредитної картки
        }
        else if (paymentMethod == "PayPal")
        {
            Console.WriteLine("Обробка платежу через PayPal");
            // Логіка для PayPal
        }
        else if (paymentMethod == "Bitcoin")
        {
            Console.WriteLine("Обробка платежу Bitcoin");
            // Логіка для Bitcoin
        }
        else
        {
            throw new NotSupportedException($"Метод оплати '{paymentMethod}' не підтримується");
        }
    }
}
Проблеми:
  1. Додавання нового методу оплати (наприклад, Apple Pay) вимагає модифікації класу PaymentProcessor
  2. Ризик помилок: Кожна зміна може зламати існуючі методи
  3. Порушення OCP: Клас не закритий для модифікації
  4. Складність тестування: Потрібно перетестовувати весь клас при кожній зміні

Рефакторинг до OCP: Використання Абстракцій

Створимо абстракцію через інтерфейс:

/// <summary>
/// Абстракція для методів оплати
/// </summary>
public interface IPaymentMethod
{
    string Name { get; }
    void ProcessPayment(Order order);
    bool ValidatePayment(Order order);
}

Додавання Нового Методу: Apple Pay

Тепер додамо Apple Pay НЕ змінюючи PaymentProcessor:

ApplePayPayment.cs
/// <summary>
/// Нова реалізація додається БЕЗ модифікації існуючого коду
/// </summary>
public class ApplePayPayment : IPaymentMethod
{
    public string Name => "Apple Pay";

    public void ProcessPayment(Order order)
    {
        Console.WriteLine($"Обробка платежу на суму {order.TotalAmount:C} через Apple Pay");

        // Специфічна логіка для Apple Pay
        AuthenticateWithTouchID();
        ProcessThroughAppleServers(order);
    }

    public bool ValidatePayment(Order order)
    {
        // Валідація Apple Pay токена
        return true;
    }

    private void AuthenticateWithTouchID() { /* ... */ }
    private void ProcessThroughAppleServers(Order order) { /* ... */ }
}
Ключовий момент: Ми додали новий метод оплати створивши новий клас, а не змінюючи PaymentProcessor. Це і є суть OCP — розширення через додавання, а не модифікацію.

OCP та Strategy Pattern

Наш приклад з платежами — це реалізація Strategy Pattern (Шаблон Стратегія):

Loading diagram...
sequenceDiagram
    participant Client
    participant Processor as PaymentProcessor
    participant Strategy as IPaymentMethod
    participant Concrete as CreditCardPayment

    Client->>Processor: new PaymentProcessor(new CreditCardPayment())
    Client->>Processor: ProcessOrder(order)
    Processor->>Strategy: ValidatePayment(order)
    Strategy->>Concrete: ValidatePayment(order)
    Concrete-->>Strategy: true
    Strategy-->>Processor: true
    Processor->>Strategy: ProcessPayment(order)
    Strategy->>Concrete: ProcessPayment(order)
    Concrete-->>Strategy: void
    Strategy-->>Processor: void
    Processor-->>Client: Замовлення оброблено

    style Client fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style Processor fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Strategy fill:#64748b,stroke:#334155,color:#ffffff
    style Concrete fill:#10b981,stroke:#059669,color:#ffffff

Візуалізація Архітектури

Loading diagram...
classDiagram
    class IPaymentMethod {
        <<interface>>
        +Name string
        +ProcessPayment(order) void
        +ValidatePayment(order) bool
    }

    class PaymentProcessor {
        -IPaymentMethod paymentMethod
        +ProcessOrder(order) void
    }

    class CreditCardPayment {
        +Name string
        +ProcessPayment(order) void
        +ValidatePayment(order) bool
    }

    class PayPalPayment {
        +Name string
        +ProcessPayment(order) void
        +ValidatePayment(order) bool
    }

    class BitcoinPayment {
        +Name string
        +ProcessPayment(order) void
        +ValidatePayment(order) bool
    }

    class ApplePayPayment {
        +Name string
        +ProcessPayment(order) void
        +ValidatePayment(order) bool
    }

    IPaymentMethod <|.. CreditCardPayment : implements
    IPaymentMethod <|.. PayPalPayment : implements
    IPaymentMethod <|.. BitcoinPayment : implements
    IPaymentMethod <|.. ApplePayPayment : implements
    PaymentProcessor --> IPaymentMethod : uses

    style IPaymentMethod fill:#f59e0b,stroke:#b45309,color:#ffffff
    style PaymentProcessor fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style CreditCardPayment fill:#64748b,stroke:#334155,color:#ffffff
    style PayPalPayment fill:#64748b,stroke:#334155,color:#ffffff
    style BitcoinPayment fill:#64748b,stroke:#334155,color:#ffffff
    style ApplePayPayment fill:#10b981,stroke:#059669,color:#ffffff

Переваги Дотримання OCP

АспектБез OCPЗ OCP
Додавання функціональностіЗміна існуючого кодуСтворення нового класу
Ризик регресіїВисокийНизький
ТестуванняПеретестовування всьогоТестування лише нового класу
Командна роботаКонфлікти mergeПаралельна робота без конфліктів

Best Practices

Коли застосовувати OCP:
  1. Ідентифікуйте точки варіативності: Де в вашій системі очікуються зміни/розширення?
  2. Створіть абстракції: Інтерфейси або абстрактні класи для цих точок
  3. Програмуйте через інтерфейси: Залежіть від абстракцій, а не конкретних класів
  4. Не передбачайте все: Застосовуйте OCP лише там, де зміни вірогідні (принцип YAGNI)

Troubleshooting

Liskov Substitution Principle (LSP)

Визначення та Формулювання

Liskov Substitution Principle: Об'єкти підкласу повинні бути взаємозамінними з об'єктами базового класу без порушення коректності програми.

— Barbara Liskov

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

Loading diagram...
graph TD
    A[Клієнтський код<br/>очікує BaseClass] --> B{Передали<br/>DerivedClass}
    B -->|LSP дотримано| C[✓ Програма працює<br/>коректно]
    B -->|LSP порушено| D[✗ Неочікувана<br/>поведінка/Exception]

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style C fill:#10b981,stroke:#059669,color:#ffffff
    style D fill:#ef4444,stroke:#dc2626,color:#ffffff
    style B fill:#f59e0b,stroke:#b45309,color:#ffffff

Зв'язок з Контрактами: Design by Contract

LSP тісно пов'язаний з концепцією Design by Contract (Проектування за Контрактом), яка включає три види умов:

Тип умовиОписLSP вимога
Preconditions (Передумови)Умови, що мають бути істинними перед виконанням методуПідклас не може посилювати передумови
Postconditions (Постумови)Умови, що мають бути істинними після виконання методуПідклас не може послаблювати постумови
Invariants (Інваріанти)Умови, що мають залишатися істинними протягом існування об'єктаПідклас має зберігати інваріанти базового класу
Preconditions: Підклас може приймати ширший діапазон значень, ніж базовий клас (ослаблення), але не може вимагати більш строгі умови (посилення).Postconditions: Підклас має гарантувати принаймні те саме, що і базовий клас, або навіть більше (посилення), але не може гарантувати менше (ослаблення).

Класичний Приклад Порушення: Rectangle-Square

Розглянемо класичну проблему Rectangle-Square, яка ілюструє порушення LSP:

/// <summary>
/// Базовий клас: прямокутник
/// Інваріант: Width та Height можуть бути незалежними
/// </summary>
public class Rectangle
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }

    public int CalculateArea()
    {
        return Width * Height;
    }
}
Проблема: Коли ми передаємо Square в метод, що очікує Rectangle, поведінка змінюється неочікуваним чином. Метод TestRectangle припускає, що встановлення Width та Height незалежні операції (інваріант Rectangle), але Square порушує цей інваріант.Це класичне порушення LSP: підклас змінює інваріанти базового класу.

Рішення: Правильна Ієрархія

Правильніше створити спільну абстракцію:

/// <summary>
/// Абстракція для геометричних фігур
/// </summary>
public interface IShape
{
    int CalculateArea();
    string GetDescription();
}

Порушення LSP: NotImplementedException Anti-Pattern

Інший поширений приклад порушення LSP:

BadDesign.cs
public abstract class Bird
{
    public abstract void Fly();
}

public class Sparrow : Bird
{
    public override void Fly()
    {
        Console.WriteLine("Горобець летить");
    }
}

public class Penguin : Bird
{
    public override void Fly()
    {
        // Пінгвіни не літають!
        throw new NotImplementedException("Пінгвіни не вміють літати");
    }
}

// Клієнтський код
public void MakeBirdFly(Bird bird)
{
    bird.Fly(); // Якщо передати Penguin — Exception!
}
Кидання NotImplementedException в перевизначеному методі — це явна ознака порушення LSP. Якщо підклас не може виконати контракт базового класу, він не повинен від нього успадковуватися.

Правильне рішення:

GoodDesign.cs
public abstract class Bird
{
    public abstract void Move();
}

public interface IFlyable
{
    void Fly();
}

public class Sparrow : Bird, IFlyable
{
    public override void Move()
    {
        Fly();
    }

    public void Fly()
    {
        Console.WriteLine("Горобець летить");
    }
}

public class Penguin : Bird
{
    public override void Move()
    {
        Swim();
    }

    private void Swim()
    {
        Console.WriteLine("Пінгвін пливе");
    }
}

// Клієнтський код
public void MoveBird(Bird bird)
{
    bird.Move(); // Працює для всіх Bird
}

public void MakeFly(IFlyable flyable)
{
    flyable.Fly(); // Працює лише для птахів, що літають
}

Візуалізація Правильної Ієрархії

Loading diagram...
classDiagram
    class Bird {
        <<abstract>>
        +Move() void
    }

    class IFlyable {
        <<interface>>
        +Fly() void
    }

    class Sparrow {
        +Move() void
        +Fly() void
    }

    class Penguin {
        +Move() void
        -Swim() void
    }

    Bird <|-- Sparrow : extends
    Bird <|-- Penguin : extends
    IFlyable <|.. Sparrow : implements

    style Bird fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style IFlyable fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Sparrow fill:#10b981,stroke:#059669,color:#ffffff
    style Penguin fill:#64748b,stroke:#334155,color:#ffffff

Best Practices для LSP

Як забезпечити дотримання LSP:
  1. Не посилюйте передумови: Підклас має приймати принаймні те саме, що і базовий клас
  2. Не послаблюйте постумови: Підклас має гарантувати принаймні те саме, що і базовий клас
  3. Зберігайте інваріанти: Підклас не може змінювати інваріанти базового класу
  4. Уникайте NotImplementedException: Якщо метод не має сенсу в підкласі, перегляньте ієрархію
  5. Тестування заміною: Перевіряйте, чи клієнтський код працює коректно при заміні базового класу на підклас

Troubleshooting


Interface Segregation Principle (ISP)

Визначення та Формулювання

Interface Segregation Principle: Клієнти не повинні залежати від інтерфейсів, які вони не використовують.

— Robert C. Martin

Інша формулювка: Краще мати багато спеціалізованих інтерфейсів, ніж один універсальний (Fat Interface).

Loading diagram...
graph LR
    A[Fat Interface<br/>Багато методів] -->|Рефакторинг| B[Спеціалізовані<br/>Інтерфейси]
    B --> C[IReader]
    B --> D[IWriter]
    B --> E[IValidator]

    style A fill:#ef4444,stroke:#dc2626,color:#ffffff
    style B fill:#f59e0b,stroke:#b45309,color:#ffffff
    style C fill:#10b981,stroke:#059669,color:#ffffff
    style D fill:#10b981,stroke:#059669,color:#ffffff
    style E fill:#10b981,stroke:#059669,color:#ffffff

Fat Interfaces vs. Role Interfaces

Fat InterfaceRole Interface
Один інтерфейс з багатьма методамиКілька маленьких, фокусованих інтерфейсів
Клієнти імплементують непотрібні методиКлієнти імплементують лише потрібне
Висока зв'язаністьНизька зв'язаність
Складно тестуватиЛегко мокати окремі ролі

Приклад Порушення ISP

FatInterface.cs
/// <summary>
/// Fat Interface: занадто багато відповідальностей
/// </summary>
public interface IWorker
{
    void Work();
    void Eat();
    void Sleep();
    void TakeSalary();
    void FileTimeSheet();
}

/// <summary>
/// Робот має імплементувати методи, які не мають сенсу
/// </summary>
public class Robot : IWorker
{
    public void Work()
    {
        Console.WriteLine("Робот працює");
    }

    public void Eat()
    {
        // Роботи не їдять!
        throw new NotSupportedException();
    }

    public void Sleep()
    {
        // Роботи не сплять!
        throw new NotSupportedException();
    }

    public void TakeSalary()
    {
        // Роботи не отримують зарплату!
        throw new NotSupportedException();
    }

    public void FileTimeSheet()
    {
        Console.WriteLine("Робот реєструє робочий час");
    }
}

public class Human : IWorker
{
    // Людина має імплементувати ВСІ методи
    public void Work() { /* ... */ }
    public void Eat() { /* ... */ }
    public void Sleep() { /* ... */ }
    public void TakeSalary() { /* ... */ }
    public void FileTimeSheet() { /* ... */ }
}
Проблеми:
  1. Robot змушений імплементувати методи Eat(), Sleep(), TakeSalary(), які не мають сенсу
  2. Кидання NotSupportedException — порушення LSP
  3. Зміна інтерфейсу впливає на всі класи, навіть якщо їм не потрібен новий метод
  4. Важко тестувати: щоб замокати IWorker, потрібно реалізувати всі методи

Рефакторинг до ISP: Розділення Інтерфейсів

/// <summary>
/// Role Interface: здатність працювати
/// </summary>
public interface IWorkable
{
    void Work();
}

Візуалізація ISP

Loading diagram...
classDiagram
    class IWorkable {
        <<interface>>
        +Work() void
    }

    class IFeedable {
        <<interface>>
        +Eat() void
    }

    class ISleepable {
        <<interface>>
        +Sleep() void
    }

    class IPayable {
        <<interface>>
        +TakeSalary() void
    }

    class ITimeTrackable {
        <<interface>>
        +FileTimeSheet() void
    }

    class Robot {
        +Work() void
        +FileTimeSheet() void
    }

    class Human {
        +Work() void
        +Eat() void
        +Sleep() void
        +TakeSalary() void
        +FileTimeSheet() void
    }

    IWorkable <|.. Robot : implements
    ITimeTrackable <|.. Robot : implements

    IWorkable <|.. Human : implements
    IFeedable <|.. Human : implements
    ISleepable <|.. Human : implements
    IPayable <|.. Human : implements
    ITimeTrackable <|.. Human : implements

    style IWorkable fill:#f59e0b,stroke:#b45309,color:#ffffff
    style IFeedable fill:#f59e0b,stroke:#b45309,color:#ffffff
    style ISleepable fill:#f59e0b,stroke:#b45309,color:#ffffff
    style IPayable fill:#f59e0b,stroke:#b45309,color:#ffffff
    style ITimeTrackable fill:#f59e0b,stroke:#b45309,color:#ffffff
    style Robot fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style Human fill:#10b981,stroke:#059669,color:#ffffff

Переваги ISP

АспектFat InterfaceSegregated Interfaces
ГнучкістьКласи змушені імплементувати всеКласи вибирають потрібне
ТестуванняВажко мокати великий інтерфейсЛегко мокати маленькі інтерфейси
ЗміниЗміна впливає на всіхЗміни локалізовані
ЧитабельністьНезрозуміло, що саме потрібноЧітко видно залежності

ISP та Dependency Injection

ISP особливо важливий при використанні DI:

WithDI.cs
public class OrderProcessor
{
    // Залежить лише від того, що йому потрібно
    private readonly IEmailSender _emailSender;
    private readonly IOrderRepository _orderRepository;
    private readonly IPaymentGateway _paymentGateway;

    public OrderProcessor(
        IEmailSender emailSender,
        IOrderRepository orderRepository,
        IPaymentGateway paymentGateway)
    {
        _emailSender = emailSender;
        _orderRepository = orderRepository;
        _paymentGateway = paymentGateway;
    }

    public void ProcessOrder(Order order)
    {
        _paymentGateway.Charge(order.Amount);
        _orderRepository.Save(order);
        _emailSender.SendConfirmation(order.CustomerEmail);
    }
}
Переваги маленьких інтерфейсів з DI:
  • Легко створити mock-об'єкти для тестування
  • Чітко видно залежності класу
  • Можна легко замінити одну реалізацію на іншу
  • Дотримання SRP: кожен інтерфейс має одну відповідальність

Best Practices

Коли розділяти інтерфейс:
  1. Різні клієнти: Якщо різні класи використовують різні підмножини методів
  2. NotImplementedException: Якщо деякі реалізації кидають виключення для непотрібних методів
  3. Опціональні методи: Якщо методи опціональні для деяких реалізацій
  4. Тестування: Якщо важко створити mock через розмір інтерфейсу
Як розділяти:
  • Групуйте методи за ролями/відповідальностями
  • Використовуйте описові назви інтерфейсів (ISendable, IStorable, IValidatable)
  • Один інтерфейс може успадковувати кілька інших для composition

Troubleshooting


Dependency Inversion Principle (DIP)

Визначення та Формулювання

Dependency Inversion Principle:

  • Високорівневі модулі не повинні залежати від низькорівневих модулів. Обидва мають залежати від абстракцій.
  • Абстракції не повинні залежати від деталей. Деталі мають залежати від абстракцій.

— Robert C. Martin

Loading diagram...
graph TD
    subgraph "Без DIP"
        A1[High-Level Module] -->|залежить від| B1[Low-Level Module]
    end

    subgraph "З DIP"
        A2[High-Level Module] -->|залежить від| C[Abstraction]
        B2[Low-Level Module] -->|реалізує| C
    end

    style A1 fill:#ef4444,stroke:#dc2626,color:#ffffff
    style B1 fill:#ef4444,stroke:#dc2626,color:#ffffff
    style A2 fill:#10b981,stroke:#059669,color:#ffffff
    style C fill:#f59e0b,stroke:#b45309,color:#ffffff
    style B2 fill:#10b981,stroke:#059669,color:#ffffff

High-Level vs. Low-Level Modules

High-Level ModulesLow-Level Modules
Бізнес-логікаІнфраструктурні деталі
ОркестраціяКонкретні реалізації
Політики та правилаТехнічні механізми
Приклад: OrderServiceПриклад: SqlOrderRepository
Приклад: PaymentProcessorПриклад: StripePaymentGateway
Традиційна модель: High-level → залежить від → Low-level
DIP інверсія: High-level → визначає абстракцію ← імплементує ← Low-levelТому принцип називається "Dependency Inversion" — залежність інвертується: low-level модуль залежить від абстракції, визначеної high-level модулем.

Приклад Порушення DIP

WithoutDIP.cs
/// <summary>
/// High-level модуль залежить від конкретної реалізації
/// </summary>
public class OrderService
{
    private readonly SqlDatabase _database; // Пряма залежність!

    public OrderService()
    {
        _database = new SqlDatabase(); // Створення залежності всередині!
    }

    public void ProcessOrder(Order order)
    {
        // Бізнес-логіка
        order.Status = OrderStatus.Confirmed;

        // Збереження через конкретну реалізацію
        _database.ExecuteQuery($"INSERT INTO Orders VALUES ('{order.Id}', ...)");
    }
}

public class SqlDatabase
{
    public void ExecuteQuery(string sql)
    {
        // SQL логіка
    }
}
Проблеми:
  1. Тісна зв'язаність: OrderService знає про SqlDatabase
  2. Неможливо тестувати: Не можна замінити БД на mock
  3. Неможливо змінити БД: Щоб перейти на MongoDB, потрібно змінювати OrderService
  4. Порушення OCP: Додавання нового типу БД вимагає модифікації OrderService

Рефакторинг до DIP

/// <summary>
/// Абстракція, визначена HIGH-LEVEL модулем
/// </summary>
public interface IOrderRepository
{
    void Save(Order order);
    Order? GetById(Guid id);
    IEnumerable<Order> GetAll();
}

Dependency Injection Patterns

DIP реалізується через Dependency Injection (DI). Існує три основних патерни ін'єкції:

Найпоширеніший та рекомендований спосіб.

public class OrderService
{
    private readonly IOrderRepository _repository;
    private readonly IEmailService _emailService;
    private readonly ILogger _logger;

    // Залежності передаються через конструктор
    public OrderService(
        IOrderRepository repository,
        IEmailService emailService,
        ILogger logger)
    {
        _repository = repository ?? throw new ArgumentNullException(nameof(repository));
        _emailService = emailService ?? throw new ArgumentNullException(nameof(emailService));
        _logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }

    public void ProcessOrder(Order order)
    {
        _repository.Save(order);
        _emailService.Send(order);
        _logger.Log($"Order {order.Id} processed");
    }
}

Переваги:

  • ✓ Залежності явно видні в сигнатурі
  • ✓ Об'єкт завжди у валідному стані після створення
  • ✓ Залежності можна зробити readonly
  • ✓ Легко тестувати

Недоліки:

  • ✗ Може призвести до "constructor bloat" при багатьох залежностях (ознака порушення SRP)

IoC Containers

Inversion of Control (IoC) Container — це framework, що автоматизує створення об'єктів та ін'єкцію залежностей.

WithIoC.cs
// Реєстрація залежностей (Composition Root)
var services = new ServiceCollection();

// Реєстрація інтерфейсів та реалізацій
services.AddScoped<IOrderRepository, SqlOrderRepository>();
services.AddScoped<IEmailService, SmtpEmailService>();
services.AddScoped<ILogger, FileLogger>();
services.AddScoped<OrderService>();

// Побудова контейнера
var serviceProvider = services.BuildServiceProvider();

// Контейнер автоматично створює OrderService з усіма залежностями
var orderService = serviceProvider.GetRequiredService<OrderService>();
orderService.ProcessOrder(order);

Lifetimes (життєві цикли):

LifetimeОписВикористання
TransientНовий екземпляр при кожному запитіStateless сервіси, легкі об'єкти
ScopedОдин екземпляр на scope (HTTP request в ASP.NET)Repository, DbContext
SingletonОдин екземпляр на весь час роботи додаткуConfiguration, Logger, Cache
services.AddTransient<IEmailService, SmtpEmailService>();  // Новий щоразу
services.AddScoped<IOrderRepository, SqlOrderRepository>(); // Один на request
services.AddSingleton<ILogger, FileLogger>();               // Один на application

Візуалізація DIPДіаграма залежностей:

Loading diagram...
graph TB
    subgraph "High-Level Layer"
        OS[OrderService]
    end

    subgraph "Abstraction Layer"
        IOR[IOrderRepository<br/>interface]
        IES[IEmailService<br/>interface]
    end

    subgraph "Low-Level Layer"
        SQL[SqlOrderRepository]
        Mongo[MongoOrderRepository]
        InMem[InMemoryOrderRepository]
        SMTP[SmtpEmailService]
        SG[SendGridEmailService]
    end

    OS -.->|depends on| IOR
    OS -.->|depends on| IES

    SQL -.->|implements| IOR
    Mongo -.->|implements| IOR
    InMem -.->|implements| IOR

    SMTP -.->|implements| IES
    SG -.->|implements| IES

    style OS fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style IOR fill:#f59e0b,stroke:#b45309,color:#ffffff
    style IES fill:#f59e0b,stroke:#b45309,color:#ffffff
    style SQL fill:#64748b,stroke:#334155,color:#ffffff
    style Mongo fill:#64748b,stroke:#334155,color:#ffffff
    style InMem fill:#10b981,stroke:#059669,color:#ffffff
    style SMTP fill:#64748b,stroke:#334155,color:#ffffff
    style SG fill:#64748b,stroke:#334155,color:#ffffff

Best Practices

Дотримання DIP:
  1. Composition Root: Створюйте об'єкти та налаштовуйте залежності в одному місці (зазвичай на вході в додаток)
  2. Програмуйте через інтерфейси: Типи параметрів, полів, та властивостей — абстракції
  3. Уникайте new: У бізнес-логіці не створюйте залежності через new, ін'єктуйте їх
  4. Тестуйте ізольовано: Використовуйте mocks/stubs для залежностей у тестах
  5. IoC Container: Використовуйте DI container для автоматизації

Troubleshooting


DRY (Don't Repeat Yourself)

Визначення та Формулювання

DRY Principle: Кожен елемент знання має мати єдине, недвозначне, авторитетне представлення всередині системи.

— Andy Hunt and Dave Thomas, "The Pragmatic Programmer"

Проста формулювка: Не дублюйте код та знання. Якщо ви копіюєте код — ви робите щось неправильно.

DRY — це не тільки про дублювання коду, але й про дублювання знань. Два фрагменти коду можуть виглядати по-різному, але якщо вони представляють одне і те ж знання, це порушення DRY.

Дублювання Коду vs. Дублювання Знань

Важливо розрізняти:

ТипОписПриклад
Дублювання кодуІдентичний або схожий код в різних місцяхCopy-paste одного і того ж методу
Дублювання знаньРізний код, що представляє одне знанняЛогіка валідації email в UI та на backend
Випадковий збігКод виглядає схоже, але представляє різні концепціїДва методи Calculate() в різних контекстах
Не кожен схожий код — порушення DRY! Якщо два фрагменти коду випадково схожі, але представляють різні бізнес-концепції, їх не варто об'єднувати. Це призведе до штучної зв'язаності.

Приклад Порушення DRY

ViolatesDRY.cs
public class UserService
{
    public void CreateUser(string email, string password)
    {
        // Валідація email (копія #1)
        if (string.IsNullOrEmpty(email))
            throw new ArgumentException("Email не може бути порожнім");
        if (!email.Contains("@"))
            throw new ArgumentException("Email має містити @");

        // Створення користувача
    }

    public void UpdateUserEmail(Guid userId, string newEmail)
    {
        // Валідація email (копія #2)
        if (string.IsNullOrEmpty(newEmail))
            throw new ArgumentException("Email не може бути порожнім");
        if (!newEmail.Contains("@"))
            throw new ArgumentException("Email має містити @");

        // Оновлення email
    }

    public void SendInvitation(string email)
    {
        // Валідація email (копія #3)
        if (string.IsNullOrEmpty(email))
            throw new ArgumentException("Email не може бути порожнім");
        if (!email.Contains("@"))
            throw new ArgumentException("Email має містити @");

        // Відправка запрошення
    }
}
Проблеми дублювання:
  1. Зміна правил валідації вимагає оновлення в трьох місцях
  2. Ризик неузгодженості: можна забути оновити одне з місць
  3. Збільшення розміру коду без користі
  4. Складність підтримки

Рефакторинг: Усунення Дублювання

/// <summary>
/// Єдине джерело знань про валідацію email
/// </summary>
public static class EmailValidator
{
    public static void Validate(string email)
    {
        if (string.IsNullOrEmpty(email))
            throw new ArgumentException("Email не може бути порожнім", nameof(email));

        if (!email.Contains("@"))
            throw new ArgumentException("Email має містити @", nameof(email));

        // Додаткова валідація через regex
        var emailRegex = new Regex(@"^[^@\s]+@[^@\s]+\.[^@\s]+$");
        if (!emailRegex.IsMatch(email))
            throw new ArgumentException("Некоректний формат email", nameof(email));
    }

    public static bool IsValid(string email)
    {
        try
        {
            Validate(email);
            return true;
        }
        catch
        {
            return false;
        }
    }
}
Переваги після рефакторингу:
  • ✓ Одне місце для зміни логіки валідації
  • ✓ Гарантована консистентність валідації
  • ✓ Можливість легко розширити валідацію (regex додано лише в одному місці)
  • ✓ Повторне використання в інших сервісах

DRY в Конфігураціях

Дублювання в конфігураційних файлах теж порушує DRY:

{
    "EmailSettings": {
        "SmtpServer": "smtp.gmail.com",
        "SmtpPort": 587,
        "EnableSsl": true
    },
    "NotificationSettings": {
        "SmtpServer": "smtp.gmail.com",
        "SmtpPort": 587,
        "EnableSsl": true
    },
    "ReportSettings": {
        "SmtpServer": "smtp.gmail.com",
        "SmtpPort": 587,
        "EnableSsl": true
    }
}

Anti-Pattern: Copy-Paste Programming

Loading diagram...
graph TD
    A[Потрібна схожа функціональність] --> B{Як реалізувати?}
    B -->|Copy-Paste| C[✗ Дублювання коду]
    B -->|Абстракція| D[✓ Створення методу/класу]

    C --> E[Проблеми при змінах]
    D --> F[Легка підтримка]

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#f59e0b,stroke:#b45309,color:#ffffff
    style C fill:#ef4444,stroke:#dc2626,color:#ffffff
    style D fill:#10b981,stroke:#059669,color:#ffffff
    style E fill:#ef4444,stroke:#dc2626,color:#ffffff
    style F fill:#10b981,stroke:#059669,color:#ffffff

Best Practices

Коли застосовувати DRY:
  1. Правило трьох (Rule of Three): Перший раз — напишіть код. Другий раз — допустимо скопіювати. Третій раз — рефакторте!
  2. Логіка, яка змінюється разом: Якщо дві частини коду завжди змінюються разом, вони мають бути об'єднані
  3. Бізнес-правила: Валідація, розрахунки, алгоритми — завжди в одному місці
  4. Константи та "магічні числа": Винесіть в константи/конфігурацію
Коли НЕ застосовувати DRY:
  • Випадковий збіг коду
  • Різні бізнес-концепції, що випадково мають схожу реалізацію
  • Передчасна оптимізація (не варто створювати абстракції "на всякий випадок")

KISS (Keep It Simple, Stupid)

Визначення та Формулювання

KISS Principle: Більшість систем працюють найкраще, якщо вони залишаються простими, а не ускладненими.

Альтернативна формулювка: Простота має бути ключовою метою в дизайні, а непотрібної складності слід уникати.

KISS не означає "примітивно" або "без функціональності". Це означає:
  • Simple (Просто): Легко зрозуміти та пояснити
  • Not Easy (Не обов'язково легко): Потрібна думка та зусилля, щоб зробити щось простим
"Простота — це остаточна витонченість" — Leonardo da Vinci

Простота vs. Легкість

Simple (Просте)Easy (Легке)
Одна відповідальністьШвидко написати
Зрозуміла структураНе вимагає думки
Мінімум залежностейCopy-paste рішення
Приклад: Окремі маленькі класиПриклад: Один великий клас з усім
Легке не завжди просте! Найлегше — це скопіювати код. Але це створює складність в майбутньому. Простота вимагає зусиль для правильного проектування.

Приклад Порушення KISS: Over-Engineering

OverEngineered.cs
/// <summary>
/// Надмірно складна система для простого завдання
/// </summary>
public interface ICalculationStrategy
{
    decimal Execute(decimal a, decimal b);
}

public class AdditionStrategy : ICalculationStrategy
{
    public decimal Execute(decimal a, decimal b) => a + b;
}

public class CalculationContext
{
    private readonly ICalculationStrategy _strategy;

    public CalculationContext(ICalculationStrategy strategy)
    {
        _strategy = strategy;
    }

    public decimal Calculate(decimal a, decimal b)
    {
        return _strategy.Execute(a, b);
    }
}

public class CalculationFactory
{
    public ICalculationStrategy CreateStrategy(string operation)
    {
        return operation switch
        {
            "add" => new AdditionStrategy(),
            _ => throw new NotSupportedException()
        };
    }
}

// Використання для простого додавання!
var factory = new CalculationFactory();
var strategy = factory.CreateStrategy("add");
var context = new CalculationContext(strategy);
var result = context.Calculate(5, 3); // Просто додати 5 + 3!
Проблема: Для простого додавання двох чисел створено:
  • 3 класи
  • 1 інтерфейс
  • 1 фабрика
  • Strategy pattern (який не потрібен для такої простої задачі)
Це класичний over-engineering — надмірне ускладнення простого завдання.

Просте Рішення

Simple.cs
/// <summary>
/// Просте рішення для простого завдання
/// </summary>
public static class Calculator
{
    public static decimal Add(decimal a, decimal b) => a + b;
    public static decimal Subtract(decimal a, decimal b) => a - b;
    public static decimal Multiply(decimal a, decimal b) => a * b;
    public static decimal Divide(decimal a, decimal b) =>
        b != 0 ? a / b : throw new DivideByZeroException();
}

// Використання
var result = Calculator.Add(5, 3); // Просто та зрозуміло
Правило: Використовуйте найпростіше рішення, яке вирішує поточну задачу. Додавайте складність лише коли вона стає необхідною.

Principle of Least Surprise (POLA)

Частина KISS — це Principle of Least Surprise: код має поводитися так, як очікує розробник.

// Несподівана поведінка
public class User
{
    public string Name { get; set; }

    // Несподівано! Getter робить I/O операцію
    public string Email => LoadEmailFromDatabase();

    private string LoadEmailFromDatabase()
    {
        // Запит до БД кожен раз!
        return "user@example.com";
    }
}

// Використання
var user = new User();
var email1 = user.Email; // Запит до БД
var email2 = user.Email; // Ще один запит до БД
// Несподівано: просте читання property робить два запити!

Best Practices

Як дотримуватись KISS:
  1. Пишіть код для людей, не для машин: Називайте змінні зрозуміло, уникайте хитрих трюків
  2. Уникайте передчасної оптимізації: "Premature optimization is the root of all evil" — Donald Knuth
  3. Вирішуйте поточну проблему: Не додавайте функціональність "на всякий випадок" (YAGNI)
  4. Читабельність > Короткість: Код читають набагато частіше, ніж пишуть
  5. Один рівень абстракції: Метод має працювати на одному рівні абстракції

YAGNI (You Aren't Gonna Need It)

Визначення та Формулювання

YAGNI Principle: Завжди імплементуйте речі, коли вони дійсно потрібні, а не коли ви просто передбачаєте, що вони можуть знадобитися.

— Extreme Programming

Проста формулювка: Не пишіть код для функціональності, яка може знадобитися в майбутньому. Пишіть лише те, що потрібно зараз.

YAGNI тісно пов'язаний з Agile та Iterative Development: розробляйте інкрементально, додаючи функціональність тоді, коли вона дійсно потрібна, а не коли "може колись знадобитися".

Speculative Generality Anti-Pattern

Speculative Generality (Спекулятивна Загальність) — створення абстракцій та гнучкості для можливих майбутніх потреб.

SpeculativeGenerality.cs
/// <summary>
/// Зараз підтримується тільки SQL, але "може в майбутньому" потрібна буде MongoDB
/// Створюємо абстракцію "на всякий випадок"
/// </summary>
public interface IRepository<T>
{
    void Save(T entity);
    T GetById(object id);
    IEnumerable<T> GetAll();
    void Delete(object id);
    void Update(T entity);
}

public interface IUnitOfWork
{
    void BeginTransaction();
    void Commit();
    void Rollback();
}

public class GenericRepository<T> : IRepository<T>
{
    // Складна generic реалізація
    // ...100+ рядків коду
}

// Використовується тільки для User!
public class UserService
{
    private readonly IRepository<User> _repository;

    public UserService(IRepository<User> repository)
    {
        _repository = repository;
    }

    // Використовується лише Save та GetById
}
Проблеми Speculative Generality:
  1. Витрачений час: Написали 100+ рядків generic коду, який може ніколи не знадобитися
  2. Складність: Код складніший, ніж потрібно зараз
  3. YAGNI порушено: Створили абстракції для майбутніх сценаріїв, які можуть не настати
  4. Ризик помилки: Чим більше коду, тим більше місць для багів

Правильний Підхід: Iterative Development

YAGNI_Approach.cs
/// <summary>
/// Версія 1: Просто збереження користувачів (що потрібно ЗАРАЗ)
/// </summary>
public class UserRepository
{
    private readonly string _connectionString;

    public UserRepository(string connectionString)
    {
        _connectionString = connectionString;
    }

    public void SaveUser(User user)
    {
        using var connection = new SqlConnection(_connectionString);
        connection.Open();
        // SQL логіка
    }

    public User? GetUserById(Guid id)
    {
        // SQL логіка
        return null;
    }
}

// Якщо ПІЗНІШЕ дійсно знадобиться MongoDB:
// 1. Створимо інтерфейс IUserRepository
// 2. UserRepository : IUserRepository
// 3. MongoUserRepository : IUserRepository
// Але ЛИШЕ коли це дійсно потрібно, а не "на всякий випадок"
YAGNI не заборонює рефакторинг! Коли з'являється реальна потреба в абстракції (наприклад, потрібна друга БД), ви тоді рефакторите код. Сучасні IDE роблять рефакторинг легким та безпечним.

YAGNI та Agile

Loading diagram...
graph LR
    A[Поточні вимоги] --> B[Імплементація]
    B --> C[Delivery]
    C --> D[Нові вимоги]
    D --> E[Рефакторинг +<br/>Нова функціональність]
    E --> C

    style A fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style B fill:#10b981,stroke:#059669,color:#ffffff
    style C fill:#f59e0b,stroke:#b45309,color:#ffffff
    style D fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style E fill:#10b981,stroke:#059669,color:#ffffff

Коли YAGNI НЕ Застосовується

Винятки з YAGNI:
  1. Безпека: Шифрування, автентифікація, авторизація — краще передбачити одразу
  2. Масштабованість на рівні архітектури: Якщо ви знаєте, що навантаження зросте (наприклад, startup з очікуваним ростом)
  3. Складно змінити пізніше: Базова структура БД, API контракти для зовнішніх клієнтів
  4. Regulatory requirements: Вимоги законодавства (GDPR, PCI DSS тощо)
Але навіть тут — додавайте лише необхідний мінімум, не більше.

Best Practices

Баланс між YAGNI та планув анням:
YAGNI (Робити)NOT YAGNI (Не робити)
Імплементувати поточні вимогиДодавати "корисні" features "на всякий випадок"
Писати тести для поточної функціональностіПисати тести для неіснуючих кейсів
Проектувати для поточних потребСтворювати generic frameworks
Рефакторити при появі нових вимогРобити "гнучкість" заздалегідь
Питання перед додаванням коду:
  • Чи потрібно це зараз?
  • Чи є конкретна вимога?
  • Чи використається це в поточному спринті/релізі?
Якщо "ні" — не пишіть. YAGNI!

Law of Demeter (Principle of Least Knowledge)

Визначення та Формулювання

Law of Demeter: Об'єкт має спілкуватися лише зі своїми "безпосередніми друзями" і не повинен знати про внутрішню структуру об'єктів, які йому повертаються.

Альтернативна назва: Principle of Least Knowledge (Принцип Найменшого Знання)

Правила Law of Demeter

Метод об'єкта O може викликати методи лише таких об'єктів:

Сам об'єкт (this)

Методи самого об'єкта завжди доступні.

public void DoSomething()
{
    this.HelperMethod(); // ✓ Дозволено
}

Параметри методу

Об'єкти, передані як параметри в метод.

public void ProcessOrder(Order order)
{
    order.Confirm(); // ✓ Дозволено
}

Об'єкти, створені всередині методу

Локальні змінні, створені в методі.

public void CreateUser()
{
    var user = new User();
    user.SetName("John"); // ✓ Дозволено
}

Поля/властивості об'єкта

Безпосередні залежності об'єкта.

private readonly ILogger _logger;

public void Log(string message)
{
    _logger.Log(message); // ✓ Дозволено
}

"Dot Counting" Anti-Pattern

Симптом порушення LoD: багато крапок в ланцюжку викликів.

ViolatesLoD.cs
/// <summary>
/// Порушення Law of Demeter: "знає занадто багато"
/// </summary>
public class OrderProcessor
{
    public void ProcessOrder(Customer customer)
    {
        // "Dot counting" - багато крапок!
        var street = customer.GetAddress().GetStreet().GetName();

        // Ще гірше: ми знаємо внутрішню структуру Address і Street
        // Якщо структура зміниться, OrderProcessor зламається
        var discount = customer.GetMembershipLevel().GetBenefits().GetDiscount();
    }
}
Проблеми "dot counting":
  1. Тісна зв'язаність: OrderProcessor залежить від внутрішньої структури Customer, Address, Street
  2. Ламка архітектура: Зміна в Address або Street може зламати OrderProcessor
  3. Порушення інкапсуляції: OrderProcessor знає занадто багато про внутрішні деталі
  4. Важко тестувати: Потрібно мокати всі проміжні об'єкти

Рефакторинг до Law of Demeter

/// <summary>
/// Customer приховує внутрішню структуру та надає методи для доступу
/// </summary>
public class Customer
{
    private readonly Address _address;
    private readonly MembershipLevel _membershipLevel;

    public Customer(Address address, MembershipLevel membershipLevel)
    {
        _address = address;
        _membershipLevel = membershipLevel;
    }

    // Tell, Don't Ask: замість геттерів надаємо поведінку
    public string GetStreetName()
    {
        return _address.GetStreetName(); // Делегування
    }

    public decimal GetMembershipDiscount()
    {
        return _membershipLevel.GetDiscount(); // Делегування
    }
}

Tell-Don't-Ask Principle

Tell-Don't-Ask — це пов'язаний принцип, який каже: замість того, щоб запитувати об'єкт про його стан і приймати рішення, скажіть об'єкту, що робити.

// "Ask" - Запитуємо та приймаємо рішення ззовні
public class OrderService
{
    public void ProcessOrder(Order order)
    {
        // Запитуємо стан
        if (order.Status == OrderStatus.Pending)
        {
            if (order.TotalAmount > 0)
            {
                // Приймаємо рішення ззовні
                order.Status = OrderStatus.Confirmed;
                SaveOrder(order);
            }
        }
    }
}

Візуалізація Law of Demeter

Loading diagram...
graph TD
    subgraph "Порушення LoD"
        A1[OrderProcessor] -->|знає про| B1[Customer]
        A1 -->|знає про| C1[Address]
        A1 -->|знає про| D1[Street]
        A1 -->|знає про| E1[MembershipLevel]
    end

    subgraph "Дотримання LoD"
        A2[OrderProcessor] -->|знає тільки про| B2[Customer]
        B2 -.->|приховує| C2[Address]
        C2 -.->|приховує| D2[Street]
        B2 -.->|приховує| E2[MembershipLevel]
    end

    style A1 fill:#ef4444,stroke:#dc2626,color:#ffffff
    style A2 fill:#10b981,stroke:#059669,color:#ffffff
    style B2 fill:#3b82f6,stroke:#1d4ed8,color:#ffffff

Best Practices

Дотримання Law of Demeter:
  1. Уникайте ланцюжків: Якщо бачите a.b().c().d() — це ознака проблеми
  2. Делегування: Додайте методи-делегати в проміжні класи
  3. Tell-Don't-Ask: Говоріть об'єктам, що робити, замість запитів про стан
  4. Fluent API — виняток: Ланцюжки в Fluent API (builder pattern) — це OK, бо це один об'єкт
    // OK: це один об'єкт (builder)
    var query = queryBuilder
        .Where(x => x.Age > 18)
        .OrderBy(x => x.Name)
        .Take(10);
    

Практика та Резюме

Практичні Завдання

Перевірте своє розуміння принципів проектування на трьох рівнях складності.

Завдання Рівня 1 (Beginner): Виявлення Порушень

Мета: Навчитись розпізнавати порушення принципів у коді.

Завдання Рівня 2 (Intermediate): Рефакторинг

Мета: Практикувати рефакторинг коду для дотримання принципів.

Завдання Рівня 3 (Advanced): Проектування з Нуля

Мета: Застосувати всі принципи при проектуванні нової системи.


Резюме: Всі Принципи Разом

SOLID

P: Single Responsibility
O: Open/Closed
L: Liskov Substitution
I: Interface Segregation
D: Dependency Inversion

Основа об'єктно-орієнтованого проектування

Інші Принципи

DRY: Don't Repeat Yourself
KISS: Keep It Simple, Stupid
YAGNI: You Aren't Gonna Need It
LoD: Law of Demeter

Направляють до простоти та maintainability

Взаємозв'язок SOLID Принципів

Loading diagram...
graph LR
    SRP[SRP<br/>Одна відповідальність] -->|Допомагає| ISP[ISP<br/>Маленькі інтерфейси]
    ISP -->|Полегшує| DIP[DIP<br/>Залежність від абстракцій]
    OCP[OCP<br/>Розширення через абстракції] -->|Базується на| DIP
    LSP[LSP<br/>Поведінкова сумісність] -->|Забезпечує| OCP

    style SRP fill:#3b82f6,stroke:#1d4ed8,color:#ffffff
    style OCP fill:#10b981,stroke:#059669,color:#ffffff
    style LSP fill:#f59e0b,stroke:#b45309,color:#ffffff
    style ISP fill:#64748b,stroke:#334155,color:#ffffff
    style DIP fill:#ef4444,stroke:#dc2626,color:#ffffff

Взаємозв'язок Принципів

ПринципСприяєКонфліктує
SRPISP, DIP, DRY-
OCPDIP, Strategy PatternYAGNI (якщо передчасно)
LSPOCP (корректність підстановки)-
ISPSRP, DIP-
DIPOCP, Testability-
DRYMaintainabilityKISS (якщо абстракція складна)
KISSReadabilityDRY (іноді дублювання простіше)
YAGNIKISS, AgileOCP (якщо потрібна гнучкість)
LoDLow Coupling-
Важливо: Принципи — це не догми, а guidelines. Іноді принципи конфліктують, і потрібно знайти баланс. Досвід допоможе розуміти, коли та які принципи застосовувати.

Ключові Висновки

  1. SOLID — фундамент якісної архітектури в ООП
  2. DRY — уникайте дублювання знань, не коду
  3. KISS — простота — ключ до maintainability
  4. YAGNI — не передбачайте майбутнє, розробляйте ітеративно
  5. Law of Demeter — мінімізуйте зв'язаність через приховування структури

Подальше Вивчення

Тепер, коли ви розумієте принципи проектування, наступні кроки:

Design Patterns

Вивчіть класичні шаблони проектування (Gang of Four), які реалізують ці принципи

Clean Architecture

Дізнайтесь, як організувати всю архітектуру додатку на основі принципів

Domain-Driven Design

Поглибте знання проектування складних бізнес-систем
Практика, практика, практика! Найкращий спосіб освоїти принципи — застосовувати їх у реальних проєктах. Починайте з code review свого коду, шукайте порушення принципів та рефакторте.