Перейти до вмісту

Патерни проєктування

Що таке патерни проєктування, де вони вже вбудовані в .NET, чим схожі патерни відрізняються один від одного і які антипатерни найчастіше трапляються в живому коді. На співбесіді перевіряють не знання каталогу напам’ять, а вміння назвати проблему, яку патерн розв’язує, і його ціну.

Перед цією темою: SOLID і принципи.

Junior+

Що таке патерни проєктування і як їх класифікують?

Section titled “Що таке патерни проєктування і як їх класифікують?”
Відповідь

Що це

Патерн проєктування — опис розв’язання типової задачі: яка проблема, які класи беруть участь, як вони пов’язані і до чого це призводить. Це не бібліотека і не код для копіювання: патерн щоразу реалізують під свою задачу.

Навіщо

  • Спільний словник: «обгорни в декоратор» коротше, ніж пояснювати схему класів.
  • Перевірені розв’язання: не треба заново вигадувати, як додати поведінку, не змінюючи клас.

Як класифікують

Книга «Design Patterns» (1994) Гамми, Гелма, Джонсона і Вліссідеса — «банди чотирьох», GoF — описує 23 патерни у трьох групах:

Група Що розв’язує Приклади
Породжувальні (5) як створювати об’єкти Singleton, Factory Method, Builder
Структурні (7) як збирати об’єкти в структури Adapter, Decorator, Facade
Поведінкові (11) як об’єкти взаємодіють Strategy, Observer, Iterator

Типова помилка

Учити патерни як шаблони коду і вставляти їх усюди. Патерн потрібен там, де є проблема, яку він розв’язує.

Як влаштовано

Кожен патерн GoF описано за однією схемою: призначення, мотивація, застосовність, учасники, наслідки, реалізація. Найважливіші — призначення і наслідки: що патерн розв’язує і чим за це платять.

Де зустріти в .NET

Патерн Де в .NET
Builder StringBuilder, WebApplicationBuilder
Abstract Factory DbProviderFactory: CreateConnection, CreateCommand
Prototype вираз with у record
Decorator BufferedStream, GZipStream поверх Stream
Iterator IEnumerable<T>, yield return
Observer event, IObservable<T>
Strategy IComparer<T> у List<T>.Sort

Пастки

  • Схожі схеми. Адаптер, декоратор і проксі виглядають однаково — обгортка над об’єктом. Відрізняються вони наміром, а не кодом.
  • Назва — не патерн. Статичний метод Create — просто фабричний метод-конструктор, а не Factory Method із GoF: той передбачає, що клас створюваного об’єкта обирає підклас.

Правило

Спершу проблема, потім патерн. Якщо не вдається назвати проблему, яку патерн розв’язує в цьому місці, він зайвий.

Під капотом

  • GoF класифікує патерни за двома осями: призначення (породжувальні, структурні, поведінкові) і рівень (класу чи об’єкта).
  • Патерни рівня класу — Factory Method, Template Method, Interpreter, адаптер класу — будуються на успадкуванні й фіксуються під час компіляції. Решта будуються на композиції та змінюються під час виконання.

Патерни і мова

Пітер Норвіг ще 1996 року показав, що в динамічних мовах 16 із 23 патернів стають простішими або зникають. У C# те саме зробили можливості мови: стратегія й команда — Func<T> і Action, ітератор — yield return, спостерігач — event, прототип — with, лінивий singleton — Lazy<T>.

Компроміси

  • Кожен патерн додає типи й непрямість: навігація кодом складніша, стек викликів довший. Натомість з’являється точка розширення.
  • Точка розширення окуповується, лише якщо нею користуються: друга реалізація, підміна в тесті, часті зміни в минулому.
  • GoF — не єдиний каталог: є патерни корпоративних застосунків Фаулера (Repository, Unit of Work), інтеграційні патерни, хмарні патерни Microsoft (Retry, Circuit Breaker).

Як обговорювати в команді

Назва патерну на рев’ю — скорочення для розмови про наслідки: «тут декоратор, отже, порядок реєстрації важливий». Якщо патерн треба знати, щоб зрозуміти клас, а сам він нічого не дає, — це «патерн заради патерну».

Підсумок: патерн — словник для обговорення дизайну; у C# багато патернів GoF стали можливостями мови, а обирати їх варто за наслідками, а не за каталогом.

