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

Topics

Что такое паттерны, зачем и почему?

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

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

Хорошее понимание паттернов и алгоритмов развивает гибкость мышления, повышает качество конечного продукта и уменьшает вероятность появления «архитектурного мусора» (оставление кривых решений в итоговом продукте, которые видят и ощущают пользователи).

Но, паттерн — это инструмент, а не догма. Не всё надо «паттернизировать», иначе получится архитектурная перегрузка.

Нейминг, выбор имен для сущностей

Так как код читают в 5–10 раз чаще, чем пишут, хорошее имя часто позволяет понять, что происходит, не заглядывая в реализацию.

Удачно подобранное имя:

  • сразу передаёт смысл данных и устраняет неоднозначность
  • снижает когнитивную нагрузку, устраняет необходимость знания контекста
  • уменьшает количество ошибок
  • делает код самодокументируемым
  • ускоряет ревью
  • упрощает рефакторинг

Плохие имена заставляют:

  • читать тело функции
  • искать комментарии
  • бояться менять код
...

Неверно подобранное имя создаёт неоднозначность зоны ответственности, что создаёт почву для антипаттерна «божественный объект». Например, вместо имени TaskManager лучше использовать конкретные имена, отражающие зоны ответственности: TaskScheduler, TaskRepository, TaskValidator.

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

  • если код решает бизнес-задачу — имена должны быть бизнесовыми
  • если код решает техническую задачу — имена должны быть техническими

Если в бизнес-логику проникло имя, отображающее детали реализации, следует обратить внимание на архитектуру.

Публичные имена не должны раскрывать детали реализации

Публичное имя — это часть контракта, а не реализации. Оно должно говорить, что делает код, а не как это сделано.

Если публичное имя раскрывает конкретику, вы привязываете пользователя API к текущей реализации.

Примеры неудачных публичных имён:

  • getUserFromHashMap
  • saveToPostgres
  • loadFromFile
  • parseJsonWithSerde

Иногда конкретика допустима, если она составляет смысл API.

Будьте последовательны — придерживайтесь единого стиля

Признаки «грязного кода» в именовании:

  • стиль меняется от файла к файлу
  • зависит от автора
  • не подчиняется правилам

Примеры плохой консистентности:

  • user_count
  • userCount
  • UserCnt
  • numUsers

Или методы, выполняющие одно и то же но с разным стилем:

  • getUser()
  • fetchUser()
  • retrieveUser()

Имя должно полно и точно описывать сущность

  • date — плохое имя, непонятно, какая дата
  • currentDate — лучше, сразу ясно, что это текущая дата

Имя выражает «что это», а не «как используется»

Имя не должно описывать реализацию или способ получения значения:

  • temp — плохо
  • flag — плохо
  • dataReady — лучше
  • currentCustomerNumber — лучше

Порядок слов имеет значение

  • NumCustomer — общее количество клиентов
  • CustomerNum — номер текущего клиента

Именование констант

  • FIVE = 5.5 — плохое имя
  • CYCLE_NEEDED — лучше
  • MAX_RETRY_COUNT — лучше

Слова-паразиты

Следует избегать слов, которые не дают полезной информации: manager, data, info, helper, handler, processor


Code review

Хорошее код-ревью анализирует изменения и их соответствие общей структуре кода. Оно проверяет понятность названия и описания, а также причину внесения того или иного изменения. Рецензия включает в себя ...

Code review

Книга "Орош Гергели - Разработчик ПО (Библиотека программиста)"

Хорошее код-ревью анализирует изменения и их соответствие общей структуре кода. Оно проверяет понятность названия и описания, а также причину внесения того или иного изменения. Рецензия включает в себя проверку корректности кода, тестового покрытия, изменений функционала и соблюдения основных принципов программирования и лучших практик. В хорошем код-ревью указывается, например, что необходимо упростить сложный для понимания код, улучшить неясные имена переменных, убрать закомментированный код, восполнить отсутствие тестов или учесть граничные случаи. Также нужно отметить, если в коде для ревью собрано за раз слишком много изменений, и предложить разбить внесение изменений на более мелкие этапы. Лучшие рецензии также оценивают изменения в контексте всей системы и проверяют, насколько легко их будет сопровождать. Они могут поставить под сомнение необходимость изменений и уточнить их влияние на другие части системы. Они обращают внимание на абстракции, добавляемые в код, и оценивают, насколько хорошо они вписываются в существующую архитектуру. Кроме того, такие ревью делают замечания относительно сопро­ вождаемости кода — сложной логики, которую можно упростить, структуры тестов, дублирования и других возможных недочетов.

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

Лучшие рецензенты также проявляют чуткость к разработчику, понимая, что он потратил много времени и усилий на рецензируемые изменения. Они всегда отмечают удачные решения и не скупятся на добрые слова.

