Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Anti patterns

Есть три большие области, в которых выделяют антипаттерны: разработка, архитектура и управление проектами.

Паттерны - это методы решения распространенных проблем при кодировании. Однако, хотя шаблоны проектирования дают нам преимущества, антишаблоны создают больше проблем.

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

Разработка программного обеспечения на основе архитектуры — наиболее эффективный подход к созданию систем. Подходы, основанные на архитектуре, превосходят подходы, основанные на требованиях, документах и ​​методологии. Проекты часто достигают успеха вопреки методологии, а не благодаря ей.

Антипаттерны в Rust

Клонирование для проверки заимствований (borrow checker)

Если вы используете .clone() что-бы обойти правила заимствования Rust, то это анти-паттерн. В общем, клонирование должно быть осознанным, с полным пониманием последствий.

// define any variable
let mut x = 5;

// Borrow `x` -- but clone it first
let y = &mut (x.clone());

// without the x.clone() two lines prior, this line would fail on compile as
// x has been borrowed
// thanks to x.clone(), x was never borrowed, and this line will run.
println!("{}", x);

// perform some action on the borrow to prevent rust from optimizing this
//out of existence
*y += 1;

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

Несмотря на то, что .clone() указывает на плохой шаблон, иногда можно написать неэффективный код, например, в следующих случаях:

  • developer все еще является новым владельцем;

  • код не имеет больших ограничений по скорости или памяти (как проекты хакатонов или прототипы);

  • выполнить проверку заимствований действительно сложно, и вы предпочитаете оптимизировать читаемость, а не производительность;

Также обязательно всегда запускайте свой проект c cargo clippy, который обнаружит некоторые случаи, в которых нет необходимости в .clone()



Использование подавление предупреждений компилятора при сборке

Использование #![deny(warnings)] является анти-паттерном, так как ведет к потенциальным ошибкам в будущем.

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

Кроме того, иногда API устаревают, поэтому при их использовании будет выдаваться предупреждение там, где раньше его не было.

Альтернативы

Есть два способа решения этой проблемы: во-первых, мы можем отделить настройку сборки от кода, а во-вторых, мы можем явно назвать тесты, которые хотим запретить.

Следующая командная строка будет построена со всеми предупреждениями, установленными на deny:

RUSTFLAGS="-D warnings" cargo build

Это может сделать любой отдельный разработчик (или настроить в инструменте CI, таком как Travis, но помните, что это может привести к поломке сборки, если что-то изменится) без необходимости внесения изменений в код.

Альтернативно, мы можем указать нужные нам линты denyв коде. Вот список предупреждений, которые (надеюсь) можно безопасно отклонить (начиная с Rustc 1.48.0):

#![deny(bad_style,
       const_err,
       dead_code,
       improper_ctypes,
       non_shorthand_field_patterns,
       no_mangle_generic_items,
       overflowing_literals,
       path_statements,
       patterns_in_fns_without_body,
       private_in_public,
       unconditional_recursion,
       unused,
       unused_allocation,
       unused_comparisons,
       unused_parens,
       while_true)]

Кроме того, может быть хорошей идеей запретить следующие разрешенные линты:

#![deny(missing_debug_implementations,
       missing_docs,
       trivial_casts,
       trivial_numeric_casts,
       unused_extern_crates,
       unused_import_braces,
       unused_qualifications,
       unused_results)]

Некоторые, возможно, также захотят дополнить missing-copy-implementations в свой список.

Список линтов в вашей системе rustc -W help Общий список опций rustc --help

rust-clippy — это набор тестов для улучшения кода Rust Коллекция всех клипповых ворсинок



Неправильное использование трейта Deref

Неправильно используйте трейт Deref для эмуляции наследования между структурами.

Пример Иногда нам нужно эмулировать следующий общий шаблон объектно-ориентированных языков, таких как Java:

class Foo {
    void m() { ... }
}

class Bar extends Foo {}

public static void main(String[] args) {
    Bar b = new Bar();
    b.m();
}

Для этого мы можем использовать антишаблон полиморфизма deref:

use std::ops::Deref;

struct Foo {}

impl Foo {
    fn m(&self) {
        //..
    }
}

struct Bar {
    f: Foo,
}

