Статьи

Образцы антипаттернов: дизайн / тестирование

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

Я определенно не первый, кто их идентифицировал и, конечно же, не последний (так что пусть все кредиты идут в первоначальные источники, если таковые имеются). Все они взяты из реальных проектов, в первую очередь Java, хотя другие детали должны остаться не раскрытыми. С этим …

Сначала код, нет времени думать

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

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

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

Случайный деятель

Хорошо, хватит философии, теперь мы переходим к настоящим антипаттернам. Несомненно, многие из вас были свидетелями таких частей кода:

1
2
3
4
5
public void setSomething(Object something) {
    if (!heavensAreFalling) {
        this.something = something;
    }
}

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

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

Вселенная

Часто в приложении есть класс, который ссылается (или на него ссылается) в основном на любой другой класс приложения. Например, все классы в проекте должны быть унаследованы от некоторого базового класса.

1
2
3
public class MyService extends MyBase {
  ...
}
1
2
3
public class MyDao extends MyBase {
  ...
}

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

Другой крайностью является наличие класса, который служит центральным мозгом приложения. Он знает о каждом сервисе / dao /… и предоставляет средства доступа (в большинстве случаев статические), поэтому любой может просто обратиться к нему и попросить что-нибудь, например:

1
2
3
4
5
public class Services {
    public static MyService1 getMyService1() {...}
    public static MyService2 getMyService2() {...}
    public static MyService3 getMyService3() {...}
}

Это вселенные, в конце концов они проникают в каждый класс приложения (легко вызвать статический метод, верно?) И объединяют все вместе. В более или менее большой кодовой базе избавиться от таких вселенных исключительно сложно.

Чтобы предотвратить такие вещи, которые могут испортить ваши проекты, используйте шаблон внедрения зависимостей , реализованный Spring Framework , Dagger , Guice , CDI , HK2 , или передавайте необходимые зависимости через конструкторы или аргументы методов. Это на самом деле поможет вам увидеть запахи на ранней стадии и устранить их еще до того, как они станут проблемой.

Наследственное наследование

Это действительно весело и страшно, очень часто присутствует в проектах, которые предоставляют и реализуют веб-сервисы REST (ful). Предположим, у вас есть класс Customer с несколькими различными свойствами, например:

1
2
3
4
5
6
7
8
public class Customer {
    private String id;
    private String firstName;
    private String lastName;
    private Address address;
    private Company company;
    ...
}

Теперь вас попросили создать API (или предоставить другим способом), который находит клиента, но возвращает только его id , firstName и lastName . Звучит просто, не правда ли? Давайте сделаем это:

1
2
3
4
5
public class CustomerWithIdAndNames {
    private String id;
    private String firstName;
    private String lastName;
}

Выглядит довольно аккуратно, верно? Все счастливы, но на следующий день вы приступаете к работе над другой функцией, где вам нужно разработать API, который возвращает id , firstName , lastName, а также компанию . Звучит довольно просто, у нас уже есть CustomerWithIdAndNames , нужно только обогатить его компанией . Идеальная работа для наследства, верно?

1
2
3
public class CustomerWithIdAndNamesAndCompany extends CustomerWithIdAndNames {
    private Company company;
}

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

Еще одна дорога в ад … Здесь есть довольно много вариантов, самый простой из которых — JSON Views (вот несколько примеров, использующих Джексона ), где базовый класс остается тем же, но могут быть возвращены различные представления о нем. В случае, если вы действительно хотите не получать ненужные данные, другой вариант — это GraphQL, который мы рассмотрели в прошлый раз . По сути, сообщение здесь таково: не создавайте такие луковичные иерархии, используйте одно представление, но используйте разные методы для его сборки и эффективного извлечения необходимых фрагментов.

IDD: развитие на основе if

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

1
2
3
4
int price = ...;
if (calculateTaxes) {
    price += taxes;
}

Со временем бизнес-правила развиваются, так же как и условное выражение, которое они олицетворяют, превращаясь в конце в настоящих монстров:

1
2
3
4
5
int price = ...;
if (calculateTaxes && (country == "US" || country == "GB") &&
    discount == null || (discount != null && discount.getType() == TAXES)) {
    price += taxes;
}

Вы видите, куда я иду с этим, проект построен на основе лучших практик IDD: разработка на основе if. Мало того, что код становится хрупким, подвержен ошибкам условий, очень трудно следовать, его также очень страшно менять! Чтобы быть справедливым, вам также нужно протестировать все возможные комбинации таких ветвей, чтобы убедиться, что они имеют смысл, и правильно подобранная ветка (держу пари, никто на самом деле этого не делает, потому что это огромное количество тестов и усилий)!

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

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

Тестовая лотерея

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

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

Тесты так же важны, как и основной код, который вы запускаете в производство. Они должны быть сохранены, переработаны и содержаться в чистоте. Сохраняйте все свои сборки зелеными все время, если вы видите какие-то ошибки или случайные сбои, исправляйте их немедленно. Это не легко, но во многих случаях вы можете столкнуться с реальными ошибками! Вы должны доверять своим тестам, иначе зачем они вообще нужны?

Test Framework Factory

Этот парень выдающийся. Это происходит так часто, что создается впечатление, что каждая организация просто обязана создавать собственную тестовую среду, обычно поверх существующих. Сначала это звучит как хорошая идея, и, возможно, это действительно так. К сожалению, в 99% случаев результатом является еще одна чудовищная структура, которую никто не хочет использовать, но вынужден это делать, потому что «потребовалось 15 человек / лет, чтобы развиться, и мы не можем выбросить ее после этого». Все борются, производительность падает со скоростью света, а качество вообще не улучшается.

Подумайте дважды, прежде чем идти по этому маршруту. Цель упрощения тестирования сложных приложений, над которыми работает организация, безусловно, является хорошей. Но не попадайтесь в ловушку, бросая на это людей, которые собираются потратить несколько месяцев или даже лет, работая над «идеальным и мощным тестовым каркасом» в изоляции, могут делать большую работу в целом, но не решают никаких реальных проблем. , Вместо этого просто создаю новые. Если вы все-таки решили сесть на этот поезд, постарайтесь оставаться сосредоточенным, устранять болевые точки в их основе, прототипе, всегда запрашивать обратную связь, слушать и повторять… И сохранять его стабильным, простым в использовании, полезным, надежным и легким. насколько это возможно.

Выводы

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

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

Ссылка: Образцы антипаттернов: дизайн / тестирование от нашего партнера по JCG Андрея Редько в блоге Андрея Редько .