Паттерны проектирования Паттерны в реальном коде: когда они мешают
0%

Паттерны в реальном коде: когда они мешают

Паттерны в реальном коде: когда они мешают

Весь трек мы разбирали, как паттерны решают проблемы. Эта статья — про обратную сторону: каждый паттерн является платой, и довольно часто плата превышает выгоду. Это не призыв «не использовать паттерны». Это попытка дать инструмент, которого обычно не хватает: процедуру, по которой решают не применять паттерн, и процедуру, по которой применённый паттерн убирают.

Тезис статьи в одну фразу: паттерн — это ставка на будущее изменение. Ставка может не сыграть, и код с непроигравшей ставкой дешевле кода с проигравшей.


Почему это вообще проблема

Есть асимметрия, которая ломает интуицию инженеров.

Добавить абстракцию легко, убрать — почти невозможно. Когда вы вводите интерфейс PaymentGateway с одной реализацией, это 20 минут работы и приятное чувство «сделал правильно». Когда через два года выясняется, что реализация так и одна, а интерфейс успел разъехаться по 40 файлам, мокам в тестах, DI-конфигу и двум сервисам, — удаление стоит недели и никем не приоритизируется. Абстракции ведут себя как храповик: крутятся только в одну сторону.

Плюсы паттерна видны сразу, минусы — распределены во времени. Автор кода получает выгоду в момент написания (гордость, ощущение расширяемости). Издержки платят все, кто потом читает код: каждый новый разработчик тратит дни на «а где тут вообще происходит работа». Классическая проблема экстерналий.

Паттерн — это ещё и статусный сигнал. В резюме и на ревью «применил Strategy» звучит лучше, чем «написал if». Джоэл Спольски называл близкое явление «архитектурными астронавтами» — людьми, которые поднимаются на такой уровень абстракции, где кислорода уже нет.

Хорошая новость: у отрасли есть накопленный словарь для этой обратной стороны. Мартин Фаулер в «Рефакторинге» вынес отдельный запах кода — Speculative Generality, «спекулятивная обобщённость»: механизм, добавленный ради гипотетического будущего требования, которое не наступило.

Точка окупаемости паттерна

Смысл графика: у прямого кода низкая входная цена и крутой рост при каждом новом варианте (правки расползаются по нескольким местам). У паттерна высокая входная цена и пологий рост. Кривые пересекаются, и всё решает где именно вы окажетесь на оси X. Беда в том, что решение принимается в точке 0, а истинное положение точки окупаемости становится известно только задним числом.


Цена абстракции: из чего она складывается

Обсуждать «стоимость паттерна» абстрактно бесполезно. Разложим её на измеримые составляющие.

Статья издержек В чём выражается Как измерить
Когнитивная навигация Чтобы понять один сценарий, надо открыть N файлов Число прыжков «go to definition» до реальной логики
Косвенность в отладке Стектрейс длиннее, точки останова не там Доля кадров стека, не содержащих доменной логики
Поиск реализации «Кто это реализует?» — неочевидно Число реализаций интерфейса; 1 — плохой знак
Конфигурационная связность Сборка объекта размазана по DI-контейнеру Расстояние между «где создаётся» и «где используется»
Тестовая обвязка Моки на каждый шов Отношение строк setup к строкам assert
Runtime Виртуальные вызовы, аллокации обёрток Профиль; см. раздел про производительность

Ключевой практический показатель — сигнал/шум в стектрейсе. Если при разборе инцидента из девяти кадров стека смысл несут два, а семь только пересылают вызов, это и есть налог на абстракцию в чистом виде.

Налог на абстракцию в стектрейсе

Отдельно подчеркну: правый вариант на картинке не отменяет логирование, ретраи и транзакцию. Он делает их явными вызовами внутри одной функции. Декораторы окупаются, когда обвязка переиспользуется десятками команд и её состав задаётся конфигурацией; ради одного хендлера пятиэтажная цепочка — чистый убыток. Механику самих декораторов мы разбирали в структурных паттернах.