impl Deref for Bar {
    type Target = Foo;
    fn deref(&self) -> &Foo {
        &self.f
    }
}
fn main() {
    let b = Bar { f: Foo {} };
    b.m();
}

В Rust нет наследования структур. Вместо этого мы используем композицию и включаем экземпляр Foo в Bar.

impl Bar {
    fn m(&self) {
        self.f.m()
    }
}

Недостатки. Будущие программисты, читающие это в коде, не будут ожидать, что это произойдет. Это потому, что мы неправильно используем Deref. Признак Deref предназначен для реализации пользовательских типов указателей. Цель состоит в том, чтобы он принимал указатель на T, а не выполнял преобразование между различными типами.

Это еще и потому, что механизм здесь совершенно неявный.

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

Обсуждение. Нет ни одной хорошей альтернативы. В зависимости от конкретных обстоятельств может быть лучше повторно реализовать использование признаков или написать фасадные методы для ручной диспетчирезации Foo.


Антипаттерны разработки ПО


Это ошибки на уровне кода и реализации, которые ухудшают читаемость, тестируемость и поддержку.
  • 1. Spaghetti Code (Спагетти-код) Спагетти-код - слабо структурированная и плохо спроектированная система, запутанная и очень сложная для понимания. Код с множеством goto, копипастой, затрудняет расширение и оптимизацию кода. Пример из Linux SCSI driver.

Решение — частый рефакторинг может улучшить структуру и поддержку.

  • 2. God Object / Blob (Божественный объект / Блоб) Гигантский класс, который делает всё и становится почти операционной системой. За таким "блобом" обычно скрываются десятки других плохих практик. Часто встречается в легаси-системах (например, банковских).

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

  • 3. Copy-Paste Programming / Копипаста Повторное использование программного кода методом вырезания и вставки, без переиспользования. Приводит к серьёзным проблемам сопровождения когда ошибка в коде скопированна в разных его частях, и программист забывает вносить требуемые изменения в скопированный код. Может быть как экономией времени, так и источником проблем.

Решение — создание общих решений и их использование.

  • 4. Premature Optimization (Преждевременная оптимизация) Оптимизация до того, как подтверждена реальная необходимость, ухудшающая читаемость и усложняющая поддержку.

  • 5. Lava Flow (Поток лавы) Код копируется из проекта в проект с минимальными изменениями, с большим количеством закомментированных или устаревших функций и вообще недокументированы, или такому коду сопутствует комментарий вида "Не знаю, как оно работает, но оно работает. Не удалять и не менять!". В итоге образуются огромные блоки мёртвого кода, который боятся удалить. Особенно распространён в аутсорсинговых компаниях. Главными причинами возникновения потоков лавы являются — написание больших частей проекта одним программистом, отсутствие code review, ошибки в проектировании архитектуры.

  • 6. Golden Hammer (Золотой молоток) Золотой молоток — уверенность в полной универсальности любого решения. На практике, это — применение одного решения (чаще всего какого-либо одного паттерна проектирования) для всех возможных и невозможных задач. Использование одной знакомой технологии или подхода для решения всех задач. Например, предпочтение IBM MQ вместо Apache Kafka. Золотой молоток — это уверенность в универсальности любого решения, что на практике приводит к неэффективным результатам.

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

  • 7. Boat Anchor (Лодочный якорь) Наследование большого количества бесполезного кода или зависимостей, которые не служат полезной цели и мешают развитию. Часто возникает после слияний компаний.

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

  • 8. Magic Numbers (Магические числа) Необъяснённые числовые значения без использования констант и комментариев. Числа затрудняют понимание кода и его рефакторинг. Главными причинами этой ошибки — спешка при разработке, отсутствие практики программирования. Данный анти-паттерн надо пресекать на корню, оговаривая использование числовых констант перед началом разработки.

  • 9. Shotgun Surgery (Разбросанные изменения) При изменении логики нужно вносить правки в десятки разрозненных мест кода.

  • 10. Poltergeists (Полтергейсты) Классы с очень коротким жизненным циклом, содержащие минимум данных и логики, существующие только для вызова методов других классов. Нарушают принципы ООП. Пример: класс DatabaseConnector, который только создаётся, открывает соединение, вызывает метод и тут же умирает. Логичнее интегрировать функционал в основной класс.

  • 11. Dead End (Тупик) Использование классов или API, которые не могут быть расширены или модифицированы, не предоставляют нужной функциональности и ведут архитектуру в тупик. Например, проприетарная библиотека, не поддерживаемая разработчиком.

  • 12. Input Kludge (Кладж ввода) Костыльная обработка входных данных: система принимает данные в неудобном формате, логика парсинга разбросана по всему коду, нет чётких интерфейсов. Пример: парсинг CSV в 15 разных местах по разным правилам.

  • 13. Blind Faith (Слепая вера) Слепое доверие внешним API без проверки ответов, входным данным без валидации, сторонним библиотекам без понимания их работы. Пример: использование результата API без проверки статуса ответа.