Опытные ревьюеры не утверждают изменения, пока остаются какие-либо нерешенные вопросы. Но они всегда указывают, если какие-то недочеты являются некритичными, называя их мелочами (nitpicks), а одобряют изменения фразами вроде LGTM (looks good to me — «все в порядке»). Кроме того, они четко указывают на необходимость дополнительных действий, используя для этого специальные инструменты или командные соглашения.

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

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

Мелкие замечания (nitpicks) касаются незначительных изменений, которые не оказывают существенного влияния на общее качество пул-реквеста, например: предложение другого имени переменной или функции, требование расставить объявления переменных в алфавитном порядке или поправить отступы. Опытные ревьюеры всегда указывают, какие комментарии являются «мелочью», и стараются с ними не перебарщивать, ведь слишком много таких замечаний не только огорчают, но и отвлекают от более важных проблем.

Лучшие ревьюеры объясняют новичкам предпочитаемые в команде подходы и предлагают им ресурсы для дальнейшего обучения. Они доброжелательно настроены и всегда отмечают первые удачные изменения, внесенные новичком в кодовую базу.

Дополнительные рекомендации по проведению качественных код-ревью можно найти в:

  • Руководстве по код-ревью от Google
  • Руководстве по код-ревью от GitLab

Refactoring

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

Стив Макконнелл. «Совершенный код». Глава 24. Рефакторинг

Steve McConnell - (Code Complete)

Факторинг - это декомпозиция на составные части.

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

Причины рефакторинга:

  • код повторяется
  • метод слишком велик или принимает много параметров
  • большая вложенность цикла
  • класс с большой связанностью
  • интерфейс класса не формирует согласованную абстракцию
  • класс имеет две и более областей ответственности из-за чего его части изменяются независимо от других
  • одно изменение требует множественного изменения в других классах
  • зависимость от иерархии параллельного наследования
  • родственные елементы данных используемые вместе не организованны вместе
  • метод использует больше елементов другого класса чем своих
  • один класс слишком много знает о другом классе
  • данные-члены сделаны открытыми, что стирает грань между интерфейсов класса и реализацией
  • подкласс использует лишь малую часть методов своих предков, измените отношение классов с "яыляется" на "содержит" (композиция,агрегация) и реализуйте интерфейс с этой малой долей методов
  • программа содержит код который когда-то понадобится

Refactoring

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

Важность рефакторинга становится очевидной по мере роста и развития программного проекта. Код, написанный в начале разработки, может быть неоптимальным и сложным. Накопление такого кода замедляет разработку и закладывает проблемы на будущее. Рефакторинг позволяет устранить этот технический долг, делая код более понятным, гибким и эффективным.

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

💡Написать код, понятный компилятору, сможет любой дурак. Но лишь не­ многие смогут написать код, понятный людям.

Основные этапы рефакторинга:

  1. Выявление проблем (code smells и антипаттернов).
  2. Применение техник рефакторинга для их устранения.
    1. Rename Method → Улучшение читаемости.
    2. Extract Class → Разделение ответственности.
    3. Replace Conditional with Polymorphism → Упрощение логики.
    4. Introduce Parameter Object → Уменьшение количества параметров.
  3. Тестирование, чтобы убедиться, что функциональность не сломана.

Когда нужно выполнять рефакторинг - им следует за­ниматься понемногу и постоянно. Надо не принимать решение о проведении ре­факторинга, а проводить его, потому что вы должны делать свою работу.

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

