Русский
Русский
English
Статистика
Реклама

Antipatterns

А такой ли уж анти-паттерн этот Service Locator?

28.01.2021 10:04:30 | Автор: admin

В индустрии сложилось устойчивое мнение, что Service Locator является анти-паттерном. Из wiki:

Стоит заметить, что в некотором случае локатор служб фактически является анти-шаблоном.

В этой публикации я рассматриваю тот случай, когда, на мой взгляд, Service Locator анти-шаблоном не является.

Вот что пишут в интернетах по поводу Локатора:

Некоторые считают Локатор Служб анти-паттерном. Он нарушает принцип инверсии зависимостей (Dependency Inversion principle) из набора принциповSOLID. Локатор Служб скрывает зависимости данного класса вместо их совместного использования, как в случае шаблона Внедрение Зависимости (Dependency Injection). В случае изменения данных зависимостей мы рискуем сломать функционал классов, которые их используют, вследствие чего затрудняется поддержка системы.

Service Locator идёт рука об руку с DI настолько близко, что некоторые авторы (Mark Seemann, Steven van Deursen) специально предупреждают:

Service Locator is a dangerous pattern because it almost works. ... Theres only one area where Service Locator falls short, and that shouldnt be taken lightly.

Т.е., Локатор чертовски хорош и работает почти как надо, но есть один момент, который всё портит. Вот он:

The main problem withService Locators the impact of reusability of the classes consuming it. This manifests itself in two ways:

* The class drags along theService Locatoras a redundantDependency.

* The class makes it non-obvious what itsDependenciesare.

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

Другими словами, вот так создавать объекты и внедрять в них зависимости благословляется:

public function __construct(IDep1 $dep1, IDep2 $dep2, IDep3 $dep3){    $this->dep1 = $dep1;    $this->dep2 = $dep2;    $this->dep3 = $dep3;}

а вот так - нет:

public function __construct(ILocator $locator){    $this->locator = $locator;    $this->dep1 = $locator->get(IDep1::class);    $this->dep2 = $locator->get(IDep2::class);    $this->dep3 = $locator->get(IDep3::class);}

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

Property Injection should only be used when the class youre developing has a good Local Default, and you still want to enable callers to provide different implementations of the classs Dependency. Its important to note that Property Injection is best used when the Dependency is optional. If the Dependency is required, Constructor Injection is always a better pick.

Если мы перепишем наш класс с Локатором в таком виде:

public function __construct(ILocator $locator = null){    if ($locator) {        $this->dep1 = $locator->get(IDep1::class);    }}public function setDep1(IDep1 $dep1){    $this->dep1 = $dep1;}

то, а) мы делаем его независимым от наличия Локатора (например, в тестовой среде), б) явным образом выделяем зависимости в setter'ах (также можно аннотировать, документировать, ставить префиксы и решать проблему "неочевидности" зависимостей любым другим доступным способом, вплоть до Ctrl+F по ключу "$locator->get" в коде).

Вот мы и подошли к тому моменту, когда, на мой взгляд, использование Локатора оправдано. В комментах к статье "Какое главное отличие Dependency Injection от Service Locator?" коллега @symbix резюмировал тему статьи так:

SL работает по принципу pull: конструктор "вытягивает" из контейнера свои зависимости.

DI работает по принципу push: контейнер передает в конструктор его зависимости.

Т.е., по сути дела, DI-контейнер объектов может использоваться и как Service Locator:

// push deps into constructorpublic function __construct(IDep1 $dep1, IDep2 $dep2, IDep3 $dep3) {}// pull deps from constructorpublic function __construct(IContainer $container) {    if ($container) {        $this->dep1 = $container->get(IDep1::class);        $this->dep2 = $container->get(IDep2::class);        $this->dep3 = $container->get(IDep3::class);    }}

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

"Анти-паттерн" Service Locator же позволяет нам "вытягивать" из контейнера нужные нам зависимости по мере обращения к ним:

class App {    /** @var \IContainer */    private $container;    /** @var \IDep1 */    private $dep1;    public function __construct(IContainer $container = null) {        $this->container = $container;    }    private function initDep1() {        if (!$this->dep1) {            $this->dep1 = $this->container->get(IDep1::class);        }        return $this->dep1;    }    public function run() {        $dep1 = $this->initDep1();    }    public function setDep1(IDep1 $dep1) {        $this->dep1 = $dep1;    }}