Случай 1. Интерфейс с одной реализацией

Самый частый и самый дорогой в сумме случай. Выглядит так:

# payments/gateway.py
from abc import ABC, abstractmethod

class PaymentGateway(ABC):
    """Абстракция платёжного шлюза — на случай, если появится второй провайдер."""

    @abstractmethod
    def charge(self, amount_cents: int, token: str) -> str: ...

    @abstractmethod
    def refund(self, charge_id: str) -> None: ...


# payments/stripe_gateway.py
class StripeGateway(PaymentGateway):
    def charge(self, amount_cents: int, token: str) -> str:
        resp = self._client.charges.create(amount=amount_cents, source=token)
        return resp.id

    def refund(self, charge_id: str) -> None:
        self._client.refunds.create(charge=charge_id)

Вопрос: что здесь плохого? Ответ: ничего — если второй провайдер действительно появится. Проблема в том, что происходит обычно.

Абстракция протекает по форме первой реализации. Сигнатура charge(amount_cents, token) -> str целиком списана со Stripe: копейки, одноразовый токен, строковый id. Когда через год приходит локальный провайдер, у которого нужен трёхшаговый flow с редиректом на 3-D Secure, идемпотентный ключ и асинхронный вебхук о финальном статусе, — интерфейс не подходит. Его придётся переписывать, то есть абстракция не сэкономила ничего, а стоила два года навигационного налога. Джоэл Спольски сформулировал это как «закон дырявых абстракций»: нетривиальная абстракция в какой-то степени протекает всегда.

Работающее правило. Абстракцию, предназначенную для нескольких реализаций, имеет смысл проектировать, когда у вас на руках минимум два реальных, а не воображаемых варианта. До этого момента честнее держать конкретный класс, а швы обеспечивать иначе — об этом ниже.

Отдельный законный повод завести интерфейс с одной реализацией: тестируемость границы с внешним миром (сеть, время, случайность). Но и здесь есть более дешёвые варианты:

# Вариант А: инверсия зависимости на уровне функции, без иерархии классов
def place_order(order: Order, charge: Callable[[int, str], str]) -> OrderId:
    charge_id = charge(order.total_cents, order.payment_token)
    ...

# в проде
place_order(order, stripe_gateway.charge)
# в тесте
place_order(order, lambda cents, token: "ch_test_1")
# Вариант Б: подменить транспорт, а не домен — тест бьёт по фейковому HTTP
# (responses / httpx.MockTransport / WireMock). Проверяется настоящий код шлюза,
# включая сериализацию и обработку кодов ответа, — как раз то, что ломается в проде.

Вариант Б обычно ловит больше багов: мок интерфейса проверяет, что вы вызвали свой же мок, а фейковый HTTP проверяет реальный контракт. Про то, что интерфейс должен определяться потребителем, а не поставщиком, полезно прочитать у Фаулера про Role Interfaces.


Случай 2. Strategy там, где хватало функции

// Было: «правильно»
public interface DiscountStrategy {
    Money apply(Money base, Customer c);
}

public class NoDiscount implements DiscountStrategy { /* ... */ }
public class LoyaltyDiscount implements DiscountStrategy { /* ... */ }

public class DiscountStrategyFactory {
    private final Map<CustomerTier, DiscountStrategy> registry;
    public DiscountStrategy forTier(CustomerTier tier) { return registry.get(tier); }
}

Четыре типа, DI-регистрация, фабрика — ради двух формул. В языке с функциями первого класса тот же самый Strategy выражается так:

// Стало: Strategy никуда не делся, но исчезла церемония
public record PricingRules(Map<CustomerTier, UnaryOperator<Money>> byTier) { }

// или ещё проще, пока правил мало:
Money price(Money base, Customer c) {
    return switch (c.tier()) {
        case REGULAR -> base;
        case LOYALTY -> base.minusPercent(5);
        case VIP     -> base.minusPercent(10);
    };
}