Код с душком и его рефакторинг по Мартину Фаулеру.

  • Дублирование кода

    Увидев одинаковые кодовые структуры в нескольких местах, можно быть уверенным, что если удастся их объединить, программа от этого только выиграет.

    Лечение: применить рефакторинг "Извлечение метода", "Извлечение класса". Если оба метода делают одно и то же с помощью разных алгоритмов то применить рефакторинг "Замена алгоритма".

  • Длинный метод

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

    Лечение: использовать рефакторинг "Извлечение метода", найдите части метода, которые кажутся связанны­ми между собой, и образуйте новый метод. Найти код который нуждается в комментарии и выделить его в отдельный метод с ясным названием, имя которого основано на этом комментарии. Если у вас метод со множеством параметров и временных переменных, это мешает выделению нового метода. Применяя рефакторинг "Извлечение метода", приходится передавать в качестве параметров такое количество параметров и временных переменных, что результат оказывается ничуть не проще, чем ориги­нал. Устранить временные переменные можно с помощью рефакторинга "Замена временной переменной запросом", а длинные списки параметров мож­но сократить с помощью рефакторингов "Введение объекта параметра" и "Сохранение всего объекта" или если после этого все равно остается слишком много временных переменныхи параметров то "Замена метода объектом методов".

  • Большой класс

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

    Лечение: устранение избыточности, рефакторинг "Извлечение класса" или "Извлечение подкласса" позволяет связать некоторое коли­чество членов-данных вместе для которых это имеет смысл. Полезным приемом является определение того, как клиенты используют класс, и применение рефакторинга "Извлечение интерфейса" для каждо­го из этих применений. В результате может выясниться, как разделить класс еще сильнее. Если большой класс является классом GUI, может потребоваться переместить его данные и поведение в отдельный объект предметной области. Это может по­требовать хранения дублированных данных в двух местах и поддерживать их со­гласованность. Рефакторинг "Дублирование видимых данных"

  • Длинный список параметров

    В длинных списках параметров трудно разбираться, они становятся противоречивыми и сложными в использовании, а также потому, что их приходится вечно изменять по мере того, как возникает необходимость в новых данных.

    Лечение: если же передавать методам объекты, то изменений потребуется меньше, так как для получения новых данных, скорее всего, хватит пары запросов.

    Исключение, когда такие изменения вносить не следует, когда мы не хотим создавать явную зависимость.

  • Расходящиеся модификации

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

    Лечение: тогда имеет смысл выделить новые классы по типу будущих изменений. Для приведения кода в порядок сле­дует определить все, что модифицируется по данной конкретной причине, а затем применить рефакторинг "Извлечение класса". Так каждый класс будет иметь свою точно определенную зону от­ветственности и изменяться только в соответствии с изменениями в этой зоне.

  • «Стрельба дробью»

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

    Лечение: в этой ситуации следует свести все изменения в один класс, использовав ре­факторинги "Перенос метода" и "Перенос поля". Если подходяще­го кандидата среди имеющихся классов нет, создайте новый класс. Часто можно применить рефакторинг "Встраивание класса" и поместить целый пакет методов в один класс.

  • Параллельные иерархии наследования

    Параллельные иерархии наследования в действительности являются особым случаем «стрельбы дробью». В данном случае всякий раз при порождении подкласса одного из классов приходится создавать подкласс другого класса.

    Лечение: общая стратегия устранения дублирования состоит в том, чтобы заставить экземпляры одной иерархии ссылаться на экземпляры другой. С помощью рефак­торингов "Перенос метода" и "Перенос поля" можно устранить излишнюю иерархию.

  • Завистливые функции

    Весь смысл объектов в том, что они позволяют хранить данные вместе с процедурами их обработки. Классический пример дурного запаха - метод, который больше интересуется не тем классом, в котором он находится, а каким-то другим. Чаще всего предметом зависти являются данные. Не счесть случаев, когда мы сталкивались с методом, вызывающим полдюжины методов доступа к данным другого объекта, чтобы вычислить некоторое значение.

    Лечение: метод следует перенести в другое место с помощью рефакторинга "Перенос метода". Разумеется, есть несколько сложных схем, нарушающих это правило. На ум сразу приходят паттерны «Стратегия» (Strategy pattern, Gang of Four) и «Посетитель» (Visitor pattern, Gang of Four)

  • Группы данных

    Данные - как дети: они любят сбиваться в тесные группы, элементы данных встречаются во множестве мест: поля в паре классов, параметры в нескольких методах.

    Лечение: данные, встречающиеся совместно, имеет смысл превращать в отдельный класс. Рефакторинг "Извлечение класса" преобразовать группы данных в объект. Затем следу­ет обратить внимание на сигнатуры методов и применить рефакторинг "Введение объекта параметра" или "Сохранение всего объекта" для сокраще­ния их объема. Непосредственной выгодой от этого являются сокращение многих списков параметров и упрощение вызовов методов.

  • Классы данных

    Такие классы содержат поля, методы для получения get... и установки значений set... этих полей и ничего больше. Такие классы - бессловесные хранилища данных, которыми другие классы наверняка манипулируют излишне обстоятельно.

    Лечение: рефакторинг "Инкапсуляция поля", "Инкапсуляция кол­лекции", все поля такого класса должны быть инкапсулированы. Рефакторинг "Сокрытие метода" применяется ко всем полям, значение которых не должно изменяться. Рефакторинг "Перенос метода" для перемеще­ния поведения в класс данных. Если метод не удается переместить целиком, вос­пользуйтесь рефакторингом "Извлечение метода", чтобы создать метод, который можно переместить. Через некоторое время можно начать применять рефакторинг "Сокрытие метода" к методам получения и установки значе­ний полей. Классы данных подобны детям. В качестве начальной точки они годятся, но чтобы участвовать в работе в качестве взрослых объектов, они должны принять на себя определенную ответственность.

  • Одержимость элементарными типами

    Те, кто занимается объектами недавно, обычно не любят использовать малень­кие объекты для маленьких задач, такие как, например, денежные классы, объеди­няющие численное значение и валюту, диапазоны с верхней и нижней границами или специализированные строки наподобие телефонных номеров или почтовых индексов. Выбраться в мир объектов помогает рефакторинг "Замена значения данных объектом".

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

    Rust: Type safety (Newtype)

  • Операторы типа switch

    Одним из очевидных признаков объектно-ориентированного кода служит сравнительная немногочисленность операторов типа switch (или case). Проблема, обусловленная применением switch, по существу, связана с дублированием. Часто один и тот же блок switch оказывается разбросанным по разным местам программы. При добавлении в switch нового варианта приходится искать все эти блоки switch и модифицировать их. Объектно­ ориентированная концепция полиморфизма предоставляет элегантный способ справиться с этой проблемой.

    Лечение: задача заключается в том, чтобы выяснить, где должен применяться полиморфизм. Часто инструкция switch работает с кодами ти­пов, переключая поведение для разных типов. При этом необходим метод или класс, хранящий значение кода типа. Воспользуйтесь рефакторингом "Извлече­ние метода" для выделения инструкции switch, а затем рефакторингом "Перенос метода" для ее вставки в класс, где требуется полиморфизм. В этот момент вы должны решить, чем именно воспользоваться - рефакторин­гом "Замена кода типа подклассами" или рефакторингом "Замена кода типа состоянием/стратегией". Разобравшись в структуре наследования, можно применить рефакторинг "Замена условной инструкции полиморфиз­мом".

    Замена условной инструкции (if, case, match, switch...) полиморфиз­мом:

    enum Payment {
       CreditCard { amount: f64 },
       Paypal { amount: f64 },
       Crypto { amount: f64 },
    }
    impl Payment {
       fn process(&self) {
          match self {
                Payment::CreditCard { amount } => {
                   println!("Processing credit card payment: ${amount}");
                }
                Payment::Paypal { amount } => {
                   println!("Processing PayPal payment: ${amount}");
                }
                Payment::Crypto { amount } => {
                   println!("Processing crypto payment: ${amount}");
                }
          }
       }
    }
    fn main() {
       let p = Payment::Paypal { amount: 42.0 };
       p.process();
    }

    Замена на полиморфизм:

    trait Payment {
       fn process(&self);
    }
    struct CreditCard {
       amount: f64,
    }
    impl Payment for CreditCard {
       fn process(&self) {
          println!("Processing credit card payment: ${}", self.amount);
       }
    }
    struct Paypal {
       amount: f64,
    }
    impl Payment for Paypal {
       fn process(&self) {
          println!("Processing PayPal payment: ${}", self.amount);
       }
    }
    struct Crypto {
       amount: f64,
    }
    impl Payment for Crypto {
       fn process(&self) {
          println!("Processing crypto payment: ${}", self.amount);
       }
    }
    // Обобщённая функция, которая работает с любым типом, реализующим Payment
    fn process_payment<T: Payment>(payment: &T) {
       payment.process();
    }
    fn main() {
       // статическая диспетчеризация
       let credit = CreditCard { amount: 100.0 };
       let paypal = Paypal { amount: 42.0 };
       let crypto = Crypto { amount: 0.005 };
       process_payment(&credit);
       process_payment(&paypal);
       process_payment(&crypto);
       // динамическая диспетчеризация (trait objects)
       let payments: Vec<Box<dyn Payment>> = vec![
          Box::new(CreditCard { amount: 100.0 }),
          Box::new(Paypal { amount: 42.0 }),
          Box::new(Crypto { amount: 0.005 }),
       ];
       for p in payments {
          p.process();
       }
    }
  • Ленивый класс

    Сопровождение и понимание каждого создаваемого класса влечет опреде­ленные затраты. Класс, существование которого не окупается выполняемымиим функциями, должен быть ликвидирован.

    Лечение: если у вас есть подклассы с недостаточной функциональ­ностью, попробуйте применить рефакторинг "Свертывание иерархии". Компоненты, являющиеся практически бесполезными, должны быть подвергну­ты рефакторинrу "Встраивание класса".

    💬 осторожно цикличность, добавив классы на предыдущих этапах рефакторинга, в этом случае снова его удалить :)

  • Временное поле

    Обычно временные поля возникают, когда сложному алгоритму требуется несколько переменных. Программист, который реализовывал алгоритм, не захо­тел пересылать большой список параметров (да и кто бы захотел?), поэтому он разместил их в полях. Но поля корректны только во время работы алгоритма; в других контекстах они лишь вводят в заблуждение.

    Лечение: в таком случае можно приме­нить рефакторинг "Извлечение класса" к переменным и методам, в кото­рых требуются эти поля. Новый объект является объектом метода

  • Цепочки вызовов

    Такие a->b->c->d после­довательности вызовов означают, что клиент связан с навигацией по структуре классов. Любые изменения промежуточных связей приводят к необходимости модификации клиента.

    Лечение: рефакторинг "Сокрытие делегирования", при­чем его можно использовать в различных местах цепочки.

  • Посредник

    Одной из основных характеристик объектов является инкапсуляция - сокры­тие внутренних деталей реализации от внешнего мира. Инкапсуляции часто со­путствует делегирование. Если делегирования слишком много, например, мы просмат­риваем интерфейс класса и обнаруживаем, что половина методов делегирует об­работку другому классу.

    Лечение: В таком случае нужно воспользоваться рефакторингом "Удаление посредника" и общаться с объектом, который действительно знает, что происходит.

  • Неуместная близость

    Иногда классы оказываются в слишком близких отношениях и чаще, чем следовало бы, погружены в закрытые части друг друга.

    Лечение: с помощью рефакторингов "Пе­ренос метода" и "Перенос поля" необходимо разделить части кода, чтобы уменьшить их близость. Посмотрите, нельзя ли прибегнуть к рефакторингу "Замена двунаправленной связи однонаправленной". Если у классов есть общие интересы, воспользуйтесь рефакторингом "Извлечение класса", чтобы поместить общую деятельность в безопасное место и превратить их в доб­ропорядочные классы. Можно также применить рефакторинг "Замена наследо­вания делегированием", позволив другому классу выступать в качестве промежуточного звена.

  • Неполный библиотечный класс

    Если в библиотечный класс надо включить всего лишь пару новых методов, используйте рефакторинг "Введение внешнего метода". Если дополни­тельная функциональность достаточно велика, необходимо применить рефакто­ринг "Введение локального расширения"

  • Отказ от наследования

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

    Лечение: необходимо создать новый класс на одном уровне с потомком и с помощью «Спуска метода» (Push Down Method) и «Спуска поля» (Push Down Field) вытолкнуть в него все бездействующие методы. Благодаря этому в родительском классе будет содержаться только то, что используется совместно.

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

  • Комментарии

    Комментарии полезны, когда вы не знаете, что делать. Помимо описания происходящего, комментарии могут отмечать те места, в которых вы не увере­ны. Комментарии - хорошее место пояснить, почему вы поступаете именно так. Эта информация пригодится тем, кто будет работать с вашим кодом в будущем.

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

    При проведении рефакторинга, важным предварительным условием является наличие надежных тестов. Существует естественная связь между паттернами и рефакторингом. Паттерны представляют собой цели; рефакторинг дает методы их достижения.

