DRY, KISS, YAGNI и цена преждевременной абстракции
Эти три аббревиатуры знает каждый, кто хоть раз проходил собеседование. И почти каждый понимает их неправильно — потому что выучил лозунг, а не аргумент. DRY превращается в «запрещено копипастить», KISS — в «пиши поменьше кода», YAGNI — в «не думай о будущем». В таком виде принципы вредят: они делают код связанным, непонятным и хрупким, но с чистой совестью, ведь «мы же следуем принципам».
Эта статья — про то, что за лозунгами стоит. Разберём формулировки в оригинале, построим модель стоимости, из которой видно, когда абстракция окупается, и научимся отвечать на главный практический вопрос: дублировать сейчас или обобщать сейчас? Если вводная статья трека — https://courses.digitable.life/post/principles/00-overview/ — ещё не прочитана, начните с неё.
Общая интуиция: все три принципа — про стоимость изменения
Программу пишут один раз, а меняют десятки раз. Поэтому «хорошесть» кода почти полностью сводится к одному: сколько стоит следующее изменение. Все три принципа — эвристики, которые пытаются эту стоимость снизить, но каждая давит на свою переменную:
| Принцип | Что минимизирует | Чем платит |
|---|---|---|
| DRY | число мест, куда надо внести одну правку | ростом связанности между этими местами |
| KISS | когнитивную нагрузку на читателя | иногда — многословием и повторами |
| YAGNI | объём кода, который надо тащить и не сломать | иногда — переделкой, когда будущее всё-таки наступило |
Обратите внимание: они конфликтуют между собой. DRY толкает к обобщению, KISS — к прямолинейности, YAGNI — к тому, чтобы не строить обобщение вообще. Инженерия начинается там, где вы умеете выбирать между ними осознанно, а не цитировать тот, что удобнее в споре.
DRY: принцип про знание, а не про текст
Формулировка Энди Ханта и Дэйва Томаса в The Pragmatic Programmer (1999):
Every piece of knowledge must have a single, unambiguous, authoritative representation within a system. — «Каждый фрагмент знания должен иметь единственное, непротиворечивое и авторитетное представление в системе.»
Ключевое слово — knowledge, не «код», не «строки», не «символы». В 20-м юбилейном издании авторы отдельно оговариваются, что принцип «украли» и превратили в «не копируй код». DRY нарушается, когда одно и то же решение о том, как устроен мир, записано дважды: при изменении мира их придётся править синхронно, а если синхронности нет — система начинает противоречить сама себе.
Что считается нарушением DRY на самом деле
DRY — не только про код. В книге выделены четыре вида дублирования: навязанное (среда
заставляет: схема БД и DTO, IDL и сгенерированный клиент, документация и код — лечится генерацией из
одного источника истины), неумышленное (не заметили, что поле вычислимо из других: храним
total, subtotal и tax, а знание одно), нетерпеливое («скопирую, потом причешу» — обычная
копипаста) и межразработческое (две команды независимо написали свой parse_phone_number —
самый дорогой и самый незаметный вид).
# Нарушение DRY БЕЗ единой одинаковой строки: знание «заказ дороже 5000 ₽ — крупный»
# записано трижды разными словами, и версии уже разошлись (> против >=).
def needs_manual_review(order):
return order.total > 5000 # место 1
def shipping_cost(order):
return 0 if order.total >= 5000 else 400 # место 2 — здесь уже баг
REPORT_SQL = "SELECT * FROM orders WHERE total > 5000" # место 3
# Исправление — не «вынести константу», а НАЗВАТЬ ЗНАНИЕ:
LARGE_ORDER_THRESHOLD = Money(5000, "RUB")
def is_large(order) -> bool:
"""Единственное авторитетное определение «крупного заказа»."""
return order.total > LARGE_ORDER_THRESHOLD
Дальше все три места спрашивают is_large(order). Отчётный SQL — отдельная боль: знание утекло в
другой рантайм, и лечится это либо вычисляемой колонкой/view в БД, либо генерацией запроса из того
же определения. Утечка бизнес-правил в отчёты, дашборды и ETL — самый частый источник «цифры в BI не
сходятся с цифрами в приложении».
И наоборот: одинаковый код, который НЕ является дублированием
# billing/validators.py И support/validators.py — символ в символ одинаковые функции
def validate(payload):
if not payload.get("id"):
raise ValueError("id обязателен")
if len(payload.get("comment", "")) > 255:
raise ValueError("comment слишком длинный")
Код совпадает буква в букву. Но это совпадение, а не одно знание: лимит комментария в биллинге
задан требованием платёжного шлюза, а в поддержке — шириной колонки в интерфейсе оператора. Завтра
поддержка поднимет лимит до 2000, а биллинг не сможет. Если вы «отDRYили» эти функции в общий
validate_payload, вы получите флаг is_billing=True или тихий баг.
Правильный вопрос не «одинаково ли выглядит?», а «обязаны ли эти два места меняться одновременно и по одной причине?». Это тот же критерий, что и в Single Responsibility Principle (https://courses.digitable.life/post/principles/01-solid/): DRY и SRP — две проекции одной мысли, одна причина изменения → одно место в коде.
одновременно и
по одной причине?"} B -- "Нет, причины разные" --> C["Это совпадение.
Оставить как есть.
Возможно, добавить комментарий
«похоже на X, но не оно»"] B -- "Да" --> D{"Знание уже
повторилось 3+ раза
или повтор дорог?"} B -- "Не знаю" --> E["Не знаю = нет.
Ждать третьего случая"] D -- "Нет" --> E D -- "Да" --> F{"Можно выразить
без флагов и
без параметров-режимов?"} F -- "Да" --> G["Извлечь абстракцию.
Дать ей имя из предметной области"] F -- "Нет" --> H["Извлечь только общую механику
(парсинг, retry, IO),
политику оставить раздельной"] G --> I["Проверить: интерфейс уже,
чем спрятанная сложность?"] H --> I I -- "Нет" --> C
Цена преждевременной абстракции
Самая точная формулировка принадлежит Сэнди Метц:
Duplication is far cheaper than the wrong abstraction. — Sandi Metz, The Wrong Abstraction (2016)
Метц описывает конкретный сценарий деградации, и он воспроизводится в любой кодовой базе:
Ловушка в том, что каждый шаг локально рационален. Никто не решает «давайте сделаем нечитаемый код»: добавить флаг всегда дешевле, чем разъединить функцию обратно. Классический эффект невозвратных затрат — код написан, «жалко выбрасывать», абстракция обрастает исключениями.
Считаем: когда абстракция окупается
Модель совокупной стоимости владения. Пусть n — число мест со знанием, c — стоимость правки в
одном месте, m — число будущих изменений, B — стоимость построения абстракции, v — доля
изменений, которые «почти подходят», p — штраф за вариацию в общем коде. Тогда:
- Дублирование:
Cost_dup = m · n · c→ O(m·n), линейно и предсказуемо. - Верная абстракция (
v ≈ 0):Cost_abs = B + m · c→ O(m). - Неверная абстракция (
v > 0):Cost_wrong = B + m·c + v·m·p·n, причём самpрастёт с числом накопленных флагов — на практике это суперлинейно, ближе кO(m · 2^k)по числу комбинаций флаговk, которые надо удерживать в голове и в тестах.
Абстракция выигрывает у копии, когда B + m·c < m·n·c, то есть при n > 1 + B / (m · c). Отсюда
вся практика: при большом B и малом m (редко меняющийся код, дорогое обобщение) копируйте; при
большом m (горячая зона изменений) порог по n падает почти до двух; при заметном v (вы не
уверены, что случаи одинаковы) третий член съедает всю выгоду.
Порог зависит не от количества строк, а от вашей уверенности в том, что случаи одинаковы.
Именно поэтому работает правило трёх: два совпадения могут быть случайностью, три обычно означают
закономерность.
Правило трёх (Дон Робертс, приведено Фаулером в Refactoring): «Первый раз — просто делаешь. Второй раз морщишься от дублирования, но всё равно дублируешь. На третий — рефакторишь.»
Прикинем численно.
"""Расчёт O(1) по времени и памяти; его ценность — превратить неявные
допущения спора («так же чище!») в явные числа, которые можно оспорить."""
def compare(n, m, c, B, v, p):
dup = m * n * c # правим каждую копию каждый раз
abstr = B + m * c + v * m * p * n # строим один раз + штраф за вариации
if abs(dup - abstr) / max(dup, abstr) < 0.15: # разница в пределах шума оценок
return dup, abstr, "паритет — решает читаемость, а не арифметика"
return dup, abstr, "абстракция" if abstr < dup else "дублирование"
print(compare(n=3, m=12, c=1.0, B=6, v=0.05, p=2)) # (36.0, 21.6, 'абстракция')
print(compare(n=2, m=12, c=1.0, B=6, v=0.6, p=2)) # (24.0, 32.4, 'дублирование')
print(compare(n=8, m=2, c=1.5, B=20, v=0.3, p=3)) # (24.0, 28.4, 'дублирование')
# 1) ядро биллинга, 3 места, стабильные правила → обобщать
# 2) валидация в 2 модулях с разными причинами изменения → копировать
# 3) шаблон в 8 адаптерах, правки дважды в год → копировать
Третий сценарий — самый поучительный: восемь копий, и всё равно абстракция проигрывает, потому что код почти не меняется, а обобщение дорогое. Число копий само по себе ничего не доказывает.
Как выглядит неверная абстракция в коде
# ПЛОХО: одна функция «на все случаи». Классическая boolean trap.
def send_notification(user, text, *, is_urgent=False, is_marketing=False,
skip_quiet_hours=False, use_fallback_sms=False,
dry_run=False, legacy_template=False):
if is_marketing and not user.marketing_consent:
return
if not skip_quiet_hours and user.in_quiet_hours() and not is_urgent:
return
template = OLD_TEMPLATES if legacy_template else TEMPLATES
...
# Шесть булевых параметров = 2^6 = 64 логических пути, осмысленны из них 4–5.
# Вызов send_notification(u, t, False, True, False, True, False, False) нечитаем,
# тестами это не покрывается, каждый новый флаг ломает старые сочетания.
# ЛУЧШЕ: одинаковое — доставка (механика). Разное — правила, кому и когда
# писать (политика). Механику объединяем, политики держим раздельно.
class Channel(Protocol):
def deliver(self, user: User, message: Message) -> DeliveryResult: ...
def _deliver_with_retry(channel: Channel, user, message, attempts: int = 3):
"""Единственное знание про ретраи — оно действительно одно на всех."""
for attempt in range(attempts):
result = channel.deliver(user, message)
if result.ok or not result.retriable:
return result
sleep(backoff(attempt))
return result
def send_marketing(user, message):
"""Знание маркетинга: нужно согласие, уважаем тихие часы."""
if not user.marketing_consent or user.in_quiet_hours():
return DeliveryResult.suppressed()
return _deliver_with_retry(PushChannel(), user, message)
def send_security_alert(user, message):
"""Знание безопасности: игнорируем тихие часы, падаем в SMS."""
result = _deliver_with_retry(PushChannel(), user, message)
return result if result.ok else _deliver_with_retry(SmsChannel(), user, message)
Здесь дублируется вызов _deliver_with_retry — и это нормально. Мы вынесли механику (то, что
действительно одно знание) и оставили раздельными политики (разные знания, которые меняются по
разным причинам и разными людьми). Структурно разница выглядит так:
KISS: простое — это не то же самое, что короткое
KISS («keep it simple, stupid») приписывают Кларенсу «Келли» Джонсону, главному инженеру Lockheed Skunk Works. Исходный смысл был инженерно-конкретным: самолёт должен чиниться в полевых условиях средним механиком с базовым набором инструментов. Не «сделайте попроще», а «сложность обязана окупаться в эксплуатации». Главное непонимание: короткий код часто не простой — однострочник с тремя вложенными comprehension’ами короче и сложнее цикла.
Простое против лёгкого
Рич Хикки в докладе Simple Made Easy (Strange Loop, 2011) разводит два понятия, которые постоянно путают. Simple (лат. sim-plex, «одна складка») — объективное свойство: у вещи одна ответственность, она не сплетена с другими; антоним — complex, «сплетённый». Easy (лат. adjacens, «лежащий рядом») — субъективное: близко к тому, что вы уже знаете.
Знакомое ≠ простое. ORM с ленивой загрузкой — easy (одна строка) и при этом complex (сплетает время жизни объекта, транзакцию, сессию и SQL). Явный запрос — hard поначалу и simple по существу. Хикки называет сплетение complecting — ровно то, что делает преждевременная абстракция: связывает несвязанные случаи ради видимого сокращения кода.
Сущностная и привнесённая сложность
Фред Брукс в No Silver Bullet (1986) разделил сложность на essential (сложность самой задачи: налоги действительно устроены сложно) и accidental (внесённую нашими инструментами и решениями: сборка, конфиги, пять слоёв обёрток вокруг HTTP-клиента). KISS — это война с accidental. Попытка «упростить» essential даёт не простоту, а неправильную программу. Практический критерий: если после «упрощения» появились случаи, которые система обрабатывает неверно, вы не упростили — вы удалили требования.
Практика KISS: измеримые вещи
# ДО: цикломатическая сложность 8, глубина вложенности 4.
def price(order, user, promo):
if order is not None:
if order.items:
total = 0
for item in order.items:
if item.qty > 0:
if item.category == "book":
total += item.price * item.qty * 0.9
elif user.is_premium:
total += item.price * item.qty * 0.95
else:
total += item.price * item.qty
if promo and promo.valid_until > now():
total *= (1 - promo.percent / 100)
return total
else:
return 0
return 0
# ПОСЛЕ: guard clauses + таблица правил. Цикломатическая сложность 3.
CATEGORY_DISCOUNT = {"book": Decimal("0.10")} # знание о скидках — в данных, а не в ветках
PREMIUM_DISCOUNT = Decimal("0.05")
def price(order, user, promo) -> Decimal:
if not order or not order.items:
return Decimal(0)
total = sum(_line_total(i, user) for i in order.items if i.qty > 0)
return _apply_promo(total, promo)
def _line_total(item, user) -> Decimal:
default = PREMIUM_DISCOUNT if user.is_premium else Decimal(0)
return item.price * item.qty * (1 - CATEGORY_DISCOUNT.get(item.category, default))
def _apply_promo(total: Decimal, promo) -> Decimal:
if not promo or promo.valid_until <= now():
return total
return total * (1 - Decimal(promo.percent) / 100)
Что произошло: ранние возвраты убрали два уровня вложенности (глубина вложенности — лучший
дешёвый прокси читаемости, правило «не больше двух уровней» работает); ветвление превратилось в
таблицу — if category == ... был данными, притворявшимися кодом, и новая категория больше не
требует правки логики (это OCP из https://courses.digitable.life/post/principles/01-solid/); каждая функция называет своё
намерение, что делает комментарии ненужными (https://courses.digitable.life/post/principles/04-clean-code/); заодно исправлен
реальный баг — float в деньгах, где 0.1 + 0.2 != 0.3.
Формально цикломатическая сложность (McCabe, 1976) = число решающих узлов + 1 = число независимых путей. Она задаёт нижнюю границу количества тестов для покрытия ветвей — то есть сложность буквально равна вашему счёту за тестирование (https://courses.digitable.life/post/principles/06-testing-principles/).
Глубина модуля — лучшая метрика простоты
Джон Оустерхаут в A Philosophy of Software Design предлагает критерий, который я считаю самым полезным из всех существующих:
Польза модуля = объём спрятанной сложности − сложность его интерфейса.
open(path) в Unix — глубокий модуль мечты: три слова в интерфейсе, за ними права доступа, inode,
кэш страниц, драйверы и файловые системы. Обёртка UserServiceHelperFactory с восемью методами,
каждый из которых делегирует одну строку, — мелкий модуль: интерфейса больше, чем содержимого.
Преждевременная абстракция почти всегда порождает мелкие модули — это её наглядный симптом. Если
после извлечения абстракции читателю приходится знать столько же или больше, вы добавили слой, а не
спрятали сложность.
YAGNI: «вам это не понадобится»
Принцип из экстремального программирования, авторство обычно приписывают Рону Джеффрису. Каноничный разбор — Yagni Мартина Фаулера.
Важнейшее уточнение, которое почти всегда теряют: YAGNI применим к предполагаемой функциональности, а не к качеству кода. Тесты, понятные имена, обработка ошибок, модульность — это не «на будущее», это то, что делает изменения возможными сегодня. «YAGNI, поэтому я не пишу тесты» — не YAGNI, а его противоположность.
Четыре стоимости по Фаулеру
потратили время сейчас"] B["cost of delay
не сделали то,
что нужно было сегодня"] end subgraph later["Живём с ней"] C["cost of carry
усложняет код,
тормозит все правки"] D["cost of repair
угадали неверно —
переделка дороже постройки"] end A --> C B --> C C --> D D --> E{"Итог"} E --> F["Не понадобилась:
потеряли build + delay + carry"] E --> G["Понадобилась, но иначе:
потеряли всё + repair —
часто дороже, чем не делать вовсе"]
Тонкость cost of repair: неверно угаданная абстракция дороже пустого места. Когда фича наконец
нужна, приходится не «дописать», а сначала разобрать неправильное обобщение, не сломав нынешних
потребителей. Тот же механизм, что у неверной абстракции.
Пример: плагинная архитектура на одну реализацию
// YAGNI-нарушение: инфраструктура под «будущих провайдеров», которых пока один.
interface PaymentProvider {
charge(amount: Money, token: string): Promise<ChargeResult>;
refund(chargeId: string, amount: Money): Promise<RefundResult>;
supports(currency: Currency): boolean;
}
class PaymentProviderRegistry {
private providers = new Map<string, PaymentProvider>();
register(name: string, p: PaymentProvider) { this.providers.set(name, p); }
resolve(currency: Currency): PaymentProvider { /* правила выбора */ }
}
// В проекте зарегистрирован ровно один провайдер. Полтора года.
// YAGNI-версия: прямолинейно и честно.
export async function charge(amount: Money, token: string): Promise<ChargeResult> {
return stripe.charges.create({
amount: amount.minorUnits, currency: amount.currency, source: token,
});
}
Плохо здесь не «в принципе», а конкретно. Во-первых, интерфейс спроектирован под единственную
известную реализацию и неизбежно повторяет её особенности (token: string — потому что так у
Stripe); когда придёт провайдер с трёхшаговым 3-D Secure, интерфейс всё равно придётся переписать.
Абстракция, выведенная из одного примера, — не абстракция. Во-вторых, каждый разработчик теперь
делает два лишних прыжка по коду, чтобы понять, что происходит при оплате: это cost of carry,
помноженный на размер команды. В-третьих, реестр и резолвинг надо тестировать и поддерживать, не
давая пользователю ничего.
Когда появится второй провайдер, у вас будет два реальных примера, и интерфейс получится
выведенным из различий, а не угаданным. Извлечь интерфейс из готовой функции дёшево (B мало), а
исправить угаданный — дорого. Ровно это и говорит арифметика из раздела выше.
Где YAGNI не работает
YAGNI — эвристика про обратимые решения. Она молчит там, где откат невозможен или цена ошибки
катастрофична. Забыть tenant_id в схеме мультитенантной системы — это не «дописать потом», а
переписать всё и мигрировать продовые данные. Публичный API нельзя молча поменять: версионирование
закладывают сразу. Аудит-лог, шифрование PII и разделение прав по принципу «добавим, когда
потребуется» означают «добавим после инцидента». То же с наблюдаемостью (https://courses.digitable.life/post/principles/07-twelve-factor/) и с решениями, задающими форму системы: если она точно будет
распределённой, синхронный вызов вместо очереди — не YAGNI, а долг с процентами.
Джефф Безос делит решения на «тип 1» (односторонняя дверь, назад не выйдешь) и «тип 2» (двусторонняя). YAGNI — про двери второго типа. Для первых работает обратная эвристика: думайте заранее, потому что откат стоит дороже всего проектирования.
Как это выглядит в проде
Go и «немного копирования». Одна из Go Proverbs Роба Пайка: «A little copying is better than a little dependency.» Стандартная библиотека Go осознанно дублирует небольшие куски вместо общих утилит: зависимость между пакетами — это навсегда, а тридцать строк копии локальны и удаляются в одиночку (https://courses.digitable.life/post/golang/00-overview/).
Микросервисы и общие библиотеки. Самый дорогой вид неверного DRY — вынести общий код в разделяемую библиотеку между сервисами разных команд: чтобы поменять поле, нужно согласовать релиз библиотеки с пятью командами и задеплоить их в правильном порядке — получается распределённый монолит. В DDD это разница между Shared Kernel (дорогой контракт, требующий постоянной координации) и Anticorruption Layer (каждый контекст держит свою модель); см. также https://courses.digitable.life/post/principles/03-coupling-and-cohesion/. Правило индустрии: дублировать DTO между сервисами — норма; шарить доменную логику — почти всегда ошибка.
Кодогенерация вместо «ручного DRY». Там, где дублирование навязано средой (схема БД ↔
модель ↔ API-клиент), правильный ответ — не абстракция в рантайме, а единственный источник истины
плюс генерация: Protobuf/gRPC, OpenAPI-генераторы, sqlc, Ent, Prisma. Знание одно,
представлений много, синхронизирует их машина.
Монорепозитории. В монорепо Google (статья в CACM) общих библиотек больше, потому что там решена главная проблема: атомарное изменение всех потребителей одним коммитом. Вывод общий: допустимая степень DRY зависит от того, насколько дёшево у вас синхронно менять все места.
DAMP в тестах. В тестах принято отступать от DRY в пользу DAMP (Descriptive And
Meaningful Phrases): тест читается сверху вниз без прыжков в хелперы. Общий setUp и цепочки фабрик
экономят строки, но когда тест падает, вы не видите, в каком он был состоянии. В тестах читаемость
важнее отсутствия повторов.
Типичные ошибки
- DRY по форме, а не по смыслу. Объединили куски с разными причинами изменения. Симптом: через месяц в функции появился первый булев флаг.
- Абстракция из одного примера. Интерфейс, выведенный из единственной реализации, описывает эту реализацию, а не концепцию. Ждите второго и третьего.
- Boolean trap. Флаг, меняющий поведение функции, — это две функции, слипшиеся в одну.
- «Слой на всякий случай». Обёртка вокруг логгера «вдруг сменим логгер»: смените один раз за десять лет, а читать этот слой будут ежедневно.
- Конфигурируемость вместо решения. Каждый неразрешённый спор становится параметром конфига. Через год — 200 флагов, и никто не знает работающую комбинацию.
- KISS как оправдание невежества. «Не буду разбираться в транзакциях, сделаю проще» — это не простота, а удаление сущностной сложности вместе с корректностью.
- YAGNI против качества. Отказ от тестов и осмысленных имён «на потом». Фаулер писал прямо: YAGNI — про фичи, не про ремесло.
- Отказ разбирать плохую абстракцию. Добавить ещё флаг вместо inline обратно. Возврат к дублированию — легитимный рефакторинг (https://courses.digitable.life/post/principles/05-code-smells-and-refactoring/).
- DRY через наследование. Общий базовый класс «чтобы не повторяться» — самый жёсткий вид связанности; композиция почти всегда лучше, см. https://courses.digitable.life/post/principles/01-solid/.
- Игнорирование горизонта. Код, который выкинут через три месяца, не нуждается
в абстракциях:
mв формуле близко к нулю.
Чеклист на ревью
Увидев извлечённую абстракцию, задайте шесть вопросов. Одно ли это знание — изменятся ли все
потребители одновременно и по одной причине? Сколько примеров её породило: меньше трёх —
обосновывайте отдельно. Есть ли флаги-режимы — каждый флаг минус к обоснованности.
Уже ли интерфейс, чем спрятанное — если нет, это не абстракция, а перекладывание.
Есть ли имя из предметной области — PricingPolicy хорошо, DataProcessorUtils —
признак того, что объединили несвязанное. Обратимо ли решение — если разъединить легко, можно
рискнуть раньше; если абстракция станет публичным API, ждите дольше.
Мини-итог одной фразой:
DRY — про знание, а не про текст. KISS — про читателя, а не про длину. YAGNI — про фичи, а не про качество. Дублируйте, пока не поймёте, что именно у вас общего; абстрагируйте, когда это станет очевидным, а не правдоподобным.
Источники
- A. Hunt, D. Thomas. The Pragmatic Programmer, 20th Anniversary Edition (2019) — DRY и ортогональность.
- Sandi Metz. The Wrong Abstraction (2016).
- Martin Fowler. Yagni, Rule of Three, Design Stamina Hypothesis.
- Martin Fowler. Refactoring, 2nd ed. — каталог на refactoring.com и refactoring.guru.
- Rich Hickey. Simple Made Easy, Strange Loop 2011.
- F. P. Brooks. No Silver Bullet (1986) — essential vs accidental.
- John Ousterhout. A Philosophy of Software Design, 2nd ed. (2021) — deep modules, «complexity is incremental».
- Rob Pike. Go Proverbs — «A little copying is better than a little dependency».
- Kent Beck. Extreme Programming Explained — исходный контекст YAGNI.
- T. J. McCabe. A Complexity Measure, IEEE TSE, 1976 — цикломатическая сложность.
- K. Henney (ed.). 97 Things Every Programmer Should Know — эссе «Beware the Share».
Что дальше
Мы много раз упирались в одну и ту же мысль: абстракция плоха не сама по себе, а тем, что создаёт связанность между местами, которые должны меняться независимо. Пора разобрать это понятие строго — измерить виды связанности, понять, что такое связность модуля, и научиться формулировать правила доступа между объектами.
Следующая статья: Связанность, связность и закон Деметры.