Итого, приведённый выше код:

  • может быть использован без контейнера в конструкторе за счёт возможности внедрения зависимости через setter (например, в тестах);

  • зависимости явно описываются через набор private-методов с префиксом init;

  • иерархия зависимостей не тянется при создании экземпляра данного класса, а создаётся по мере использования.

В таком варианте использования паттерн Service Locator вызывает во мне положительные эмоции и не вызывает отрицательных. Ну если только за малым исключением - при внедрении зависимостей в конструктор (режим "push") DI-контейнер знает, для какого класса создаются зависимости и может внедрять различные имплементации одного и того же интерфейса на основании внутренних инструкций. В режиме "pull" у контейнера нет информации для кого он создаёт зависимости, нужно её дать:

$this->dep1 = $this->container->get(IDep1::class, self::class);

Вот в таком варианте Service Locator становится очень даже "pattern" без всяких "anti".

Подробнее..

Перевод Антипаттерн константа размера массива

11.09.2020 16:15:38 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса C++ Developer. Professional.



Хочу обратить ваше внимание на антипаттерн, который я часто встречаю в коде студентов на Code Review StackExchange и даже в довольно большом количестве учебных материалов (!) других людей. У них имеется массив, скажем, из 5 элементов; а затем, поскольку магические числа это плохо, они вводят именованную константу для обозначения количества элементов 5.