Приклад
using System.IO.Compression;
// Ітератор: yield return генерує IEnumerator<T>
foreach (var n in Evens(6)) Console.Write($"{n} "); // 0 2 4
Console.WriteLine();
// Стратегія: алгоритм порівняння передається ззовні
var words = new List<string> { "ccc", "a", "bb" };
words.Sort(Comparer<string>.Create((x, y) => x.Length.CompareTo(y.Length)));
Console.WriteLine(string.Join(",", words)); // a,bb,ccc
// Декоратор: GZipStream додає стиснення до будь-якого Stream
using var buffer = new MemoryStream();
using (var gzip = new GZipStream(buffer, CompressionLevel.Optimal, leaveOpen: true))
gzip.Write(new byte[1000]);
Console.WriteLine(buffer.Length < 1000); // True
static IEnumerable<int> Evens(int max)
{
for (var i = 0; i < max; i += 2) yield return i;
}
Middle+

Singleton, фабрика і будівельник: коли що і чому Singleton часто називають антипатерном?

Section titled “Singleton, фабрика і будівельник: коли що і чому Singleton часто називають антипатерном?”
Відповідь

Що це

Патерн Що робить Приклад у .NET
Singleton один екземпляр на застосунок AddSingleton у DI
Фабрика створює об’єкт і обирає клас IHttpClientFactory.CreateClient()
Будівельник збирає об’єкт крок за кроком StringBuilder, WebApplicationBuilder

Навіщо

Коду, якому потрібен об’єкт, ні до чого знати, як його зібрати: який клас обрати, які параметри передати, в якому порядку налаштувати.

Як користуватися

  • Потрібен один спільний об’єкт — зареєструйте його в DI як singleton, а не пишіть статичну властивість Instance.
  • Клас обирається за умовою — фабрика.
  • Багато необов’язкових налаштувань — будівельник із ланцюжком викликів.

Типова помилка

Писати singleton зі static Instance для сервісу із залежностями. Його не підмінити в тесті, і всі тести ділять один стан.

Як влаштовано

  • Singleton. Приватний конструктор і статична властивість. Потокобезпечно й ліниво — через Lazy<T>: за замовчуванням його фабрика виконується рівно один раз навіть за гонки потоків.
  • Factory Method. Базовий клас оголошує віртуальний метод створення, а підклас вирішує, який об’єкт повернути.
  • Abstract Factory. Інтерфейс із кількома методами створення для сімейства узгоджених об’єктів: DbProviderFactory створює з’єднання, команду й параметр одного провайдера.
  • Builder. Об’єкт збирається викликами методів, а Build() перевіряє дані й віддає результат.

Коли що обирати

Задача Розв’язання
Один екземпляр на застосунок AddSingleton у DI
Об’єкт із параметрами часу виконання фабрика-делегат або інтерфейс фабрики
Сімейство узгоджених об’єктів Abstract Factory
Багато необов’язкових частин і перевірка Builder

Пастки

  • Чому Singleton — антипатерн. Глобальна точка доступу ховає залежність, стан переживає тести, а клас сам керує своїм часом життя — це зайвий обов’язок.
  • Fluent API — не завжди Builder. Ланцюжок методів — стиль інтерфейсу. Builder — коли є окремий об’єкт-збирач і підсумковий Build().

Правило

Час життя вирішує контейнер, створення з параметрами — фабрика, складне складання — будівельник.

Під капотом

  • Поле static readonly ініціалізує конструктор типу, і CLR гарантує, що він виконається один раз. Але без явного статичного конструктора момент ініціалізації обирає рантайм, тому для справжньої лінивості беруть Lazy<T>.
  • Lazy<T> за замовчуванням працює в режимі ExecutionAndPublication: фабрика виконується під блокуванням одним потоком. Виняток фабрики кешується — кожне наступне Value викине його ж. PublicationOnly винятків не кешує, але за гонки може створити зайві екземпляри.
  • IHttpClientFactory — фабрика з пулом: HttpClient створюється щоразу, а HttpMessageHandler перевикористовується й перестворюється за таймером, за замовчуванням раз на дві хвилини.

Компроміси

  • Статика проти DI. Статичний singleton доступний звідусіль, DI робить залежність видимою і замінною. Статика виправдана для незмінних значень без залежностей: Comparer<T>.Default, Array.Empty<T>().
  • Власна фабрика. Контейнер уже фабрика, і інтерфейс фабрики на кожен сервіс — зайвий шар. Власна фабрика потрібна, якщо об’єкт залежить від даних запиту або має жити менше за довгоживучого споживача.
  • Builder проти record. Builder дає покрокову перевірку й незмінний результат, але подвоює кількість типів.