refactoring.com

refactoring.guru

sourcemaking.com

Что такое рефакторинг? (foxminded.ua)


Программируйте в терминах проблемной области

Steve McConnell - (Code Complete)

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

Высокоуровневый код не должен включать подробных сведений о файлах, стеках, очередях, массивах, символах и подобных объектах, имеющих имена вроде i, j и k. Высокоуровневый код должен описывать решаемую проблему. Он должен быть наполнен описательными именами классов и вызовами методов, ясно характеризующими выполняемые действия, а не подробными сведениями о том, что файл открывается в режиме «только для чтения». Высокоуровневый код не должен быть загроможден комментариями, гласящими, что «здесь переменная i представляет индекс записи из файла о сотрудниках, а чуть позже она используется для индексации файла счетов клиентов».

Это неуклюжая методика программирования. На самом высоком уровне программы не нужно знать, что данные о сотрудниках представлены в виде записей или хранятся в файле. Информацию, относящуюся к этому уровню детальности, надо скрыть. На самом высоком уровне вы не должны иметь понятия о том, как хранятся данные. Вы не должны читать комментарии, объясняющие роль переменной i и то, что она используется с двойной целью. Вместо этого вы должны видеть две переменные с выразительными именами, такими как employeeIndex и clientIndex.

Разделение программы на уровни абстракции