Важный момент: switch здесь не деградация. Это тот же паттерн с другим механизмом реализации. Питер Норвиг ещё в 1996 году показал в докладе «Design Patterns in Dynamic Languages», что 16 из 23 паттернов GoF в языках с функциями первого класса, замыканиями и метапрограммированием либо становятся невидимыми (растворяются в языке), либо радикально упрощаются. Каталог GoF писался под C++ 1994 года — многое в нём это обход ограничений конкретного языка, а не вечная истина.

Практическая табличка соответствий:

Паттерн GoF Языковой эквивалент, если он есть
Strategy функция/лямбда как параметр
Command замыкание, partial, Runnable
Factory Method функция-конструктор, dict[str, Callable]
Abstract Factory модуль/неймспейс с функциями
Singleton модуль (Python), sync.Once (Go), object (Kotlin/Scala)
Template Method функция высшего порядка с хуками-параметрами
Iterator генератор/yield, встроенный протокол
Visitor pattern matching по сумме типов (Rust enum, Scala ADT, match в Python 3.10+)
Decorator функция, оборачивающая функцию (@decorator в Python, middleware в Go)
Observer встроенные события, реактивные потоки, каналы

Отсюда правило: сначала спросите, есть ли у вашего языка встроенный механизм для этой силы. Если есть — паттерн уже реализован, писать иерархию классов не нужно. Механику Strategy и его родственников мы разбирали в поведенческих паттернах.


Случай 3. Observer и потеря потока управления

Самая коварная категория: паттерн работает как задумано, но делает систему непрослеживаемой.

# Публикация события выглядит невинно
def confirm_order(order_id: OrderId) -> None:
    order = repo.get(order_id)
    order.confirm()
    repo.save(order)
    events.publish(OrderConfirmed(order_id=order_id))   # ← и что дальше?

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

Разбор проблем, которые видны на диаграмме:

  1. Порядок недетерминирован, но код на него полагается. Классика: подписчик аудита читает состояние, которое ещё не записал подписчик записи.
  2. Ошибка в одном подписчике не имеет очевидного владельца. Прервать цепочку? Продолжить? Ретраить только упавшего? Синхронный in-process bus почти никогда не отвечает на это внятно.
  3. Смешаны транзакционные и нетранзакционные эффекты. Письмо не откатывается.
  4. Каскад событий: подписчик публикует событие, на которое подписан ещё кто-то. Отладка превращается в археологию.

Что делать на практике. Не «не использовать Observer», а честно выбрать один из режимов:

# Режим 1: явная оркестрация. Никакой магии — просто вызовы по порядку.
# Подходит, когда шагов мало и они принадлежат одному сервису.
def confirm_order(order_id: OrderId) -> None:
    with uow:                                   # одна транзакция
        order = uow.orders.get(order_id)
        order.confirm()
        uow.stock.reserve(order.lines)          # упадёт → откатится всё
        uow.outbox.add(OrderConfirmed(order_id))  # письмо уйдёт ПОСЛЕ коммита
    # публикация во внешнюю шину — отдельным воркером из outbox

Паттерн transactional outbox решает ровно ту проблему, которую создаёт наивный Observer: разделяет «изменилось состояние» и «мир узнал об этом», сохраняя атомарность первого. Описание у Криса Ричардсона: microservices.io/patterns/data/transactional-outbox.

# Режим 2: настоящая асинхронная шина с явными гарантиями.
# Тогда события — часть контракта, у каждого есть схема, версия, DLQ и идемпотентность.

Что плохо всегда — средний режим: синхронный in-process event bus без гарантий, введённый «чтобы развязать модули». Он даёт минусы обоих подходов (непрослеживаемость асинхронности) и плюсы ни одного (нет ни изоляции, ни надёжности).


Случай 4. Repository поверх ORM