Этот анти-паттерн — недостаточная проверка корректности входных данных, исправления ошибки или результатов работы кода. Очень часто программист думает, что его код всегда будет в идеальных условиях, никогда не выдаст ошибки и не получит неверных входных данных или, ещё чего, данных неверного типа. Но все лгут, поэтому нельзя доверять никакому коду, даже собственному. Но и не следует доводить это недоверие до паранойи, то есть приходить к анти-паттерну ненужной сложности. Просто следует помнить про проверку входных данных и возможные проблемы у чужого кода, который используете вы.

  • 14. Soft Code / Hard Code (Мягкое / Жёсткое кодирование) Отсутствие чёткого разделения, что должно быть настраиваемым, а что — жёстко заданным в коде. Жёсткое кодирование — внедрение различных данных об окружении в реализацию. Например — различные пути к файлам, имена процессов, устройств и так далее. Захардкодить — жёстко прописать значение каких-либо данных в коде. Главная опасность, исходящая от этого анти-паттерна — непереносимость. В системе разработчика код будет исправно работать до перемещения или переименования файлов, изменения конфигурации устройств. На любой другой системе код может вовсе не заработать сразу же. Как правило, программист практически сразу забывает где и что он захардкодил, даже если делает это в целях отладки кода.

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

  • 15. Functional Decomposition (Функциональная декомпозиция) Разделение классов не по функциональному уровню, а по частям бизнес-процесса. Приводит к смешиванию уровней (например, фронт в бэке, запросы к веб-сервисам внутри функций сложения чисел). Возникает из-за попыток реализовать процедурные подходы на ООП-языках.

  • 16. Needless Complexity (Избыточная сложность) Код содержит ненужные усложнения, которые делают его трудным для понимания и поддержки. Простыми словами — это заумность решения. Ненужная сложность может быть внесена в решение любой задачи. Это приводит к усложнению понимания кода, понижению скорости работы. Причинами являются — отсутствие или некачественность рефакторинга, некомпетентность программиста.

  • 17. Programming by permutation (Программирование перебором) Многие начинающие программисты пытаются решать некоторые задачи методом перебора — не брутфорсом решения, а именно подбором параметров, порядка вызова функций и так далее. Все эти игры с "А если i+1? Ну тогда может i-1?" к параметрам и подобные штучки устраняют только симптомы, и не дают понимания сути происходящего. А если программист не понимает происходящего, то он не сможет предусмотреть все варианты развития событий и обязательно о чём-то забудет. Он потратит время на подбор работающего для него решения и позднее потратит время для переделки этого решения. Все подобные подобранные решения вылазят боком и хорошо ещё — если в процессе разработки или отладки. К подобному ни в коем случае нельзя привыкать, достигая успеха на небольших задачках. Если программист не может решать задачи другим путём — он некомпетентен и ему не следует доверять разработку — вам же будет хуже.

  • 18. Бездумное комментирование Результат «работы» данного анти-паттерна — большое количество лишних и неинформативных комментариев. Код не следует комментировать ради комментирования! Ни в коем случае нельзя допускать диалога разработчиков в комментариях — лучше перенести данную функцию с комментариев на специализированные инструменты для code review, или на личное обсуждение.

Антипаттерны архитектуры ПО