Як перевірити в продакшені

  • Singleton зі змінним станом видає себе гонками під навантаженням. На рев’ю шукають static-поля зі змінними колекціями і властивості Instance.
  • Невдалу ініціалізацію Lazy<T> видно як той самий виняток на кожному запиті до перезапуску процесу.

Підсумок: «один екземпляр» — рішення composition root, фабрика — для створення під час виконання, будівельник — для складного складання, і за кожним стоїть ціна непрямості.

Приклад
// Singleton через Lazy<T>: фабрика виконається один раз, навіть із кількох потоків
Parallel.For(0, 8, i => { var config = Config.Instance; });
Console.WriteLine(Config.Created); // 1
// Будівельник: покрокове складання, перевірка в Build()
var mail = new EmailBuilder().To("ann@example.com").Subject("Hi").Build();
Console.WriteLine(mail); // Email { To = ann@example.com, Subject = Hi }
public sealed class Config
{
private static readonly Lazy<Config> instance = new(() => new Config());
public static int Created;
public static Config Instance => instance.Value;
private Config() => Interlocked.Increment(ref Created);
}
public record Email(string To, string Subject);
public class EmailBuilder
{
private string? to, subject;
public EmailBuilder To(string value) { to = value; return this; }
public EmailBuilder Subject(string value) { subject = value; return this; }
public Email Build() => new(to ?? throw new InvalidOperationException("To is required"), subject ?? "");
}
Factory Method
Exporter exporter = new CsvExporter();
Console.WriteLine(exporter.Export([1, 2, 3])); // 1;2;3
// Базовий клас задає порядок роботи, а підклас обирає, який об'єкт створити
public abstract class Exporter
{
public string Export(int[] rows) => CreateFormatter().Format(rows);
protected abstract IFormatter CreateFormatter(); // фабричний метод
}
public class CsvExporter : Exporter
{
protected override IFormatter CreateFormatter() => new CsvFormatter();
}
public interface IFormatter { string Format(int[] rows); }
public class CsvFormatter : IFormatter
{
public string Format(int[] rows) => string.Join(";", rows);
}
Middle+

Adapter, Decorator, Facade і Proxy: чим відрізняються?

Section titled “Adapter, Decorator, Facade і Proxy: чим відрізняються?”
Відповідь

Що це

Усі чотири патерни обгортають інший об’єкт. Різниця — в тому, навіщо.

Патерн Навіщо обгортка Приклад у .NET
Адаптер звести чужий інтерфейс до потрібного StreamReader поверх Stream
Декоратор додати поведінку, інтерфейс той самий BufferedStream, GZipStream
Фасад простий вхід у складну підсистему DbContext
Проксі керувати доступом, інтерфейс той самий ліниве завантаження в EF Core

Як користуватися

  • Підключаєте бібліотеку з незручним API — напишіть адаптер під свій інтерфейс.
  • Потрібні логування чи кеш без зміни класу — декоратор.

Типова помилка

Називати будь-яку обгортку декоратором. Якщо інтерфейс змінюється — це адаптер. Якщо обгортка не додає поведінку, а вирішує, чи викликати об’єкт, — проксі.

Як влаштовано

  • Адаптер. Реалізує інтерфейс клієнта і перекладає виклики в API адаптованого об’єкта. У власному коді це зазвичай обгортка над SDK зовнішнього сервісу.
  • Декоратор. Реалізує той самий інтерфейс і отримує його в конструкторі, тому декоратори вкладаються один в одного. DelegatingHandler — декоратор для HttpMessageHandler: він може щось зробити до і після base.SendAsync.
  • Фасад. Власний інтерфейс, за яким кілька об’єктів. DbContext ховає з’єднання, відстеження змін і генерацію SQL, а властивість Database повертає об’єкт DatabaseFacade для операцій із самою базою.
  • Проксі. Той самий інтерфейс, але вирішує, чи викликати реальний об’єкт і коли: ліниве створення, віддалений виклик, перевірка прав.

Коли що обирати

Задача Патерн
Чужий API не збігається з вашим інтерфейсом Адаптер
Додати логування, повтори, кеш Декоратор
Сховати п’ять викликів за одним Фасад
Відкласти завантаження або викликати віддалено Проксі

Пастки

  • Проксі ховає вартість. Звернення до навігаційної властивості з лінивим завантаженням — запит до бази, і в циклі виходить N+1.
  • Порядок декораторів. Логування ззовні повторів пише один запис, усередині — запис на кожну спробу.

