Pull to refresh

Comments 101

Мы получаем какие-то данные через зависимость? Передайте сами данные, но уберите ПолучательДанных.

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

Мы же понимаем, что устранить все зависимости в системе не удастся. Классический пример — приложение, работающее с данными через Репозиторий. Зависимость обрабатывающего кода от репозитория все-равно останется. Вопрос только в том, где она в итоге будет: вы либо передадите ее туда, где нужно получить данные, либо вынесете в некий мега-менеджер, который сначала получит требуемые данные из репозитория, а потом вызовет класс, и так для каждого класса; в итоге вы просто получите что-то типа GodObject.

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

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

в итоге вы просто получите что-то типа GodObject.

Нет, вы получите объект который отвечает только за одну вещь — создание объекта.
Обычно эго называют IOC или DI контейнер
Предположим, вы имеете два класса — A и B, которым требуются данные из репозитория. Причем, каждый из классов получает данные с использованием разных критериев поиска. Если вы вынесете логику получения данных за эти классы, то там вы будете вынуждены знать, как A и B формируются критерии поиска. А если классов не два, а десять? А если больше?
IoC-контейнер, в отличие от приведенного примера, не знает об особенностях тех классов, в которые он внедряет зависимости. Он лишь знает, что и куда ему надо внедрить (например, что если в конструкторе порождаемого класса есть параметр типа IServ1).
Репозитарий возвращает IQueryable, сами фильтры внутри классов А и В.
Другой способ — классы А и В из своих фильтров формируют Specification, который в общей форме умеет обрабатывать репозиторий.
И дальше два варианта: либо вы инъектируете в A и B репозиторий, передадите ему спецификацию и получите данные, либо какой-то внешний код вызовет A.GetSpecification(...) и B.GetSpecification(...), после чего запросит у репозитория данные и подсунет их через A.SetData(...) и B.Set(...). Во втором варианте, имхо, код будет запутаннее, т.е. получится ровно то, против чего возражает автор статьи.
Так IQueryable и будет у вас интерфейсом репозитория, только до такой степени общим, что любой репозиторий может быть его реализацией (правда, только в запросной части). В статье же предлагается передать в A и B сами данные
Согласен, но справедливости ради, сейчас часто можно увидеть чрезмерное использование идей выделения абстракций. Этот паттерн применяют даже не рассматривая других альтернатив даже там, где можно проще.

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

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

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

Если вдруг в будущем у кого-то будут скомпилированные классы, но не будет доступа к исходному коду, и при этом библиотека почему-то будет нужна даже в таком виде — можно, как крайний вариант, её декомпилировать и пересобрать или, как менее крайний, сделать всякие классы-обёртки, адаптеры и прочее и уже в них ввести все нужные интерфейсы. И, вообще, то, что в будущем у кого-то может не быть доступа к коду — это не та проблема, решение которой авторам кода стоит брать на себя сегодня в первую очередь.
еще важный момент это понимать какой компонент разрабатывается — если это каркас/фреймверк, то скорее всего понадобятся хорошо продуманные абстракции, в случае же разработки доменной логики или каких-то инфраструктурных вещей — нужно делать максимально понятно и строго
Ценное замечание. Подобные мысли я читал у МакКонелла в «Совершенном коде» про выбор способа обработки ошибок. Способов несколько — от игнорирования ошибки до «обрушивания» программы, каждый — лучший для определённой ситуации, выбор определяется тем, где и зачем надо обрабатывать ошибку.

Так и тут — в зависимости от того, где и зачем, правильное решение будет разным.
Мы получаем какие-то данные через зависимость? Передайте сами данные, но уберите ПолучательДанных.
Мы передаем что-то через зависимость? Рассмотрите модель с событиями и подпиской на них, вместо передачи интерфейса.

Автору стоит посомотреть на модель акторов, как мне кажется.

А вообще SOLID — здравая концепция. Просто если любой принцип возвести в абсолют и довести до абсурда, то получится ерунда

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


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

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

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