Показательный пример паттерна, который применяют по инерции. Идея Repository из PoEAA Фаулера — «коллекция объектов домена в памяти», скрывающая доступ к данным. Но современная ORM (SQLAlchemy, Hibernate, EF Core) уже является реализацией Unit of Work и Identity Map. Обёртка поверх неё часто даёт вот что:

# Обобщённый репозиторий: выглядит красиво, работает плохо
class Repository(Generic[T]):
    def get(self, id_: UUID) -> T | None: ...
    def list(self, **filters) -> list[T]: ...
    def add(self, entity: T) -> None: ...
    def delete(self, entity: T) -> None: ...

Что ломается:

  • N+1 и жадная загрузка. Метод list() не знает, какие связи понадобятся вызывающему. Либо всегда грузим всё (медленно), либо получаем N+1. Прокинуть selectinload через обобщённый интерфейс — значит протащить деталь ORM в сигнатуру, то есть отменить смысл абстракции.
  • Проекции. Экранам нужны 4 поля из 30, но репозиторий умеет отдавать только сущности целиком.
  • Пакетные операции. UPDATE ... WHERE на миллион строк не выражается через «коллекцию в памяти».
  • Транзакционные границы. Если каждый метод коммитит сам, атомарности нет; если не коммитит, Unit of Work всё равно торчит наружу.

Итог — «репозиторий», у которого 40 методов вида find_active_by_tenant_with_last_order, то есть обычный DAO, но с лишним слоем и обещанием переносимости, которым никто не воспользуется.

Когда Repository оправдан:

  • домен богатый, а не анемичный, и вы действительно хотите защитить агрегаты от произвольных частичных обновлений (см. трек по DDD);
  • хранилищ реально несколько (Postgres + Elasticsearch + кэш) и точка выбора должна быть одна;
  • интерфейс узкий и доменный (orders.confirmed_since(date)), а не обобщённый CRUD.

Практический компромисс, который хорошо работает: репозитории на команды (запись, агрегаты, инварианты) и прямые запросы/проекции на чтение — по сути CQRS без событийной части. Фаулер описывает разделение в CQRS.


Случай 5. Наследование как «паттерн»

Формально это не паттерн из каталога, но именно здесь чаще всего срабатывает «паттерн-мышление». Template Method и иерархии Base* создают связность, которую нельзя ослабить локально.

# Так рождается ад
class BaseHandler:
    def handle(self, req):
        self.validate(req)              # хук
        data = self.load(req)           # хук
        result = self.process(data)     # хук
        return self.render(result)      # хук

class BaseAuthHandler(BaseHandler): ...
class BaseAuthPaginatedHandler(BaseAuthHandler): ...
class BaseAuthPaginatedCachedHandler(BaseAuthPaginatedHandler): ...

Через год класс OrdersHandler(BaseAuthPaginatedCachedHandler) наследует 14 хуков, из которых переопределяет 3, а поведение остальных зависит от порядка MRO. Любое изменение в BaseHandler задевает 60 наследников — это проблема хрупкого базового класса.

Замена — композиция явных шагов:

# Пайплайн вместо иерархии: состав видно в одном месте, порядок явный
def orders_handler(req):
    user = require_auth(req)
    page = parse_pagination(req)
    return cached(key=("orders", user.id, page), ttl=60)(
        lambda: render(load_orders(user, page))
    )()

Это ровно то, что описывает старое эмпирическое правило «предпочитайте композицию наследованию» — оно, кстати, сформулировано в самой книге GoF (принцип №2 введения), а не вопреки ей.


Жизненный цикл абстракции: что происходит на самом деле

Главное наблюдение из диаграммы: здоровая абстракция почти всегда приходит из дублирования, а не из предвидения. Пока у вас нет двух-трёх реальных случаев, вы не знаете, где проходит граница между общим и различным — и вырежете шов не в том месте. Это и есть содержание «правила трёх» и знаменитой формулировки Сэнди Мец: «дублирование дешевле неправильной абстракции».