Як зареєструвати декоратор у вбудованому контейнері, розібрано в темі Впровадження залежностей.

Правило

Сформулюйте, що змінює обгортка: інтерфейс, поведінку, складність чи доступ, — від цього і назва.

Під капотом

  • Динамічний проксі — тип, згенерований під час виконання через System.Reflection.Emit. Castle DynamicProxy створює підклас із перевизначеними virtual-членами або реалізацію інтерфейсу. Так працюють моки Moq і проксі лінивого завантаження з Microsoft.EntityFrameworkCore.Proxies.
  • DispatchProxy з BCL створює тип, що реалізує інтерфейс, і спрямовує всі виклики в Invoke(MethodInfo, object[]).
  • Генерація коду під час виконання недоступна в Native AOT, тому такі проксі там не працюють.

Ціна

  • Кожен шар — зайвий виклик через інтерфейс або віртуальний метод. На тлі введення-виведення це непомітно, у гарячому циклі — помітно.
  • DispatchProxy на кожен виклик створює масив аргументів і пакує значущі типи.

Компроміси

  • Ручний декоратор явний і перевіряється компілятором, але його пишуть під кожен інтерфейс. Проксі-перехоплювач один на всі інтерфейси, але його поведінки не видно в коді.
  • Source generators — третій шлях: обгортка генерується під час збирання, видна в налагоджувачі й сумісна з AOT.
  • Фасад на межі модуля знижує зв’язаність, але товстий фасад із часом збирає всю логіку.

Як перевірити в продакшені

  • Ліниве завантаження видно як потік дрібних однакових запитів у лозі EF Core на одну операцію.
  • Типи на кшталт Castle.Proxies.OrderProxy у стеку винятку чи в дампі показують, що виклик ішов через перехоплювач.

Підсумок: обгортки відрізняються наміром і контрактом; динамічний проксі потужніший за ручний декоратор, але ховає поведінку і несумісний з AOT.

Приклад
INotifier notifier = new LoggingNotifier(new SmsAdapter(new LegacySmsClient()));
notifier.Send("Order shipped");
// log: Order shipped
// SMS: ORDER SHIPPED
public interface INotifier { void Send(string message); }
// Чужий клас із незручним API: змінювати його не можна
public class LegacySmsClient
{
public void Transmit(string upperText) => Console.WriteLine($"SMS: {upperText}");
}
// Адаптер: інтерфейс INotifier поверх чужого API
public class SmsAdapter(LegacySmsClient client) : INotifier
{
public void Send(string message) => client.Transmit(message.ToUpperInvariant());
}
// Декоратор: той самий інтерфейс плюс логування
public class LoggingNotifier(INotifier inner) : INotifier
{
public void Send(string message)
{
Console.WriteLine($"log: {message}");
inner.Send(message);
}
}
Динамічний проксі через DispatchProxy
using System.Reflection;
var greeter = DispatchProxy.Create<IGreeter, LoggingProxy>();
((LoggingProxy)(object)greeter).Target = new Greeter();
Console.WriteLine(greeter.Greet("Ann"));
// call Greet
// Hello, Ann
public interface IGreeter { string Greet(string name); }
public class Greeter : IGreeter
{
public string Greet(string name) => $"Hello, {name}";
}
// Тип проксі генерується під час виконання, усі виклики приходять в Invoke
public class LoggingProxy : DispatchProxy
{
public IGreeter? Target { get; set; }
protected override object? Invoke(MethodInfo? method, object?[]? args)
{
Console.WriteLine($"call {method!.Name}");
return method.Invoke(Target, args);
}
}
Middle+

Strategy, Observer, Command, Chain of Responsibility і Template Method: де вони в щоденному коді?

Section titled “Strategy, Observer, Command, Chain of Responsibility і Template Method: де вони в щоденному коді?”
Відповідь

Що це

Поведінкові патерни описують, як об’єкти ділять обов’язки і спілкуються.

Патерн Ідея Де в .NET
Стратегія алгоритм передається ззовні IComparer<T>, лямбди в LINQ
Спостерігач об’єкт сповіщає підписників event, IObservable<T>
Команда запит — це об’єкт ICommand у WPF, обробники команд
Ланцюжок обов’язків запит проходить ланки по черзі middleware ASP.NET Core
Шаблонний метод скелет алгоритму в базовому класі BackgroundService.ExecuteAsync

Як користуватися

  • list.Sort(comparer) — ви передаєте стратегію порівняння.
  • button.Click += OnClick — підписуєтеся як спостерігач.
  • Успадковуєте BackgroundService і пишете ExecuteAsync — заповнюєте крок шаблонного методу.