В solid нет такого принципа и нет там настроя на то, что зависимости — это проблема.
Автор просто вводит свой принцип. Тут в тексте тоже есть подзаголовок, хотя я при первом прочтении тоже не заметил.
За рамками SOLID: «Принцип устранения зависимостей»
В оригинале — это вообще вторая часть поста.
Я бы сказал, что автор этой статьи плохо понимает SOLID и когда применял SOLID страдал от overarchitecture. Ну и по пунктам:
Dependency Inversion это не «используй интерфейсы». Это о том, что зависимости должны передаваться снаружи, а не создаваться внутри потребителя этой зависимости. И «Передайте сами данные, но уберите получатель данных» — это тоже инверсия зависимостей. Более того, «Передайте сами данные, но уберите получатель данных» это еще и принцип единственности ответственности. Потому что тот класс или метод, который данные обрабатывает, не должен отвечать и за получение данных
> Это о том, что зависимости должны передаваться снаружи, а не создаваться внутри потребителя этой зависимости.

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

Впрочем, Dependency Inversion вообще то не (только) про это:
A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.

Если тупо передавать зависимости снаружи — инверсии не происходит. Это, скорее, про другой DI — Dependency Injection.

> Если бы меня попросили описать все эти проблемы одним словом, я бы выбрал слово «непонятный».

Практически единственное предложение, где соглашусь с автором. Про SOLID все говорят, но мало кто понимает.
Вопрос к автору перевода: если вы добавили собственные трактовки принципов из SOLID (в оригинале их нет), то не стоит ли их пояснить? Например, Single Responsibility — это действительно «делай модули меньше»? Например, если модуль большой и несет более одной ответственности, то да «делай его меньше», а конкретнее — дроби. Но если модуль и так несет одну ответственность и является «high cohesive», то его дробление может привести к получению нескольких сильно связанных модулей (highly coupled), ответственность по которым размазана.
Оригинальные объяснения принципов — по многостраничной статье на каждый. Естественно, все попытки передать смысл в трех словах будут грубым упрощением. Впрочем, если вы придумаете способ выразить SRP в трех словах лучше, чем «делай модули меньше», то я с удовольствием в будущем буду пользоваться вашим вариантом! :)
«Принцип единственной обязанности», если верить Википедии
«Проще», не гарантирует «единственность» обязанности
«Модуль должен делать что-то одно»?
А зачем дополнительно передавать смысл в трех словах, если само определение состоит из трех слов и наиболее точно выражает смысл?
В оригинале SRP поястяется как: «не должно быть больше одной причины для изменения». Здесь нет про больше или меньше, зато критерий вполне конкретный.
Можно увидеть что нибудь живое? Какой нибудь проект в исходниках, класс или что нибудь реальное, что можно попробовать и ощутить. Я повернут на идеальном коде, Фоулере и инверсии управления (которая реально работает, когда приходиться менять функционал, который касается конкретной реализации и не затрагивает абстракцию).
UFO just landed and posted this here
Уровень 0: статистическая функция без побочных эффектов
Уровень 2: класс с изменяемым состоянием, взаимодействующий только с простыми зависимостями, такими как Объект-Значение из Уровня 1.

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

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

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

Я бы трактовал это так: Да, есть ситуации, где без SOLID и интерфейсов не получается. Но в реальности удивительно часто встречаются более простые ситуации, в которых можно подготовить данные и передать сразу их, вместо абстракции ПолучателяДанных.
У всех разная реальность.
В результате все это приводит к тому, что разработчики начинают создавать интерфейсы где попало. Они загромождают свой код интерфейсами типа IFooer, IDoer, IMooer, IPooer.

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

А вообще придерживаться принципов SOLID вместе с KISS и YAGNI — хорошая практика.
Раньше SOLID был краеугольным камнем моего набора средств проектирования. Я делал все возможное, чтобы сделать мой код как можно более SOLID. Я учил других поступать так же.