Очевидно, что на некотором уровне надо работать и в терминах реализации, но вы можете изолировать эти части программы от частей, разработанных в терминах проблемной области.

Проектируя программу, обдумайте уровни абстракции:

Уровни абстракции: 4. Высокоуровневые элементы 3. Низкоуровневые элементы
2. Низкоуровневые структуры реализации

  1. Структуры и средства языка программирования
  2. Возможности операционной системы и машинные команды

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

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

Уровень 1: структуры и средства языка программирования. Структуры языка программирования — это элементарные типы данных, управляющие структуры и т. д. Кроме того, большинство популярных языков снабжено дополнительными библиотеками, предоставляют доступ к вызовам ОС и т. д. Вы используете эти структуры и средства естественным образом, так как программировать без них невозможно. Многие программисты никогда не поднимаются выше этого уровня абстракции, чем значительно осложняют себе жизнь.

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

Уровень 3: низкоуровневые элементы проблемной области. На этом уровне вы имеете дело с примитивами, нужными для работы в терминах проблемной области. Это клей, скрепляющий нижележащие структуры компьютерных наук и высокоуровневый код проблемной области. Чтобы писать код на этом уровне, вы должны определить словарь проблемной области и создать строительные блоки, годные для решения поставленной задачи. Во многих приложениях этим уровнем является уровень бизнесобъектов или уровень сервисов.

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