Типова помилка

Підписатися на подію довгоживучого об’єкта і не відписатися. Видавець тримає посилання на підписника, і збирач сміття його не звільнить.

Як влаштовано

  • Стратегія. Інтерфейс або делегат з одним методом; контекст викликає його, не знаючи реалізації.
  • Спостерігач. event — список делегатів. IObservable<T> додає сигнали завершення й помилки та відписку через IDisposable.
  • Команда. Запит із даними — окремий об’єкт, виконує його обробник. На цьому будуються черги й CQRS-обробники, докладніше — у темі Архітектурні стилі.
  • Ланцюжок обов’язків. Ланка отримує запит і посилання на наступну та вирішує: обробити, передати далі чи обірвати ланцюжок. Так влаштовані middleware, докладніше — у темі Хостинг і конвеєр.
  • Шаблонний метод. Невіртуальний метод викликає віртуальні кроки. BackgroundService реалізує запуск і зупинку та викликає ваш ExecuteAsync.

Коли що обирати

Задача Патерн
Алгоритм змінюється за налаштуванням Стратегія
Кілька реакцій на одну подію Спостерігач
Запит треба відкласти, повторити, залогувати Команда
Послідовні перевірки й обробники Ланцюжок
Спільний порядок кроків, різні деталі Шаблонний метод

Пастки

  • Виняток в одному обробнику event перериває виклик решти.
  • Шаблонний метод із десятком хуків робить базовий клас крихким: зміна порядку кроків ламає нащадків.

Правило

Одна операція — делегат, кілька операцій або стан — інтерфейс, спільний скелет — шаблонний метод, якщо композиції не вистачає.

Під капотом

  • Подія-поле компілюється в приватний делегат і методи add та remove. Вони атомарно замінюють незмінний багатоадресний делегат через Interlocked.CompareExchange, а виклик іде за знімком списку, тому відписка під час сповіщення безпечна.
  • Лямбда-стратегія, що захопила змінні, — це алокація замикання. Лямбду без захоплення компілятор кешує.
  • Конвеєр middleware збирається один раз під час старту: кожна ланка обгортає наступну, і запит іде готовим ланцюжком делегатів.

Ціна

  • Підписка тримає підписника сильним посиланням: об’єкт, підписаний на подію singleton, живе, поки живе singleton.
  • Команда-об’єкт і диспетчер додають алокацію й непрямий виклик, а пошук обробників через рефлексію — ще й час старту.

Компроміси

  • Шаблонний метод проти стратегії. Успадкування гарантує порядок кроків, композиція дає змогу змінювати й тестувати кроки незалежно.
  • event проти IObservable<T> і каналів. Подія синхронна і без зворотного тиску; для асинхронних потоків даних є Rx і System.Threading.Channels.
  • Власний диспетчер проти MediatR. Бібліотека дає спільний конвеєр обробників, але ховає зв’язок «команда — обробник» від навігації кодом. Від версії 13 у неї комерційна ліцензія.

Як перевірити в продакшені

  • Витік через події видно в dotnet-gcdump: шлях утримання підписника йде через делегат у полі видавця.
  • Порядок middleware фіксують інтеграційним тестом на WebApplicationFactory.

Підсумок: поведінкові патерни в .NET — це переважно делегати й конвеєри; їхня ціна — неявний потік керування і сильні посилання підписок.

Приклад
// Стратегія: правило знижки передається делегатом
var checkout = new Checkout(price => price * 90 / 100);
// Спостерігач: підписники дізнаються про оплату і не знають одне про одного
checkout.Paid += total => Console.WriteLine($"email: paid {total}");
checkout.Paid += total => Console.WriteLine($"audit: {total}");
checkout.Pay(200);
// email: paid 180
// audit: 180
// Шаблонний метод: порядок кроків задано в базовому класі
new CsvReport().Print(); // header | csv rows | footer
public class Checkout(Func<int, int> discount)
{
public event Action<int>? Paid;
public void Pay(int price) => Paid?.Invoke(discount(price));
}
public abstract class Report
{
public void Print() => Console.WriteLine($"header | {Body()} | footer");
protected abstract string Body(); // крок, який задає підклас
}
public class CsvReport : Report
{
protected override string Body() => "csv rows";
}
Senior+

Які антипатерни трапляються найчастіше і як від них відходити?

Section titled “Які антипатерни трапляються найчастіше і як від них відходити?”
Відповідь

Що це