Вот собственно то, что создаёт проблему любому разработчику. Технологию следует применять в соответствии с ситуацией, а не пихать куда попало. Ведь как и раньше не существует типизированных, 100% достоверных решений различных пробем. Это касается и паттернов проектирования, они описывают типовое решение, типовых задач.
Придумывая названия, используй имена существительные (и не используй отглагольные существительные, которые имеют окончание “er”)

Скажите это разработчикам Java с их StringBuilder, Notifier и прочими.
Ну да, вместо DataLoader я назову DataLoadingService, бинго, обманул. Как будто это научит писать код лучше.
Согласно идеям автора, назвать стоило бы Data и сделать у этого класса метод Load. То есть классы по возможности должны описывать настоящие сущности. Идея в общем не нова, и автор статьи не сам ее придумал. То же DDD вокруг этого строится.
И что бы делал метод Load в классе Data?
Туда же, куда DataLoader и DataLoadingService :)
Если туда же, то это переименование никак не повлияло на дизайн приложения. А какое из названий точнее отражает суть — вопрос сильно открытый.
Т.е., получается, класс Data должен знать об устройстве классов, в которые грузятся его данные, и, вероятно, иметь доступ к их приватным полям…
Разница между первым и вторым всего лишь в том, что первый своим методом вернет загруженные данные, второй же инкапсулирует их в своём состоянии.
Первый вариант, являясь методом без состояния, имеет более низкий уровень, по классификации автора, соответственно он более предпочтителен. «я стараюсь использовать как можно более низкий уровень»(с)
Вопрос лишь в том, как этот более низкий, а конкретно — нулевой, уровень именовать.

Разница между первым и вторым в том, что первый — в общем случае потокобезопасный и взаимозаменяемый, а второй — нет.
Вариация на тему ActiveRecord? В применении к Java отброшено ещё в начале 2000-х как тупиковый путь. Если вы в рантайме можете поменять метод Load у класса Data на другой алгоритм — тогда это ещё может быть хорошей идеей. Если нет — то нет.
А почему метод Load должен быть у именно класса Data, а не у того объекта, в который данные загружаются?
Я подозреваю, что — исходя из названия ...Data — он и есть тот объект, в который данные загружаются.
Подозреваю, большая часть интефейсов и классов стандартной библиотеки Java с именами в виде отглагольных существительных с суффиксом “er” является продуктом workaround'а не-функциональности языка. В языке Java, чтобы передать куда-нибудь функцию в качестве значения аргумента, нужно прописать её сигнатуру в интерфейсе и обернуть её тело в экземпляр ad-hoc класса реализующего этот интерфейс. Много телодвижений, но технология работает, к тому же в наличии типобезопасность и статическая типизация «из коробки», поэтому некоторую громоздкость прощают и считают неизбежной платой за компромисс.
Начав с вообщем-то здравых идей уменьшения количества абстракций и зависимостей автор как-то докатился аж до отрицания наследования и интерфейсов вообще. Немного перебор как по мне.
Есть клевая старая поговорка «Заставь дурака богу молиться — он лоб разобьет».
Статья как раз об этом. Когда выключая голову, начинают тупо следовать каким-то догмам.
А потом, когда хлебнут проблем, так же тупо от них отказываются.

Все эти клевые техники, подходы, «аббревиатуры» и прочие, надо воспринимать исходя из принципа:
«Сказка — ложь, да в ней намек, добрым молодцам урок.»

