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

Проектирование и архитектура ПО

Основы объектно-ориентированного программирования (ООП)​

Инвариантность, ковариантность и контравариантность​

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

  • Инвариантность: типы должны совпадать в точности.
  • Ковариантность: позволяет дочернему классу возвращать более конкретный тип, чем указано в родителе (например, parent::make(): Animal может быть переопределён как child::make(): Dog). Поддерживается в PHP 7.4+ для возвращаемых типов.
  • Контравариантность: позволяет дочернему классу принимать менее конкретный (более широкий) тип, чем в родителе (например, parent::eat(Dog) может быть переопределён как child::eat(Animal)). Поддерживается в PHP 7.4+ для типов параметров.

Композиция vs Наследование​

  • Наследование (отношение «is-a»): используется, когда класс является специализированной версией другого. Основные риски: проблема «хрупкого базового класса» (Fragile Base Class) и сильная связанность (tight coupling).
  • Композиция (отношение «has-a»): используется, когда класс включает другой класс для выполнения задачи. Преимущества: гибкость, возможность смены поведения в рантайме, слабая связанность.
  • Правило: «Предпочитайте композицию наследованию» (Favor composition over inheritance).

Почему геттеры и сеттеры (иногда) считаются антипаттерном​

Их избыточность часто приводит к появлению анемичной доменной модели (Anemic Domain Model), где объекты превращаются в простые мешки с данными (DSP — Data Structure Projections) без собственного поведения. Это нарушает инкапсуляцию, так как внутреннее состояние объекта выставляется наружу.

  • Рекомендуемый подход: Принцип «Tell, Don't Ask» (Говори, а не спрашивай). Вместо того чтобы запрашивать состояние объекта, выполнять логику и записывать результат обратно, следует делегировать выполнение действия самому объекту.

Service Locator vs IoC-контейнер (Dependency Injection)​

  • Service Locator (Сервис-локатор): паттерн, при котором объект самостоятельно «обращается» к центральному реестру для получения своих зависимостей.
    • Минусы: скрытые зависимости, затруднённое тестирование, нарушение принципа инверсии зависимостей (DIP).
  • IoC-контейнер (Dependency Injection): зависимости «внедряются» в объект извне (через конструктор или сеттер). Объект остается пассивным и не знает, как создаются его зависимости.
    • Плюсы: явные зависимости, простота мокирования при тестировании, слабая связанность кода.

GRASP (General Responsibility Assignment Software Patterns)​

Набор из 9 фундаментальных принципов распределения ответственности между классами.

  • Information Expert (Информационный эксперт): ответственность должна быть возложена на класс, обладающий максимумом информации для её выполнения.
  • Creator (Создатель): определяет, кто должен отвечать за создание экземпляров того или иного класса.
  • Controller (Контроллер): отвечает за обработку системных событий.
  • Low Coupling (Слабая связанность): минимизация зависимостей между классами для повышения их автономности.
  • High Cohesion (Высокая связность): группировка связанной логики внутри одного класса.
  • Indirection (Посредник): использование промежуточного объекта для разрыва прямой связи между компонентами.
  • Polymorphism (Полиморфизм): обработка вариативности логики на основе типов.
  • Pure Fabrication (Чистая выдумка): создание искусственного класса (например, сервиса), не имеющего аналога в предметной области, для соблюдения принципов слабой связанности и высокой связности.
  • Protected Variations (Защищённые изменения): защита компонентов от изменений в других частях системы путём оборачивания нестабильных участков интерфейсом.

Архитектурные принципы​

Закон Деметры (Law of Demeter / Principle of Least Knowledge)​

«Общайтесь только с непосредственными друзьями». Объект не должен «пробираться» через цепочку вызовов к глубоко вложенным объектам (например, избегайте $user->getAccount()->getTransactions()->getFirst()).

Анемичная доменная модель (Anemic Domain Model)​

Модель, в которой объекты содержат данные (свойства), но лишены логики поведения. Логика в таком случае выносится в отдельные «сервисы». В контексте DDD это считается антипаттерном.

DDD (Domain-Driven Design)​

Методология разработки, ориентированная на глубокое изучение предметной области и её бизнес-логики.

Основные строительные блоки:​

  • Entity (Сущность): объект, обладающий уникальным идентификатором (ID), который сохраняется при изменении его свойств (например, User).
  • Value Object (Объект-значение): объект, определяемый только набором своих атрибутов. У него нет ID, и он должен быть неизменяемым (immutable). Два объекта-значения с одинаковыми свойствами считаются идентичными (например, Money, Address).
  • Aggregate (Агрегат): группа связанных доменных объектов, которые рассматриваются как единое целое для обеспечения целостности данных. У агрегата всегда есть Aggregate Root (корень агрегата) — единственная точка входа для работы с ним.
  • Repository (Репозиторий): интерфейс для извлечения и сохранения агрегатов.
  • Domain Service (Доменный сервис): инкапсулирует бизнес-логику, которая по своей природе не принадлежит конкретной сущности или объекту-значению.
  • DTO (Data Transfer Object): простая структура данных (без логики) для передачи информации между слоями.

Hexagonal Architecture (Ports & Adapters) vs Onion Architecture​

Оба подхода нацелены на изоляцию ядра приложения (бизнес-логики) от внешних деталей реализации (БД, API, UI).

Hexagonal Architecture (Гексагональная архитектура / Порты и адаптеры)​

Предложена Алистером Кокберном. Фокусируется на разделении «внутреннего» (ядро) и «внешнего» (инфраструктура) миров.

  • Application Core: содержит доменную логику и не зависит от внешних инструментов.
  • Ports (Порты): интерфейсы, определяемые ядром. Бывают Driven Ports (ведомые, то, что нужно ядру, например UserRepositoryInterface) и Driving Ports (ведущие, то, что ядро предоставляет наружу, например UseCaseInterface).
  • Adapters (Адаптеры): реализации, связывающие ядро с внешними системами (например, SqlUserRepository — ведомый адаптер, или UserController — ведущий адаптер).
  • Ключевая идея: Приложение может запускаться разными «драйверами» (UI, CLI, тесты) и использовать разные «ведомые» инструменты (MySQL, Redis, In-Memory), не затрагивая ядро.

Onion Architecture (Луковая архитектура)​

Предложена Джеффри Палермо. Делает упор на концентрические слои с направлением зависимостей строго внутрь.

  • Domain Model: самое сердце (Entities, Value Objects).
  • Domain Services: логика, оперирующая доменными объектами.
  • Application Services: сценарии использования (use cases), координирующие работу домена.
  • Infrastructure / UI: самый внешний слой (БД, внешние API, контроллеры).
  • Ключевая идея: «Зависимости направлены внутрь». Ядро предметной области абсолютно не зависит от деталей реализации внешних слоев.

Сравнение​

ХарактеристикаГексагональная архитектураЛуковая архитектура
Основной акцентГраница Внутреннее/Внешнее (Порты/Адаптеры)Концентрические слои (Зависимости внутрь)
Внутренняя структураНе регламентирует слои внутри ядраЧётко определяет слои (Domain, Application и т.д.)
АналогияШестиугольник с разными «гранями» (портами)Луковица с несколькими «слоями»
СходстваОба используют DIP для защиты ядра от инфраструктурыОба используют DIP для защиты ядра от инфраструктуры