Перейти к основному содержимому

6.11. Проектирование

Разработчику Архитектору Аналитику

Проектирование

Проектирование ПО включает в себя три важных компонента – подходы, принципы и паттерны. С одной стороны, покажется, будто это одно и то же, но на самом деле всё немного глубже, и мы изучим это.

ПО и как его проектируют
В этой главе мы не углубляемся в паттерны, проектирование БД, начнём с принципов и подходов.
Есть три понятия - подходы к проектированию, паттерны проектирования, принципы проектирования. Выделяют ещё архитектурные паттерны, и паттерны программирования.
Проектирование сервисов и методов – как выполнять, алгоритм
Проектирование БД – как выполнять, алгоритм;
Проектирование функциональных UI – как выполнять, алгоритм;
Чем проектирование интерфейса (функционального UI) отличается от работы художника?

Подходы к проектированию отвечают на вопрос «как мы начинаем проектировать систему?». Мы выделим следующие подходы:

  • Code First (сначала код, потом данные);
  • Database First (сначала данные, потом код);
  • ETL и ELT (особенности обработки данных).
ПодходСутьПример
Code FirstСначала пишутся классы и бизнес-логика, а база данных генерируется автоматически. Работа с данными осуществляется через код. Направление: Код → База данныхEntity Framework (.NET); Django (Python)
Database FirstСначала проектируется и создаётся база данных, затем на её основе генерируется код. Направление: База данных → КодSQL Server + ADO.NET
ETLExtract → Transform → Load Данные извлекаются, преобразуются и затем загружаются в хранилище. Суть подхода: «Привёл в порядок и положил на полку». Извлечение → Преобразование → ЗагрузкаКогда нужно очистить данные перед анализом. Пример: 1. Чтение CSV-файла; 2. Удаление дубликатов, проверка корректности и конвертация; 3. Загрузка преобразованного CSV в БД.
ELTExtract → Load → Transform Данные извлекаются и загружаются в хранилище «как есть», преобразование выполняется уже внутри хранилища. Суть подхода: «Бросил в корзину, разберу потом». Извлечение → Загрузка → ТрансформацияBig Data. Получены сырые данные — сразу загружаются, нормализация выполняется позже. Пример: 1. Сбор логов; 2. Загрузка данных в облако; 3. Выполнение SQL-запросов для трансформации.

Разница между ELT и ETL:

ETL (Extract, Transform, Load)

image.png

Извлечение (Extract)

Данные собираются из различных источников (например, базы данных, файлы, API).

Преобразование (Transform)

Данные преобразуются на промежуточном сервере (например, очистка, фильтрация, агрегация).

Загрузка (Load)

Преобразованные данные загружаются в целевую базу данных.

image-1.png

Извлечение (Extract)

Данные собираются из различных источников.

Загрузка (Load)

Необработанные данные загружаются напрямую в целевую базу данных.

Преобразование (Transform)

Данные преобразуются уже внутри целевой базы данных (например, с помощью SQL или аналитических инструментов).

Принципы проектирования

Принципы проектирования – фундаментальные правила, которые помогают избегать хаоса:

  • SOLID;
  • DRY;
  • KISS;
  • Закон Конвея;
  • SOC.
ПринципСуть
SOLIDАббревиатура из пяти принципов объектно-ориентированного дизайна, предложенных Робертом Мартином, которые помогают делать код гибким, расширяемым и легко поддерживаемым.
Single Responsibility (SRP)Класс решает одну задачу. Должен иметь только одну причину для изменения — одну зону ответственности.
Open-Closed (OCP)Код можно расширять, но не изменять. Классы должны быть открыты для расширения (через наследование или композицию), но закрыты для модификации существующего кода.
Liskov Substitution (LSP)Подтипы должны корректно заменять базовый тип. Наследник не должен нарушать поведение, ожидаемое от родительского класса.
Interface Segregation (ISP)Предпочтительнее множество узкоспециализированных интерфейсов, чем один общий. Клиенты не должны зависеть от методов, которые они не используют.
Dependency Inversion (DIP)Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракций. Зависимости — от интерфейсов, а не от конкретных реализаций.
DRYDon’t Repeat Yourself — «Не повторяйтесь». Дублирование кода следует избегать: общую логику нужно выносить в отдельные функции, классы или модули.
KISSKeep It Simple, Stupid — сохраняйте простоту. Более простые решения предпочтительнее сложных, если они решают задачу эффективно.
Закон КонвеяАрхитектура системы отражает организационную структуру команды, которая её разрабатывает.
SOCSeparation of Concerns — разделение ответственности. Различные аспекты системы должны быть независимыми и реализованы в отдельных компонентах.

Закон Конвея подразумевает, что любая организация, проектирующая систему, будет производить дизайн, который копирует структуру её коммуникационных структур. Если у нас есть 5 отделов, каждый из которых отвечает за свой модуль, то система будет состоять из 5 компонентов, соединённых между собой через интерфейсы или API, даже если логично было бы сделать иначе. Пример - отделы бэкенда, фронтенда и мобильная команда. При проектировании системы нужно учитывать структуру команд.

SOC - Separation of Concerns, или разделение ответственности, предполагает разделение разных аспектов системы на отдельные модули, когда бизнес-логика остаётся в доменном слое, а представление в UI. Разделение помогает упрощать тестирование, читаемость и поддержку кода.

Давайте рассмотрим SOLID чуть глубже.