Но люди почему-то не хотят думать своей головой. Они хоят прочитать пошаговый урок, и тупо повторять его бесконечно для получения денег. Подход как в РПГ. Не жизнь, а тупая гриндилка.
Согласен с автором в том плане что «стало непонятно». Сейчас работаю в проекте где везде сплошной DI/IoC и MVP. Это жесть. Чтобы создать одну вьюху нужно создать 10 классов/интерфейсов и внести изменения (забайндидь эти классы) ещё в 5 файлах.
SOLID — отличная вещь, он позволяет обозвать любой код говнкодом ибо невозможно написать серьезный проект 100% следуя SOLID. Где-нибудь да найдется классик, у которого будет больше одной ответственности к примеру :-).
Вообще апелляция к авторитетам — отличная вещь: всегда, если хорошо покопаться, найдётся авторитет, который говорит то, что тебе надо, и что противоположно тому, что делает оппонент.
Особенно в этом помогает KISS — любые попытки ввести в коде дополнительные абстракции можно смело резать как нарушение этому принципу.
И любые попытки следовать принципу DRY — тоже.
1. Какая же всё-таки у человека в голове каша. Просто адская каша. Путает coupling (связанность) с cohesion (связность) (или перевод неправильный).
2. Есть подозрение, ИМХО, что человек страдает от синдрома ненависти к чужому коду, поскольку категоричен.
3. Fully SOLID design — это оксюморон и так говорит сам дядя Боб. Но автору пофиг, автор сам за других решил о том кто как и о чём думает, не удосуживаясь поковыряться в подкастах от дяди Боба того же, например, и сделал свои глупые выводы.
4. Автор не придумал абсолютно ничего нового, но зато всё неправильно понял.
> Когда я продвигал SOLID, те, кто были против его использования, указывали на эти проблемы.
А однажды мне пришлось разобраться в чужом SOLID-коде, после чего от меня чуть не ушла жена. Теперь я не преподаю SOLID
«Принцип подстановки Барбары Лисков: он нам не важен. Мы вообще почти не используем механизм наследования.»
Годится только если в языке нет наследования. Достаточно однин раз это принцип нарушить, что бы вылезли проблемы в самом неожиданном месте.
Автор статьи либо умышленно лукавит, либо сам не понимает, что в очередной раз описал совершенно другую проблему, и зачем-то притянул её к SOLID. Согласен, что это самая распространённая проблема: случается, что разработчики пишут говнокод.

Это сказано в одном предложении.

Разработчики, применяющие SOLID (в том числе я), часто создают непонятный код.


Чувак, проблема не в SOLID, не в каких-то паттернах и даже не в языке программирования. Никакие золотые правила и принципы не уберегут плохого разработчика от написания говнокода, хоть что делай, уж простите.
Всё полезно в меру и зависит от проекта, это касается всех паттернов GOF, SOLID и т.п.
Но это не значит, что это плохо и давайте нигде их не использовать.
Просто надо уметь это дело «готовить».
Теперь вы в самом прямом смысле не можете найти вызов конструктора в коде.

Вот поэтому я и предпочитаю обходить стороной все IoC-контейнеры.
Вы при этом используете изолированное тестирование в вашем приложении или нет?
Такие люди ничего не используют.
Они не утруждают себя разбираться как работают IoC-контейнеры и какие техники лучше применять при их использовании. Берут копипасту из гугла всякой фигни в свой код, в итоге не могут в этой куче разобраться и провозглашают IoC отстоем по определению.
Если хоть немного пошевелить головой, то IoC не мешает делать нормальный код. Только вот шевелить люди не любят. Копипаста рулит…
Простите, но о каких «таких людях» Вы говорите? Или у Вас уже готова оценка профессионализма по одному предложению?
«Такие люди», это люди которых я описал в каменте.
Если коротко, это которые не разобравшись с технологией, неправильно ее применяют, а потом хают ее.
И в итоге полностью отказываются от нее на основе своего неправильного опыта.
А потом еще и других учат этому своему неправильному опыту.
Спасибо за характеристику. Скопипащу себе в резюме.
Что вы имеете в виду под изолированным тестированием?
То, что в англоязычной литературе называется unit testing — когда каждый элемент программы тестируется независимо от остальных.
Как вы в таком случае подменяете используемые тестируемым элементом зависимости, чтобы зафиксировать их поведение или проверить взаимодействие?
Использую мок-фреймворк. Зависимости, как правило, создаю тут же в тесте.
А как вы их передаете в тестируемый элемент?
Очевидно, что ручным вызовом конструктора =) IoC не является обязательным компонентом для юнит тестирования =)))
Если вы имеете в виду, что зависимости передаются в class under test как параметры конструктора, то это и есть IoC, паттерн Dependency Injection, стратегия Constructor Injection. Так что хотя юнит-тестирование теоретически и возможно без IoC, конкретно ваш сценарий это никак не доказывает.
предпочитаю обходить стороной все IoC-контейнеры