Ошибки на уровне структуры системы и взаимодействия компонентов
  • 1. Big Ball of Mud (Большой ком грязи) Архитектура без чёткой структуры, всё перепутано и хаотично. Система сложна для понимания, поддержки и расширения.

  • 2. Stovepipe System / Stovepipe Enterprise (Система дымоходов / Предприятие по производству дымоходов) Изолированные подсистемы без унифицированных интерфейсов, слабо связанные и плохо интегрированные, что мешает развитию и масштабированию системы. Отсутствие координации и планирования приводит к фрагментированным и трудноуправляемым архитектурам.

  • 3. Reinventing the Wheel (Изобретение велосипеда заново) Написание собственных решений вместо использования готовых стандартных библиотек или фреймворков, что приводит к излишним затратам времени и ресурсов.

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

  • 4. Vendor Lock-In (Привязка к поставщику) Полная зависимость от одного поставщика технологий или проприетарных архитектур, что затрудняет замену и обновление компонентов.

  • 5. Jenga Architecture (Архитектура Дженга) Система, где любое изменение может сломать всё остальное, подобно игре в Дженгу. Отсутствие модульности и слабое разделение ответственности.

  • 6. Anemic Domain Model (Анемичная модель) Объекты без логики, всё поведение вынесено в сервисы, что превращает модель в набор данных без бизнес-правил. Противоречит принципам ООП.

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

    Предпочитайте нормальную модель бизнес логики (иногда называют Rich Domain Model), а не процедурную, анемичную модель с DTO вместо полноценных сущностей. Анемичная модель не имеет ничего общего с ООП и ее следует рассматривать как неудачный пример процедурного программирования.

    Суть антипаттерна:

    • В предметной области (domain) объекты содержат только данные (поля) и не содержат поведения (методов с бизнес-логикой).
    • Вся логика вынесена в отдельные сервисы, которые оперируют «голыми» DTO.
    • Получается, что объекты системы не умеют ничего, кроме как хранить состояние.

    Симптомы:

    • Сущности выглядят как Java/Rust/С# классы/структуры с кучей pub или getters/setters и ни одного метода с бизнес-логикой.
    • Бизнес-правила сосредоточены в сервисах/утилитах, а не в самих объектах.
    • Чтобы изменить объект, всегда нужно звать отдельный сервис, который «знает, как с ним работать».
    • Код похож на процедурный, даже если он формально написан на ООП-языке.

    Почему это плохо:

    • Нарушает принцип инкапсуляции — данные и логика работы с ними разорваны.
    • Сложнее поддерживать код — изменения бизнес-правил требуют менять множество сервисов.
    • Усложняет тестирование — тесты нужно писать для сервисов, а не для самих объектов.
    • Порождает god services — сервисы, которые делают всё и знают всё.
    • Модель перестаёт быть самоописательной — бизнес-логика «размазана» по коду.

    Как правильно (Rich Domain Model). В богатой модели сущность сама:

    • знает свои инварианты,
    • выполняет бизнес-логику,
    • изменяет своё состояние только через свои методы.
    struct BankAccount {
        balance: i32,
    }
    impl BankAccount {
        fn deposit(&mut self, amount: i32) {
            assert!(amount > 0, "amount must be positive");
            self.balance += amount;
        }
        fn withdraw(&mut self, amount: i32) -> Result<(), &'static str> {
            if amount > self.balance {
                Err("Insufficient funds")
            } else {
                self.balance -= amount;
                Ok(())
            }
        }
    }

    Анемичный вариант выглядел бы так:

    struct BankAccount {
        balance: i32,
    }
    
    // Логика вынесена в отдельный сервис
    fn withdraw(account: &mut BankAccount, amount: i32) -> Result<(), &'static str> { ... }

    Если упростить, анемичная модель = «ООП без ООП» → процедурная архитектура под видом объектной. В DDD её считают особенно вредной, потому что она ломает ключевую идею — объединение данных и поведения в доменной модели.

  • 7. Architecture by Implication (Архитектура по смыслу / случайная архитектура) Архитектура, которая возникла случайно, без явного планирования и документирования, что приводит к неустойчивой и непредсказуемой системе.

  • 8. The Blob (Блоб) Центральный модуль, через который проходит вся логика системы, что создаёт сильную связанность и узкое место для всех изменений.

  • 9. Connector Bloat (Избыточность интерфейсов) Чрезмерно сложные или многочисленные интерфейсы и точки интеграции, что затрудняет сопровождение и развитие системы.

  • 10. Technology Obsolescence (Технология непрерывного устаревания) Использование устаревших технологий без осознанного выбора, что затрудняет поддержку и развитие системы. Быстрые изменения в экосистеме приводят к сложностям совместимости.

  • 11. Architectural Smells (Архитектурные запахи) Общие признаки плохой архитектуры, включающие смешение слоёв, дублирование, чрезмерную связанность и др.

  • 12. Swiss Army Knife (Швейцарский армейский нож) Слишком сложный и универсальный интерфейс или модуль, пытающийся удовлетворить все возможные потребности, что приводит к громоздкости и плохой поддерживаемости.

  • 13. Wolf Ticket Продукт или компонент, который заявляет об открытости и соответствии стандартам, но на деле не соответствует им и имеет собственные интерфейсы.

  • 14. Architecture by Committee (Проектирование комитетом) Архитектура, созданная большим количеством участников без чёткой ответственности, в результате чего получается сложная и неэффективная структура.

  • 15. Bloatware / Feature Creep (Перегрузка функционалом) Накопление избыточного функционала, который усложняет архитектуру и снижает её гибкость.

  • 16. Anti-integration (Антиинтеграция / Подсистемы дымоходной системы) Интеграция подсистем производится точечно и не стандартизирована, что затрудняет развитие системы в целом.