void example(){    constexpr int myArraySize = 5;    int myArray[myArraySize] = {2, 7, 1, 8, 2};    ...


Но решение это так себе! В приведенном выше коде число пять повторяется: сначала в значении myArraySize = 5, а затем еще раз, когда вы фактически присваиваете элементы myArray. Приведенный выше код столь же ужасен с точки зрения обслуживания, как:

constexpr int messageLength = 45;const char message[messageLength] =    "Invalid input. Please enter a valid number.\n";


который, конечно, никто из нас никогда не напишет.

Код, который повторяется, хорошим не является


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

   constexpr int myArraySize = 5;-   int myArray[myArraySize] = {2, 7, 1, 8, 2};+   int myArray[myArraySize] = {3, 1, 4};


Патч выше выглядит так, как будто он изменяет содержимое массива с 2,7,1,8,2 на 3,1,4, но это не так! Фактически он меняет его на 3,1,4,0,0 с дополнением нулями потому что мейнтейнер забыл скорректировать myArraySize в соответствии с myArray.

Надежный подход


Что до подсчета, то компьютеры в этом чертовски хороши. Так пусть же считает компьютер!

int myArray[] = {2, 7, 1, 8, 2};constexpr int myArraySize = std::size(myArray);


Теперь вы можете изменить содержимое массива, скажем, с 2,7,1,8,2 на 3,1,4, изменив только одну строку кода. Дублировать изменение нигде не нужно.

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

for (int elt : myArray) {    use(elt);}std::sort(myArray.begin(), myArray.end());std::ranges::sort(myArray);// Warning: Unused variable 'myArraySize'


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

Как это сделать с помощью std::array?


Иногда программист делает еще один шаг к Темной Стороне и пишет:

constexpr int myArraySize = 5;std::array<int, myArraySize> myArray = {2, 7, 1, 8, 2};


Это должно быть переписано по крайней мере как:

std::array<int, 5> myArray = {2, 7, 1, 8, 2};constexpr int myArraySize = myArray.size();  // или std::size(myArray)


Однако простого способа избавиться от ручного подсчета в первой строке нет. CTAD C++17 позволяет писать

std::array myArray = {2, 7, 1, 8, 2};


но это работает, только если вам нужен массив int это не сработает, если вам нужен массив short, например, или массив uint32_t.

C++20 дает нам std::to_array, что позволяет нам писать

auto myArray = std::to_array<int>({2, 7, 1, 8, 2});constexpr int myArraySize = myArray.size();


Обратите внимание, что это создает C-массив, а затем перемещает (move-constructs) его элементы в std::array. Все наши предыдущие примеры инициализировали myArray с помощью списка инициализаторов в фигурных скобках, который запускал агрегатную инициализацию и создавал элементы массива непосредственно на месте.

В любом случае, все эти варианты результируют в большом количестве дополнительных экземпляров шаблонов по сравнению со старыми добрыми C-массивами (которые не требуют создания экземпляров шаблонов). Поэтому я настоятельно предпочитаю T[] более новому std::array<T, N>.

В C++11 и C++14 у std::array было эргономическое преимущество, заключающееся в возможности сказать arr.size(); но это преимущество испарилось, когда C++17 предоставил нам std::size(arr) и для встроенных массивов. У std::array больше нет эргономических преимуществ. Используйте его, если вам нужна его семантика переменной целостного объекта (передать весь массив в функцию! Вернуть массив из функции! Присваивать массивы с помощью =! Сравнить массивы с помощью ==!), Но в противном случае я рекомендую избегать использование std::array.

Точно так же я рекомендую избегать std::list, если вам не нужна стабильность его итератора, быстрая склейка, сортировка без замены элементов и т. д. Я не говорю, что этим типам нет места в C++; Я просто говорю, что у них есть очень специфический сет скилов, и если вы не используете эти скилы, вы, скорее всего, переплачиваете понапрасну.


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


Читать ещё:


Подробнее..

Перевод Антипаттерн Репозиторий в Android

15.09.2020 14:04:30 | Автор: admin
Перевод статьи подготовлен в преддверии старта курса Android Developer. Professional.



Официальное руководство по архитектуре приложений Android рекомендует использовать классы репозитории (Repository) для предоставления чистого API, чтобы остальная часть приложения могла легко извлекать данные. Однако, на мой взгляд, если вы будете использовать в своем проекте этот паттерн, вы гарантированно увязнете в грязном спагетти-коде.

В этой статье я расскажу вам о паттерне Репозиторий и объясню, почему он на самом деле является антипаттерном для Android приложений.

Репозиторий


В вышеупомянутом руководстве по архитектуре приложений рекомендуется следующая структура для организации логики уровня представления:



Роль объекта репозитория в этой структуре такова:

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

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

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

Репозиторий в Android Architecture Blueprints v2


Около двух лет назад я рецензировал первую версию Android Architecture Blueprints. По идее они должны были реализовывать чистый пример MVP, но на практике эти блюпринты вылились в достаточно грязную кодовую базу. Они действительно содержали интерфейсы с именами View и Presenter, но не устанавливали никаких архитектурных границ, так что это по сути был не MVP. Вы можете посмотреть данный код ревью здесь.

С тех пор Google обновил архитектурные блюпринты с использованием Kotlin, ViewModel и других современных практик, включая репозитории. Эти обновленные блюпринты получили приставку v2.

Давайте же посмотрим на интерфейс TasksRepository из блюпринтов v2:

interface TasksRepository {   fun observeTasks(): LiveData<Result<List<Task>>>   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>   suspend fun refreshTasks()   fun observeTask(taskId: String): LiveData<Result<Task>>   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>   suspend fun refreshTask(taskId: String)   suspend fun saveTask(task: Task)   suspend fun completeTask(task: Task)   suspend fun completeTask(taskId: String)   suspend fun activateTask(task: Task)   suspend fun activateTask(taskId: String)   suspend fun clearCompletedTasks()   suspend fun deleteAllTasks()   suspend fun deleteTask(taskId: String)}


Даже до начала чтения кода можно обратить внимание на размер этого интерфейса это уже тревожный звоночек. Такое количество методов в одном интерфейсе вызвало бы вопросы даже в больших Android проектах, но мы говорим о приложении ToDo, в котором всего 2000 строк кода. Почему этому достаточно тривиальному приложению нужен класс с такой огромной поверхностью API?

Репозиторий как Божественный объект (God Object)


Ответ на вопрос из предыдущего раздела кроется в именах методов TasksRepository. Я могу примерно разделить методы этого интерфейса на три непересекающихся группы.

Группа 1:

fun observeTasks(): LiveData<Result<List<Task>>>   fun observeTask(taskId: String): LiveData<Result<Task>>


Группа 2:

   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>   suspend fun refreshTasks()   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>   suspend fun refreshTask(taskId: String)   suspend fun saveTask(task: Task)   suspend fun deleteAllTasks()   suspend fun deleteTask(taskId: String)


Группа 3:

  suspend fun completeTask(task: Task)   suspend fun completeTask(taskId: String)   suspend fun clearCompletedTasks()   suspend fun activateTask(task: Task)   suspend fun activateTask(taskId: String)


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

Группа 1 это в основном реализация паттерна Observer с использованием средства LiveData. Группа 2 представляет собой шлюз к хранилищу данных плюс два метода refresh, которые необходимы, поскольку за репозиторием скрывается удаленное хранилище данных. Группа 3 содержит функциональные методы, которые в основном реализуют две части логики домена приложения (завершение задач и активация).

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

У нас есть специальный термин для классов, которые объединяют так много обязанностей: Божественные объекты. Это широко распространенный антипаттерн в приложениях на Android. Activitie и Fragment являются стандартными подозреваемыми в этом контексте, но другие классы тоже могут вырождаться в Божественные объекты. Особенно, если их имена заканчиваются на Manager, верно?

Погодите Мне кажется, я нашел более подходящее название для TasksRepository:

interface TasksManager {   fun observeTasks(): LiveData<Result<List<Task>>>   suspend fun getTasks(forceUpdate: Boolean = false): Result<List<Task>>   suspend fun refreshTasks()   fun observeTask(taskId: String): LiveData<Result<Task>>   suspend fun getTask(taskId: String, forceUpdate: Boolean = false): Result<Task>   suspend fun refreshTask(taskId: String)   suspend fun saveTask(task: Task)   suspend fun completeTask(task: Task)   suspend fun completeTask(taskId: String)   suspend fun activateTask(task: Task)   suspend fun activateTask(taskId: String)   suspend fun clearCompletedTasks()   suspend fun deleteAllTasks()   suspend fun deleteTask(taskId: String)}


Теперь имя этого интерфейса намного лучше отражает его обязанности!

Анемичные репозитории


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

Если вы захотите извлечь, скажем, методы completeTask из TasksRepository, куда бы вы их поместили? Согласно рекомендованной Google архитектуре, вам нужно будет перенести эту логику в одну из ваших ViewModel. Это не кажется таким уж плохим решением, но как раз таким оно на самом деле и является.

Например, представьте, что вы помещаете эту логику в одну ViewModel. Затем, через месяц, ваш менеджер по работе с клиентами хочет разрешить пользователям выполнять задачи с нескольких экранов (это релевантно по отношению ко всем ToDo менеджерам, которые я когда-либо использовал). Логика внутри ViewModel не может использоваться повторно, поэтому вам нужно либо продублировать ее, либо вернуть в TasksRepository. Очевидно, что оба подхода плохи.

Лучшим подходом было бы извлечь этот доменный поток в специальный объект, а затем поместить его между ViewModel и репозиторием. Затем разные ViewModel смогут повторно использовать этот объект для выполнения этого конкретного потока. Эти объекты известны как варианты использования или взаимодействия. Однако, если вы добавите варианты использования в свою кодовую базу, репозитории станут по сути бесполезным шаблоном. Что бы они ни делали, это будет лучше сочетаться с вариантами использования. Габор Варади уже освещал эту тему в этой статье, поэтому я не буду вдаваться в подробности. Я подписываюсь почти под всем, что он сказал о анемичных репозиториях.

Но почему варианты использования намного лучше репозиториев? Ответ прост: варианты использования инкапсулируют отдельные потоки. Следовательно, вместо одного репозитория (для каждой концепции домена), который постепенно разрастается в Божественный объект, у вас будет несколько узконаправленных классов вариантов использования. Если поток зависит от сети, и от хранимых данных, вы можете передать соответствующие абстракции в класс варианта использования, и он будет проводить арбитраж между этими источниками.

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

Репозитории вне Android.


Теперь вы можете задаться вопросом, являются ли репозитории изобретением Google. Нет, не являются. Шаблон репозитория был описан задолго до того, как Google решил использовать его в своем руководстве по архитектуре.

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

Эрик Эванс в своей книге Domain Driven Design также описывал репозитории. Он написал:

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


Обратите внимание, что вы можете заменить репозиторий в приведенной выше цитате на Room ORM, и это все равно будет иметь смысл. Итак, в контексте Domain Driven Design репозиторий это ORM (реализованный вручную или с использованием стороннего фреймворка).

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

На самом деле, я почти уверен, что они сочтут эту идею наивной и обреченной на провал. Чтобы понять, почему, вы можете прочитать другую статью, на этот раз Джоэла Спольски (основателя StackOverflow), под названием Закон дырявых абстракций. Проще говоря: работа в сети слишком отличается от доступа к базе данных, чтобы ее можно было абстрагировать без значительных утечек.

Как репозиторий стал антипаттерном в Android


Итак, неужели в Google неверно истолковали паттерн репозитория и внедрили в него наивную идею абстрагироваться от доступа к сети? Я в этом сомневаюсь.

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

Заключение


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

Например, в другом блюпринте Google, на этот раз для архитектурных компонентов, использование репозиториев в конечном итоге привело к таким жемчужинам, как NetworkBoundResource. Имейте в виду, что образец браузера GitHub по-прежнему является крошечным ~2 KLOC приложением.

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

Спасибо за прочтение и, как обычно, вы можете оставлять свои комментарии и вопросы ниже.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru