← На главную ITMO Hub
Архитектурный фундамент SOLID (5) GRASP (Craig Larman)

Принципы SOLID и GRASP в ООП C#

Полный теоретический путеводитель по каноническим принципам проектирования: подробный разбор каждого принципа, визуальные схемы, антипаттерны, примеры кода на C# и корреляция между SOLID и GRASP по программе кафедры ИТМО.

S

Single Responsibility Principle (SRP) — Принцип единственной ответственности

Роберт Мартин
📌 Дословная формулировка из презентации лекций:
«Проектирование типов, таким образом что они имеют единственную причину для изменения»

Классический вариант Роберта Мартина: «A class should have one, and only one, reason to change». Под модулем на кафедре понимается любой тип в коде, инкапсулирующий логику и состояние.

❌ Нарушение SRP (Класс-бог):

Класс Tare сам хранит товары, сам считает весы, сам лезет в базу данных и сам проверяет товарное соседство через правила пожарной безопасности.

Причина проблемы: изменился формат БД — меняем Tare. Изменились правила пожарников — опять меняем Tare.

✅ Соблюдение SRP (Декомпозиция):

Tare отвечает только за состояние тары. Проверка совместимости вынесена в NeighborhoodValidator. Сохранение — в ITareRepository.

Каждый класс меняется только по своей единственной изолированной причине.

O

Open/Closed Principle (OCP) — Принцип открытости / закрытости

Бертран Мейер
📌 Дословная формулировка из презентации лекций:
«Проектирование типов, таким образом что их логику можно расширять, не изменяя их исходный код.
Тип должен быть открытым для расширения, но закрытым для изменений»

Мы должны иметь возможность добавлять в систему новое поведение без переписывания и правки существующего протестированного кода.

❌ Нарушение OCP:

Внутри сервиса написан гигантский switch (ruleType). Чтобы поддержать правило проверки срока годности, приходится лезть внутрь существующего метода и дописывать новый case, рискуя сломать старое.

✅ Соблюдение OCP:

Выделен интерфейс IDistributionRule. Новое поведение добавляется созданием нового класса (например, ExpirationRuleDecorator), а старые классы тар и палет не меняются ни на символ.

L

Liskov Substitution Principle (LSP) — Принцип подстановки Барбары Лисков

Барбара Лисков
📌 Дословная формулировка из презентации лекций:
«Проектирование иерархий типов, таким образом что логика дочерних типов не нарушает инвариант и интерфейс родительских типов»
Классический пример нарушения: «Квадрат наследуется от Прямоугольника». При изменении ширины у квадрата неявно меняется и высота, что ломает тесты, рассчитывающие на поведение обычного прямоугольника.

В нашем проекте: любое правило (TareDistributionRule, PalletDistributionRule, декоратор) можно подставить в любое место, ожидающее IDistributionRule. Ни один потомок не бросает неожиданных исключений вроде NotImplementedException и не меняет контракт контракта.

I

Interface Segregation Principle (ISP) — Принцип разделения интерфейса

Роберт Мартин
📌 Дословная формулировка из презентации лекций:
«Проектирование маленьких абстракций, которые ответственны за свой конкретный функционал, а не одной всеобъемлющей, содержащей много различного»

Назначение ISP по презентации: правило для корректного проектирования связей между типами.

❌ «Жирный» интерфейс:
interface IWarehouseManager {
  void Log(string msg);
  DateTime GetTime();
  bool CheckNeighbor();
  void SaveTare();
}

Класс, которому нужно только логирование, вынужден зависеть от методов работы с БД и временем.

✅ Узкие интерфейсы (Как у нас):
interface IRuleLogger { void Log(...); }
interface ITimeProvider { DateTimeOffset GetCurrentTime(); }
interface INeighborhoodValidator { bool CanBePlacedTogether(...); }

Каждый потребитель зависит ровно от того метода, который ему действительно нужен.

D

Dependency Inversion Principle (DIP) — Принцип инверсии зависимостей

Роберт Мартин
📌 Дословная формулировка из презентации лекций:
«Проектирование типов, таким образом что одни реализации не зависят от других напрямую»

Классическая развертка по Мартину: модули верхних уровней не должны зависеть от модулей нижних уровней (оба зависят от абстракций); абстракции не зависят от деталей реализации.

Аналогия с разъёмом USB-C: Телефон не припаян к розетке на стене. Телефон зависит от абстрактного интерфейса разъёма USB-C. С другой стороны шнура может быть розетка 220V, повербанк или прикуриватель авто.

В нашем коде: TareService не создаёт базу данных сам через new Database(). Он принимает интерфейс ITareRepository через конструктор. В тестах мы подставляем InMemoryTareRepository без реальной базы данных.