image-2.png

  1. S – Single Responsibility Principle (SRP), или принцип единственной ответственности актора подразумевает, что каждый актор имеет свою зону ответственности.

Актор - это внешняя сущность, взаимодействующая с системой. Обычно это пользователь, но может быть и другой сервис или система. И как раз каждый актор должен иметь только одну причину изменить систему.

К примеру, у нас есть три актора:

  • Администратор - добавляет товары;
  • Клиент - делает заказы;
  • Система оплаты - обрабатывает платежи.

И каждый из них имеет свою зону ответственности, не отвечает за работу другого, что позволяет избегать ситуации, когда один и тот же объект выполняет слишком много функций.

Словом, нужно разделять ответственность.

  1. O – Open/Closed Principle, или принцип открытости/закрытости сформулирован Бертраном Мейером в книге «Object-Oriented Software Construction» как «Программные сущности должны быть открыты для расширения, но закрыты для модификации». Вместо того чтобы менять существующий код, лучше добавить новое поведение через наследование или интерфейсы.

Архитектура строится таким образом, что внутренние слои ничего не знают о внешних. То есть, ядро бизнес-логики не зависит от БД, UI или веб-интерфейса. Внешние слои (контроллеры, представления) зависят от ядра. Такой подход реализуется в архитектуре «Clean Architecture», где зависимости направлены только внутрь.

К примеру:

  • пользователь нажимает кнопку «Купить»;
  • контроллер запускает интерактор;
  • интерактор вызывает бизнес-логику, сохраняет заказ;
  • презентатор формирует сообщение «Заказ оформлен»;
  • представление показывает это пользователю.

Это элементы архитектурного паттерна MVP, используемые для разделения обязанностей. Интерактор содержит бизнес-логику, вызывает методы домена, контроллер управляет навигацией и принимает входящие запросы, представление отображает данные пользователю, а презентатор форматирует данные для вывода, передавая в представление.

  1. L – Liskov Substitution Principle (LSP), Барбара Лисков и принцип подстановки гласит: «Функции, которые используют указатель или ссылку на родительский класс, должны иметь возможность использовать объекты дочернего класса, не зная об этом». То есть, подкласс должен допускать замену базового класса без нарушения логики программы.

Обычно в пример приводят, что круг не может быть прямым подклассом прямоугольника, ведь его поведение отличается. Нужно создать отдельную иерархию, где прямоугольник и круг имеют общего предка, например «фигура», но не наследуют друг от друга, становясь равноправными наследниками фигуры, без противоречий.

Барбара Хьюз Лисков получила премию Тьюринга в 2008 году за работу в области абстракции данных и объектно-ориентированного программирования, разработала концепцию абстрактных типов данных ещё в 1970-х, создала язык CLU (1975), один из первых языков, поддерживающих понятия итераторов, модулей и типов данных с множественным возвращаемым значением, и сформулировала принцип подстановки. Многие современные языки, такие как Java, C#, Python, реализуют идеи, впервые описанные ею.

Таким образом, принцип подстановки определяет, что подклассы должны вести себя так же, как и их родитель. Они могут добавлять новое поведение, но не должны нарушать ожидания, связанные с родительским классом.

  1. I – Interface Segregation Principle (ISP), или принцип разделения интерфейсов гласит, что лучше множество специализированных интерфейсов, чем один «толстый» - клиенты не должны зависеть от интерфейсов, которые они не используют.

Принцип был сформулирован Робертом Си Мартином как часть его работы над объектно-ориентированным проектированием. Он основывался на опыте реальных проектов, где большие интерфейсы становились причиной множества багов и проблем с поддержкой кода. И для решения проблемы, создаются мелкие «тонкие» интерфейсы.

public interface IPrinter // Интерфейс 1
{
void Print(Document document);
}

public interface IScanner // Интерфейс 2
{
void Scan(Document document);
}

public interface IFaxMachine // Интерфейс 3
{
void Fax(Document document);
}

public class SimplePrinter : IPrinter // Наследование только от 1
{
public void Print(Document document)
{
Console.WriteLine("Printing document...");
}
}
// Множественная реализация - класс реализует все три интерфейса
public class OfficePrinter : IPrinter, IScanner, IFaxMachine
{
public void Print(Document document) => Console.WriteLine("Printing...");
public void Scan(Document document) => Console.WriteLine("Scanning...");
public void Fax(Document document) => Console.WriteLine("Faxing...");
}

public class ScannerOnly : IScanner
{
public void Scan(Document document) => Console.WriteLine("Scanning only...");
}

Множественная реализация - это не множественное наследование как таковое. Интерфейсы как раз используются для определения методов, свойств, событий, индексаторов, могут быть реализованы несколькими классами, а один класс может реализовывать несколько интерфейсов, как приведено в примере выше.

  1. D – Dependency Inversion Principle (DIP), или инверсия зависимостей предполагает зависимость от абстракций, а не от конкретных реализаций.

Обычно для разъяснения делят на два вида компонентов - абстракции и модули, причём модули могут быть верхнего уровня и нижнего уровня. Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Смысл в том, чтобы писать программу, изначально допуская возможность замены функциональности, к примеру, чтобы класс App с логикой работы приложения мог работать независимо от того, будет выбран способ сохранения данных в БД или в файл. Базу данных можно поменять, к примеру, с MSSQL на PostgreSQL, и этот переход должен быть безболезненным, поэтому ответственный компонент выделяют, и при смене стратегии придётся переделывать лишь этот ответственный компонент (а не всё целиком).