Обратите внимание на петлю Окаменелость → Окаменелость. Она подписана «не трогай» и это самое дорогое состояние: слой не приносит пользы, но все боятся его убирать. Способ выйти — раздел про схлопывание ниже.


Процедура принятия решения

Замените рефлекс «здесь просится паттерн» явной проверкой.

Два узла здесь важнее остальных.

«Фиксируем в ADR, какое изменение паттерн покупает». Формулировка обязана быть проверяемой: не «для расширяемости», а «чтобы подключить второго провайдера доставки без правок в биллинге; переговоры с СДЭК идут, интеграция запланирована на Q3». Такую запись через полгода можно сверить с реальностью. Формат ADR — adr.github.io.

«Через два релиза: ставка сыграла?» Ревизия абстракций почти никогда не делается, и именно поэтому окаменелостей так много. Достаточно одного пункта в чеклисте квартального техдолга.


Где паттерны окупаются: карта

Ось Y здесь — не «важность фичи», а стоимость ошибки проектирования: насколько дорого будет переделать, если решение окажется неверным. Публичный API библиотеки почти невозможно изменить после релиза (сломаете чужой код), поэтому он живёт наверху независимо от вероятности изменений. Вёрстка админки переделывается постоянно и дёшево — там любая абстракция ради будущего избыточна.

Обратите внимание на квадрант «изменение неизбежно, но цена ошибки низкая» (расчёт скидок, вёрстка): интуиция кричит «часто меняется → нужна абстракция», но если переделка дешёвая, дешевле переделывать. Абстракция окупается высотой цены ошибки, а не частотой правок.


Паттерны и производительность

Раздел, который обычно пропускают. Косвенность не бесплатна и на уровне машины.

Мономорфные и мегаморфные точки вызова. JIT (JVM, V8) оптимизирует виртуальный вызов, если в данной точке встречается одна-две реализации: делает inline-кэш и заинлайнивает тело. Если реализаций много (в HotSpot порог — 3 типа на call site, дальше megamorphic), вызов идёт через vtable/itable, inline не происходит, и вместе с ним пропадают все последующие оптимизации в этом участке. Подробности про уровни инлайнинга — в документации HotSpot по JIT и у Алексея Шипилёва в JMH-примерах.

Практическое следствие: Strategy с двумя реализациями на горячем пути обычно бесплатен; Strategy c пятнадцатью — уже нет.

Аллокации обёрток. Пять декораторов на объект — это пять объектов на каждый вызов, если они создаются динамически. В хот-лупе это давление на GC.

Кэш инструкций и предсказание переходов. Длинная цепочка мелких методов в разных классах хуже ложится в I-cache, чем один линейный кусок.

# Микробенчмарк-иллюстрация, а не руководство к действию
import timeit

setup = """
class Base:
    def f(self, x): return x + 1
class A(Base): pass
class B(Base):
    def f(self, x): return x + 2
class C(Base):
    def f(self, x): return x + 3
mono = [A()] * 1000
mega = [A(), B(), C()] * 334
"""

print(timeit.timeit("sum(o.f(1) for o in mono)", setup, number=2000))
print(timeit.timeit("sum(o.f(1) for o in mega)", setup, number=2000))
# В CPython разница мала (интерпретатор и так динамичен),
# в JVM/V8 на аналогичном коде разрыв бывает в разы.

Правило. Не проектируйте архитектуру от производительности — но знайте, что в горячих 1% кода паттерны стоит разворачивать в плоский код, и это нормально, а не «нарушение чистоты». Сначала профиль, потом решения. Про измерения см. трек по алгоритмам.


Как убирать паттерн: схлопывание

Обратная операция называется в каталоге рефакторингов Collapse Hierarchy, Inline Class и Remove Middle Man. Безопасная последовательность:

# 1. Убедиться, что реализация действительно одна
rg -n "implements PaymentGateway|\(PaymentGateway\)|: PaymentGateway" --type py --type java

# 2. Проверить, что интерфейс не является публичным контрактом для внешних потребителей
rg -n "PaymentGateway" -g '!tests/**' -l | wc -l

# 3. Найти, кто мокает интерфейс — эти тесты придётся переписать на фейковый транспорт
rg -n "Mock\(spec=PaymentGateway\)|mock\(PaymentGateway" 

Пошагово:

  1. Слить реализацию в интерфейс (или наоборот): переименовать конкретный класс в имя интерфейса, удалить абстрактный. Компилятор/тесты подскажут все точки.
  2. Переписать тесты, которые мокали шов, на подмену транспорта. Это обычно самый долгий шаг — и одновременно тот, который повышает ценность тестов.
  3. Убрать регистрацию из DI, если единственный смысл записи был «связать интерфейс с классом».
  4. Удалить лишние слои по одному, каждый шаг — отдельный коммит с зелёными тестами.

Ключевой психологический момент: команды почти никогда не удаляют абстракции, потому что удаление не выглядит «полезной работой». Помогает переформулировка: -800 строк слоя, который ничего не абстрагирует, — это ускорение всех будущих задач в этом модуле. Полезно измерять и показывать метрику «число прыжков до логики» до и после.


Чеклист код-ревью

Практический набор вопросов, которые стоит задавать к PR с новым паттерном. Не для формального блокирования — для разговора.

  • Сколько реализаций у нового интерфейса в этом же PR? Одна — попросите обоснование.
  • Можно ли назвать конкретное будущее изменение, ради которого вводится шов? Если ответ звучит как «для гибкости» — это не ответ.
  • Есть ли встроенный механизм языка для той же силы (функция, match, генератор, модуль)?
  • Определён ли интерфейс потребителем? Проверка: если удалить существующую реализацию, выглядит ли интерфейс всё ещё осмысленно?
  • Что будет в стектрейсе при ошибке? Сколько кадров, где искать смысл.
  • Как выглядят тесты? Если тест на 30 строк мокает 4 зависимости, чтобы проверить одно сложение, — шов не там.
  • Куда переехала сложность? Паттерн редко убирает сложность, обычно перемещает её: из тела метода в граф объектов, из явного кода в конфигурацию. Стало ли лучше?
  • Обратим ли шаг? Насколько дорого будет схлопнуть это обратно через год.

Обратная сторона чеклиста — не превращайте его в дубину. Есть случаи, когда паттерн вводится заранее и это правильно: публичный API, границы между командами, точки, где переделка после релиза недопустима. Разница между «преждевременная абстракция» и «осознанное проектирование границы» проходит по наличию проверяемого обоснования, а не по количеству классов.


Типичные ошибки (сводка)

Ошибка Как выглядит Что делать
Паттерн от названия «Нам нужен Mediator» до формулировки проблемы Сначала опишите силы, потом ищите имя
Интерфейс на всякий случай IFoo + FooImpl, одна реализация навсегда Конкретный класс; шов при появлении второго варианта
Абстракция скопирована с реализации Сигнатуры повторяют API вендора Проектировать от потребителя (Role Interface)
Синхронная событийная шина «для развязки» events.publish() без гарантий Явная оркестрация или настоящая асинхронность + outbox
Обобщённый Repository поверх ORM 40 методов find_by_*, N+1 Узкие доменные репозитории; чтение — проекциями
Иерархия Base*Base* MRO из 5 уровней Композиция шагов, функции высшего порядка
Паттерн ради тестируемости Моки на всё подряд Подмена транспорта/времени, а не домена
Абстракция без ревизии Слой пережил свою причину Пункт «ревизия абстракций» в квартальном техдолге