не утруждают себя разбираться как работают IoC-контейнеры

Мне казалось мы обсуждаем ioc-контейнеры, а не указываем оппонентам на разницу между IoC и IoC-контейнерами. Видимо был не прав.
Я не знаю, что вы обсуждаете, а я задал andreycha вполне конкретные вопросы и хочу услышать вполне конкретные ответы. Фразу «IoC не является обязательным компонентом для юнит тестирования» зачем-то вы ввернули.
>это и есть IoC
нет, ручная инжекция — это не IoC. IoC подразумевает, что КТО-ТО другой (фреймворк, контейнер, etc) возьмет на себя управление. IoC не является синонимом DI, он просто там может использоваться для удобства.
IoC подразумевает, что КТО-ТО другой (фреймворк, контейнер, etc) возьмет на себя управление

Вы, конечно, можете привести цитату, подтверждающую ваше утверждение?

А то понимаете ли, в чем дело, с точки зрения вызываемого класса нет никакой разницы, вызывает его код, написанный тем же программистом, или код фреймворка, написанного гигантской корпорацией: он все равно отдал контроль (в данном случае — за созданием зависимостей) за свои пределы.
>inversion of control (IoC) describes a design in which custom-written portions of a computer program receive the flow of control from a generic, reusable library
lmgtfy.com/?q=inversion+of+control
Используемое вами применение термина (я согласен, что оно исторически более верное) плохо применимо к IoC-контейнерам. Как пишет Фаулер, термин Dependency Injection в этой ситуации более точно отражает суть происходящего.

Но спасибо за исправление.
Ну это кто-то другой начал использовать IoC как синоним DI, а я же как раз за правильное использование. А если имелся в виду DI, то там все верно — от того, кто инжектит принцип DIP не зависит. И это не мешает юнит-тестированию.
Как-то так:

var dependency = Mock.Generate<IDependency>();
// установка ожиданий вызова/желаемого поведения, если необходимо
var component = new ComponentUnderTest(dependency);
Очень хорошо. А в продуктивном выполнении?
А в продуктивном выполнении?

Простите, не понял.
Когда вы запускаете этот же класс в реальном окружении, как вы инициализируете его зависимости?
Точно так же, только вместо мока — реальная имплементация. При этом код этот может находиться как внутри какой-то фабрики, так и посреди «обычного» кода.
А что вы имеете в виду под «посреди обычного кода»? Можете привести пример?
Я имел в виду, что этот код не находится в явно обособленном месте, типа ComponenFactory.CreateComponent().

А вы к чему клоните? Наверное уже можно и рассказать :).

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

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


Да, лишний уровень косвенности может мешать, но адекватные DI-контейнеры вполне позволяют понять, где и когда создается экземпляр, а, главное, это на самом деле далеко не такая важная информация, как вам кажется.
Ну тут дело даже не в адекватных контейнерах, а в адекватных разработчиках.
Например излюбленный многими Java разработчиками прием «Инъекция приватных полей», способствует увеличению хаоса. А потом люди рассказывают как контейнер многократно усложнил им жизнь…
В адекватных контейнерах тоже. Например, если у контейнера конфигурация не в коде, то ее анализировать сложнее.
ручное вбрасывание (известное как Pure DI)


Это называется «Poor man DI» — DI бедняка.
Ничего себе, даже Марк Симан не использует контейнеры. Наигрался :).
Это, скажем так, неоднозначное утверждение.
Sign up to leave a comment.