Антипаттерны управления проектами

r Ошибки в менеджменте, процессах и взаимодействии команды, которые приводят к снижению эффективности, демотивации и провалам проектов
  • 1. Death March (Смертельный марш) Проект с заведомо нереалистичными сроками, требованиями и ресурсами, где команда вынуждена работать в условиях постоянного кризиса и переработок.

  • 2. Scope Creep / Featuritis / Feature Creep (Расползание области проекта / Перегрузка фичами) Постоянное добавление новых функций без пересмотра сроков и ресурсов, что ведёт к хаосу, росту стоимости и срыву дедлайнов.

  • 3. Micromanagement (Микроменеджмент) Чрезмерный контроль и вмешательство менеджеров в мелкие детали работы разработчиков, что снижает их мотивацию и продуктивность.

  • 4. Analysis Paralysis (Паралич анализа) Бесконечный сбор и анализ информации без принятия решений и перехода к реализации, что тормозит развитие проекта.

  • 5. Throw It Over the Wall (Передать работу без обратной связи) Передача задач или готового продукта другой команде или отделу без должного взаимодействия и обратной связи, что вызывает недопонимания и ошибки.

  • 6. Management by Numbers (Управление по цифрам) Фокус только на метриках и числах, игнорируя качество, человеческий фактор и контекст, что приводит к ложным выводам и неправильным решениям.

  • 7. Smoke and Mirrors (Дым и зеркала) Демонстрация фальшивого прогресса, подделка отчетов и показных демонстраций вместо реального выполнения работы.

  • 8. The Mythical Man-Month (Мифический человеко-месяц) Вера, что добавление большего числа разработчиков всегда ускоряет проект, что часто приводит к обратному эффекту из-за необходимости коммуникации и обучения.

  • 9. Warm Bodies (Теплые тела) Назначение в проект большого количества людей без учёта их квалификации и вклада, ради достижения численности, что снижает эффективность.

  • 10. Walking on Minefield (Прогулка по минному полю) Работа с технологиями или процессами, которые содержат множество скрытых рисков и проблем, приводящих к частым ошибкам и сбоям.

  • 11. Mushroom Management (Грибовидное управление) Изоляция разработчиков от конечных пользователей и важных решений, при этом им даётся мало информации, как будто их «растят в темноте и кормят навозом».

  • 12. Cover Your Assets (Прикрывайте свои активы) Создание чрезмерной документации и формальных требований с целью избежать ответственности и принятия решений, что тормозит процесс.

  • 13. Committee Design (Проектирование комитетом) Решения принимаются группой без чёткой ответственности, что приводит к раздутым и непоследовательным архитектурам и решениям.

  • 14. Blind Faith (Слепая вера) Менеджеры и команда слепо доверяют инструментам, процессам или внешним факторам без критического анализа и проверки.

  • 15. Over-Commitment (Переобязательство) Обещание больше, чем реально возможно сделать, что приводит к постоянным срывам сроков и снижению качества.

  • 16. Lack of Feedback (Отсутствие обратной связи) Отсутствие регулярной коммуникации между участниками проекта, что ведёт к накоплению ошибок и недопониманиям.



Что такое анти-паттерны?

Худшие практики разработки и архитектуры