Уровень 4: высокоуровневые элементы проблемной области. Этот уровень формирует абстракцию, позволяющую работать с проблемой в ее собственных терминах. Код, написанный на этом уровне, должен быть частично понятен даже людям, далеким от программирования — возможно, и вашим заказчикам. Он будет слабо зависеть от специфических аспектов языка программирования, потому что вы будете использовать для работы над проблемой собственный набор средств. Так что на этом уровне ваш код больше зависит от средств, созданных вами на уровне 3, чем от возможностей языка.

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

Многие программисты находят полезным дополнение этих концептуальных уровней другими, перпендикулярными «уровнями». Например, типичная трехуровневая архитектура пересекает описанные выше уровни, предоставляя дополнительные средства интеллектуального управления аспектами проектирования и кодом.


Dependency Injection (Внедрение зависимостей)

Dependency Injection (Внедрение зависимостей) — это архитектурный шаблон проектирования и техника реализации принципа инверсии управления (Inversion of Control, IoC), при которой объект получает зависящие от него компоненты извне, вместо того чтобы создавать их самостоятельно.

Основная цель DI — отделение создания зависимостей от их использования, что делает код более гибким, тестируемым и поддерживаемым.

Используя DI, можно легко заменять реальные зависимости на заглушки (stubs) или моки (mocks) при тестировании. Это позволяет изолировать код от внешних зависимостей, таких как базы данных, веб-сервисы или другие ресурсы, что упрощает процесс тестирования и делает тесты более надежными и быстрыми.

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

DI помогает снизить уровень связности между компонентами системы. Вместо жесткого связывания объектов с конкретными реализациями, объекты получают зависимости извне, что способствует более слабой связи (loose coupling) и более модульному и повторно используемому коду.

Ключевые аспекты и преимущества

  • Слабая связанность (Loose Coupling): Ваши классы взаимодействуют через интерфейсы или абстракции, а не через жестко зашитые конкретные реализации, что позволяет менять внутреннее устройство модулей без переписывания зависимого кода.
  • Тестируемость: Благодаря внешней передаче зависимостей Вы можете легко подменять реальные компоненты (например, драйверы базы данных или сетевые клиенты) на легковесные заглушки (mocks и stubs) при написании юнит-тестов.
  • Соответствие принципам SOLID: Техника напрямую реализует принцип инверсии зависимостей (Dependency Inversion Principle), отделяя создание объектов от их бизнес-логики.

Основные способы реализации

  1. Внедрение через конструктор (Constructor Injection): Зависимости передаются в класс в момент его создания. Это наиболее предпочтительный и безопасный вариант, гарантирующий, что объект никогда не окажется в невалидном состоянии без необходимых компонентов.
  2. Внедрение через сеттер (Setter Injection): Зависимости устанавливаются через специальные методы после инициализации объекта. Подходит для опциональных или изменяемых в ходе работы зависимостей.
  3. Внедрение через параметры метода (Method Injection): Зависимость передается непосредственно в сигнатуру метода, которому она требуется для выполнения разовой операции.

В крупных программных системах для автоматизации этого процесса применяются специальные контейнеры внедрения зависимостей (DI Containers или IoC Containers), которые самостоятельно разрешают граф зависимостей и управляют жизненным циклом объектов.

"Локатор служб" (Service Locator) — это альтернативный, но часто считающийся антипаттерном, шаблон для управления зависимостями. Ключевое отличие от DI:

  • DI (Внедрение зависимостей): Класс пассивно получает зависимости извне ("Don't call us, we'll call you"). Это делает зависимости явными (особенно в конструкторе).
  • Service Locator: Класс активно запрашивает зависимости из глобального или доступного контейнера ("Дайте мне то, что мне нужно"). Это скрывает зависимости, делая код менее прозрачным и усложняя тестирование.

Контейнер внедрения зависимостей (DI Container) — это тот самый "общий механизм", который автоматизирует создание объектов и разрешение их зависимостей. Он знает, как создавать сервисы и что куда передавать (на основе конфигурации, атрибутов или авторегистрации).

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

Пример:

use std::io::{self, BufWriter, Write};
use std::cell::RefCell;
pub trait Writer {
    fn to_stdout(&self, output: &[u8]) -> Result<(), String>;
    fn to_stderr(&self, output: &[u8]) -> Result<(), String>;
}
pub struct UserWriter<W: Writer> {
    writer: W,
}
// Для возможности применения Default необходим конкретный тип (ImplWriter)
// но основные методы реализованы в обобщенном виде для любого типа Writer
impl UserWriter<ImplWriter> {
    pub fn new() -> Self {
        Self {
            writer: ImplWriter,
        }
    }
}
impl Default for UserWriter<ImplWriter> {
    fn default() -> Self {
        Self::new()
    }
}
impl<W: Writer + Default> UserWriter<W> {
    pub fn new_with(writer: W) -> Self {
        Self {
            writer,
        }
    }
    pub fn call(&self, msg: &str) -> Result<(), String>{
        self.writer.to_stdout(msg.as_bytes())
    }
}
#[derive(Default)]
pub struct ImplWriter;
impl Writer for ImplWriter {
    fn to_stdout(&self, output: &[u8]) -> Result<(), String> {
        if !output.is_empty() {
            let stdout = io::stdout();
            let handle = stdout.lock();
            let mut writer = BufWriter::new(handle);
            writer.write_all(output).map_err(|e|e.to_string())?;
            writer.flush().map_err(|e|e.to_string())?;
        }
        Ok(())
    }
    fn to_stderr(&self, output: &[u8]) -> Result<(), String> {
        if !output.is_empty() {
            let stderr = io::stderr();
            let handle = stderr.lock();
            let mut writer = BufWriter::new(handle);
            writer.write_all(output).map_err(|e|e.to_string())?;
            writer.write_all(b"\n").map_err(|e|e.to_string())?;
            writer.flush().map_err(|e|e.to_string())?;
        }
        Ok(())
    }
}
// Вариант использования DI в функциях
pub fn other_some_di(writer: &impl Writer, msg: &str) -> Result<(), String>{
    writer.to_stdout(msg.as_bytes())
}
pub fn other_some_di_2<W: Writer>(writer: &W, msg: &str) -> Result<(), String>{
    W::to_stdout(writer, msg.as_bytes())
}

// Вариант использования DI в методах
struct SomeEntity;
impl SomeEntity{
    fn call(&self, writer: &impl Writer, msg: &str) -> Result<(), String>{
        writer.to_stdout(msg.as_bytes())
    }
    fn call_dyn(&self, writer: &dyn Writer, msg: &str) -> Result<(), String>{
        writer.to_stdout(msg.as_bytes())
    }
    fn call_box(&self, writer: Box<dyn Writer>, msg: &str) -> Result<(), String>{
        writer.to_stdout(msg.as_bytes())
    }
}

// Ленивая инициализация с помощью внутренней изменчивости
struct LazyDI<W: Writer + Default>(RefCell<Option<W>>); 
    
impl<W: Writer + Default> LazyDI<W>{
    fn call(&self, msg: &str)-> Result<(), String>{
        if self.0.borrow().is_none(){
            *self.0.borrow_mut() = Some(W::default());
        }
         
        if let Some(writer) = self.0.borrow().as_ref(){
            writer.to_stdout(msg.as_bytes());
        }
        
        Ok(()) 
    }
}

// Ленивая инициализация с помощью замыкания
// unimplemented!
fn main(){
    let mut user_writer = UserWriter::default();
    user_writer.call("hello 1\n");


    let w = ImplWriter;
    other_some_di(&w,"hello 2\n");
    other_some_di_2(&w,"hello 3\n");

    let s = SomeEntity;
    s.call(&w, "hello 4\n");
    s.call_dyn(&w, "hello 5\n");
    s.call_box(Box::new(w), "hello 6\n");

    let l: LazyDI<ImplWriter> = LazyDI(RefCell::new(None));
    l.call("hello 7\n");
}

#[cfg(test)]
mod test_writer{
    use std::{
        cell::RefCell,
        path::{Path, PathBuf},
        rc::Rc,
    };

    use super::{Writer, UserWriter};

    pub struct TestImplWriter {
        pub stdout: Rc<RefCell<Vec<u8>>>,
        pub stderr: Rc<RefCell<Vec<u8>>>,
    }
    impl Default for TestImplWriter {
        fn default() -> Self {
            Self {
                stdout: Rc::new(RefCell::new(Vec::new())),
                stderr: Rc::new(RefCell::new(Vec::new())),
            }
        }
    }
    impl TestImplWriter {
        pub fn set(
            &mut self,
            stdout: Rc<RefCell<Vec<u8>>>,
            stderr: Rc<RefCell<Vec<u8>>>,
        ) {
            self.stdout = stdout;
            self.stderr = stderr;
        }
    }
    impl Writer for TestImplWriter {
        fn to_stdout(
            &self,
            output: &[u8],
        ) -> Result<(), String> {
            if !output.is_empty() {
                self.stdout.borrow_mut().extend_from_slice(output);
            }
            Ok(())
        }
        fn to_stderr(
            &self,
            output: &[u8],
        ) -> Result<(), String> {
            if !output.is_empty() {
                self.stderr.borrow_mut().extend_from_slice(output);
                self.stderr.borrow_mut().extend_from_slice(b"\n");
            }
            Ok(())
        }
    }