Мини-итог

  • Паттерн — ставка на будущее изменение, у неё есть цена и вероятность выигрыша. Цена платится сразу и всеми, выигрыш — вероятностный.
  • Абстракции ведут себя как храповик: добавляются легко, удаляются почти никогда. Поэтому порог входа должен быть выше, чем кажется.
  • Здоровая абстракция приходит из дублирования, а не из предвидения: пока нет двух-трёх реальных случаев, вы не знаете, где проходит граница общего и различного.
  • Значительная часть каталога GoF — обход ограничений C++ 1994 года. Сначала проверьте, нет ли механизма в вашем языке.
  • Решающий критерий — не частота изменений, а цена ошибки проектирования: то, что дёшево переделать, дешевле переделывать.
  • Умение схлопнуть паттерн обратно так же профессионально, как умение его применить.

Финальная мысль всего трека. Каталог паттернов ценен не как список решений, а как язык описания последствий. Сказать «здесь Observer» — значит сразу сказать: связность падает, прослеживаемость падает, порядок недетерминирован, ошибки требуют владельца. Разработчик, который видит паттерны, дешевле принимает решения. Разработчик, который их только применяет, — дороже.


Источники

  • Erich Gamma et al., «Design Patterns: Elements of Reusable Object-Oriented Software», 1994 — особенно введение с принципами «программируйте на уровне интерфейса» и «предпочитайте композицию».
  • «Design Patterns 15 Years Later: An Interview with Erich Gamma, Richard Helm, Ralph Johnson» — informit.com, авторы сами говорят, какие паттерны выкинули бы.
  • Peter Norvig, «Design Patterns in Dynamic Languages», 1996 — norvig.com/design-patterns.
  • Sandi Metz, «The Wrong Abstraction», 2016 — sandimetz.com.
  • Martin Fowler, «Refactoring», 2nd ed. — каталог на refactoring.com, запахи Speculative Generality, Middle Man, Lazy Element.
  • Martin Fowler, «Yagni» — martinfowler.com/bliki/Yagni.html.
  • Joel Spolsky, «The Law of Leaky Abstractions», 2002 — joelonsoftware.com.
  • Chris Richardson, Transactional Outbox — microservices.io.
  • Michael Feathers, «Working Effectively with Legacy Code», 2004 — понятие «шов» (seam) и как вводить его безопасно.
  • Kent Beck, «Tidy First?», 2023 — экономика мелких структурных изменений и опционность.
  • Дискуссия про DI/интерфейсы в Go: «Accept interfaces, return structs» и Go Proverbs — «интерфейсы определяет потребитель».

Что дальше

Это последняя статья трека «Паттерны проектирования». Мы прошли путь от происхождения каталога через порождающие, структурные, поведенческие, конкурентные и функциональные паттерны, разобрали антипаттерны — и закончили тем, что паттерн бывает дороже проблемы, которую решает.

Куда двигаться дальше, в зависимости от того, чего вам не хватает.

  • Не хватает фундамента под решениями о структуре кода — читайте принципы проектирования и парадигмы программирования: паттерны выводятся из них, а не наоборот.
  • Паттерны стали тесны, вопросы теперь про системы целиком — переходите к архитектурным паттернам и к DDD: там те же силы, но на уровне сервисов и границ.
  • Нужна база под производительностью решенийструктуры данных и алгоритмы.
  • Хочется закрепить всё на конкретном языке — треки Go, TypeScript, C#, Elixir. Особенно полезно сравнить, как одна и та же сила выражается в языке с наследованием (C#), в языке с интерфейсами-утиными типами (Go) и в языке без изменяемого состояния (Elixir).

Общая карта портала с рекомендуемым порядком прохождения треков — дорожная карта.

Нашли неточность? Выделите фрагмент текста — рядом появится жучок.

Нужен разбор именно вашей ситуации?

Статья описывает общий случай. Если у вас частный — можно разобрать его отдельно, платно. А если не хватает целого материала, предложите тему: её оплачивают вскладчину, и она выходит открытой для всех.

Доска запросов