Антипатерн — розв’язання, яке часто трапляється і здається зручним, але з часом робить код дорожчим у змінах. Для нього є відома заміна.

Найчастіші

  • God object. Один OrderManager на 3000 рядків робить усе: перевірки, розрахунок, SQL, надсилання листів.
  • Магічні числа. if (status == 3) — що таке 3, знає лише автор.
  • Копіпаста. Та сама логіка в п’яти місцях, а виправили її в чотирьох.
  • Service Locator. provider.GetService<T>() усередині методів замість параметрів конструктора.

Як відходити

  1. Покрити код тестами, щоб рефакторинг нічого не зламав.
  2. Змінювати малими кроками: винести константу, винести метод, винести клас.

Типова помилка

Переписати все з нуля. Старий код зберігає знання про крайні випадки, які новий втратить.

Як влаштовано

Запах коду — видимий симптом: довгий метод, великий клас, довгий список параметрів. Антипатерн — діагноз, що за ним стоїть. Каталог запахів і рефакторингів проти них зібрав Мартін Фаулер у книзі «Refactoring», друге видання вийшло 2018 року.

Антипатерн Симптом Рефакторинг
God object Large Class, десятки залежностей Extract Class
Магічні числа літерали в умовах Replace Magic Literal
Одержимість примітивами string для email, decimal для грошей Replace Primitive with Object
Shotgun Surgery одна правка — багато файлів Move Function, Combine Functions into Class
Патерн заради патерну Speculative Generality Inline Class, Collapse Hierarchy
Service Locator GetService у бізнес-коді параметр конструктора

Пастки

  • Анемічна модель — спірний ярлик. Для CRUD без правил це нормально. Проблемою вона стає, коли ті самі перевірки дублюються по сервісах; докладніше — у темі Архітектурні стилі.
  • Лікування породжує нову хворобу. Якщо роздробити God object на дрібні класи без сенсу, вийде Shotgun Surgery.

Правило

Рефакторинг — окремий коміт без зміни поведінки: малі кроки і зелені тести після кожного.

Як виникають

  • Антипатерни рідко пишуть навмисно. God object росте, бо додати метод в наявний клас дешевше, ніж вигадати новий: кожне рішення локально розумне, а сума — ні.
  • Big Ball of Mud (Foote і Yoder, 1997) — результат тиску строків, зміни людей і зростання без рефакторингу.

Ціна

  • God object і Shotgun Surgery дорожчають із кожною зміною: більше конфліктів злиття, регресій і часу на рев’ю.
  • Одержимість примітивами переносить перевірки в рантайм: email перевіряють у десяти місцях або в жодному.

Компроміси

  • Не кожен ярлик — вирок. Анемічна модель прийнятна для CRUD, а TimeSpan.FromSeconds(30) читається і без константи.
  • «Патерн заради патерну» і YAGNI тягнуть в один бік, DRY і розширюваність — в інший. Абстракцію вводять на другому-третьому випадку, а не на першому.
  • Великий рефакторинг — це проєкт: його ріжуть на кроки, кожен із яких можна випустити, наприклад через Strangler Fig.

Як перевірити в продакшені

  • Аналізатори Roslyn і SonarQube ловлять частину запахів: складність методів, дублювання, магічні числа.
  • Частота змін файлів в історії git показує гарячі точки: великий файл, що змінюється майже в кожному коміті, — кандидат у God object.
  • Архітектурні тести (NetArchTest, ArchUnitNET) фіксують межі шарів, щоб Big Ball of Mud не повернувся.

Підсумок: антипатерни виростають із локально розумних рішень; їх ловлять метриками й рев’ю, а лікують поступовим рефакторингом, а не переписуванням.

Приклад
// До: if (status == 3 && email.Contains('@')) — магічне число і рядок замість типу
// Після: іменований стан і тип із перевіркою під час створення
var email = Email.Parse("ann@example.com");
Console.WriteLine(CanShip(email, OrderStatus.Paid)); // True
try { Email.Parse("not-an-email"); }
catch (ArgumentException e) { Console.WriteLine(e.Message); } // Invalid email
static bool CanShip(Email email, OrderStatus status) => status == OrderStatus.Paid;
public enum OrderStatus { New = 1, Reserved = 2, Paid = 3 }
// Replace Primitive with Object: невірний email не створити
public sealed record Email
{
public string Value { get; }
private Email(string value) => Value = value;
public static Email Parse(string value) =>
value.Contains('@') ? new Email(value) : throw new ArgumentException("Invalid email");
}