    #[test]
    fn test(){
        let result_stdout = Rc::new(RefCell::new(Vec::new()));
        let result_stderr = Rc::new(RefCell::new(Vec::new()));

        let mut writer = TestImplWriter::default();
        writer.set(result_stdout.clone(), result_stderr.clone());
        let mock_user_writer = UserWriter::new_with(
            writer,
        );
 
        mock_user_writer.call("hello\n");

        let guard = result_stdout.borrow();
        let output_str = String::from_utf8_lossy(&guard);
        assert_eq!(output_str, "hello\n");

        let guard = result_stderr.borrow();
        let output_str = String::from_utf8_lossy(&guard);
        assert_eq!(output_str, "");
    }
}

Квадрант технического долга по Мартину Фаулеру

Мартин Фаулер, известный эксперт в области разработки программного обеспечения, предложил модель квадранта технического долга, которая помогает классифицировать виды технического долга на основе двух критериев:

Technical Debt Quadrant

Мартин Фаулер, известный эксперт в области разработки программного обеспечения, предложил модель квадранта технического долга, которая помогает классифицировать виды технического долга на основе двух критериев:

  1. Намеренный (Intentional) vs. Ненамеренный (Unintentional)
  2. Безрассудный (Reckless) vs. Осторожный (Prudent)

Эта модель разделяет технический долг на четыре квадранта:

1. Намеренный и Осторожный (Prudent & Intentional Debt)

Описание:

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

Примеры:

  • Временное упрощение архитектуры для быстрого вывода продукта на рынок.
  • Использование устаревших технологий с целью сокращения времени разработки, зная, что позже будет проведено обновление.

2. Намеренный и Безрассудный (Reckless & Intentional Debt)

Описание:

Технический долг создаётся осознанно, но без достаточного обоснования или без учёта будущих последствий. Это может привести к проблемам с поддержкой и масштабированием приложения.

Примеры:

  • Игнорирование стандартов кодирования ради ускорения разработки без планирования рефакторинга.
  • Внедрение временных решений без понимания их влияния на долгосрочную устойчивость системы.

3. Ненамеренный и Осторожный (Prudent & Unintentional Debt)

Описание:

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

Примеры:

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

4. Ненамеренный и Безрассудный (Reckless & Unintentional Debt)

Описание:

Технический долг появляется из-за небрежности или отсутствия дисциплины в процессе разработки. Это часто связано с плохими практиками, недостаточным тестированием или игнорированием стандартов.

Примеры:

  • Частое внесение изменений в код без соответствующих тестов.
  • Отсутствие кода рецензирования, что приводит к накоплению ошибок и ухудшению качества кода.

Важность Понимания Квадранта Технического Долга

Понимание того, в каком квадранте находится текущий технический долг, помогает команде:

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

Заключение:

Квадрант технического долга Мартином Фаулером предоставляет полезную рамку для анализа и управления техническим долгом в проектах разработки ПО. Осознанное управление техническим долгом способствует улучшению качества кода, повышению эффективности команды и устойчивости продукта в долгосрочной перспективе.


CAP-теорема

CAP-теорема (Consistency, Availability, Partition Tolerance) это фундаментальная теорема в распределённых системах ...

CAP-теорема

Это фундаментальная теорема в распределённых системах, сформулированная Эриком Брюером в 2000 году. Она утверждает, что в распределённой базе данных невозможно одновременно обеспечить три свойства:

  1. Consistency (Согласованность) — все узлы системы видят одни и те же данные в одно и то же время.
  2. Availability (Доступность) — каждый запрос получает ответ (может быть не самым свежим).
  3. Partition Tolerance (Устойчивость к разделению) — система продолжает работать, даже если соединение между узлами разорвано.

Вывод CAP-теоремы

В реальной системе можно добиться только двух из трёх свойств:

  • CP (Consistency + Partition Tolerance) — данные всегда согласованы, но могут быть недоступны в случае разделения сети. (Пример: MongoDB в режиме строгой консистентности)
  • AP (Availability + Partition Tolerance) — система всегда доступна, но данные могут быть несогласованными. (Пример: Cassandra, DynamoDB)
  • CA (Consistency + Availability) — невозможен в распределённых системах, так как при сетевых сбоях одно из свойств обязательно будет нарушено.

Sources