Pull to refresh

Comments 13

Я правильно понимаю, что можно напрямую обращаться к автору статьи? :)

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

TDD/BDD «с моками» нужно для того, чтобы одной методологией (TDD) покрыть все стадии разработки: анализ требований – дизайн – реализация, без единого разрыва :) на up-front проектирование. Как это сделать без моков — я вариантов не встречал (если знаете, поделитесь, кстати).

По сути, вы пользуетесь результатами этого процесса, когда предлагаете свой дизайн:
Оно (приложение) принимает события от сервера и реагирует на них некоторыми командами, поддерживая внутреннюю state машину ≤…> И это по сути всё.
Как бы вы смогли сделать так обобщенное утверждение, не исследуя отдельные частные случаи (что сделали авторы книги в тестах с моками)? Если бы эти частные случаи не были бы разобраны достаточно подробно (в тестах с моками), как бы вы могли быть уверены, что ваше архитектурное решение правильное? Ну, и т.д. То есть, с одной стороны имеем открыто показанное (почти) во всех подробностях решение, иллюстрирующее общий метод, а с другой — из шляпы фокусника извлеченное лучшее (это, надеюсь, мы еще обсудим) решение. Собственно, вопрос: вы предлагаете какую-то свою методологию построения систем? Если да, в чем ее отличие от TDD/BDD, которые по сути следуют по «классическому» пути ООП (найди функциональность — найди (или создай) объект, в который эту функциональность надо поместить) на уровне анализа отдельных (а не обобщенных) требований.
Вопрос по больше части сводится к top-down vs bottom-up подходу к разработке. Mockist подход действительно помогает при top-down, т.к. позволяет «мочить» несущественные детали. Я бы не сказал, что один из подходов позволяет решать задачи проектирования лучше другого. Лично я больше тяготею к классическому bottom-up, но также понимаю людей, которые предпочитают top-down.

При разработке top-down без моков действительно никак. Но при этом эти моки не обязательно оставлять после того как реализация готова. Такие тесты можно отрефакторить и заменить тестами без моков, что я собственно и сделал в статье.
По первому абзацу можно понять, что вы противопоставляете эти два подхода. «Полноценный» TDD позволяет сочетать (как всегда и было по классике, кстати): сверху-вниз «проектируем» систему (в данном случае с помощью моков выделяя и распределяя ответственность), затем снизу-вверх реализуем (имея разбивку на объекты и структуру каждого объекта, прорабатываем необходимые детали реализации с помощью «классического» TDD). Разве нет?

Второй абзац окончательно запутывает: так, вы не против разработки с моками? Вы просто призываете их удалить после того, как «реализация готова»?
Классика — это всегда bottom-up, Кент Бек и ко не писали про моки в оригинале, лондонская школа выработалась позже. Сочетать то, что описано в книге с классикой кстати возможно, но не так как вы описали (и это соответственно не будет полноценным top-down). Можно начать с набросков доменной модели (без тестов), затем после того как структура более-менее понятна — написать первый end-to-end тест и прокладывать себе путь к его исполнению путем классического bottom-up. Получится эдакий двух-уровневый TDD (как описано в книге), но без моков и без преждевременного распределения ответственностей. Проблема в top-down подходе в том, что если вы неверно выделелили эти ответственности, то отрефакторить их довольно сложно, т.к. из-за моков тесты становятся завязаны на детали имплементации.

вы не против разработки с моками? Вы просто призываете их удалить после того, как «реализация готова»?
Это я к тому, что если вы большой приверженец подхода сверзу-вниз, то это тоже не повод оставлять моки в конечной имплементации.
Классика — это всегда bottom-up, Кент Бек и ко не писали про моки в оригинале, лондонская школа выработалась позже.
А, вы о классике TDD? Я не сразу понял. Я-то о классике программирования :) — в книжках еще про структурных подход можно почитать.
Сочетать то, что описано в книге с классикой кстати возможно, но не так как вы описали (и это соответственно не будет полноценным top-down).
Я бегло поискал по книге — они действительно об этом не пишут! То, как я это описал, работает — я так практикую уже несколько лет. Осталось выяснить, откуда я это взял :) Не сам же придумал! Был уверен, что вычитал данный подход именно в «Growing…»
Можно начать с набросков доменной модели (без тестов), затем после того как структура более-менее понятна — написать первый end-to-end тест и прокладывать себе путь к его исполнению путем классического bottom-up. Получится эдакий двух-уровневый TDD (как описано в книге), но без моков и без преждевременного распределения ответственностей. Проблема в top-down подходе в том, что если вы неверно выделелили эти ответственности, то отрефакторить их довольно сложно, т.к. из-за моков тесты становятся завязаны на детали имплементации.
На практике часто все происходит с точностью до наоборот. Если «набросать структуру», то очень легко ошибиться, и ошибка часто выявляется уже на поздних стадиях: когда снизу-вверх «поднялись» до верхних уровней и вдруг оказалось, что модуль A должен использовать объекты из модуля B, но ничего про модуль B не знает, а чтобы узнал — нужно изменить (часто просто поломать) всю сложившуюся структуру. Очень огорчает и приводит к большим временным затратам.

Когда же проектирование идет не в голове/доске/листочках бумаги, а в тестах (где конкретные требования фиксируются — специфицируются) такого практически не бывает. Распределение ответственности там происходит непосредственно по факту: данный объект должен реализовать такое-то требование? — для этого необходимо выполнить такую-то работу — кто ее может сделать? — если объекта нет, вводим мок и на следующем шаге разработки углубляемся в него; если объект уже есть — пишем классику и начинаем подъем вверх. Ошибки аналогичные описанной выше (не учли, что для выполнения некоторой работы нужен еще один объект) случаются здесь гораздо реже (потому что разбираем не абстрактно-общие понятия, а частные случаи и не в абстрактном пространстве, а в реальном коде), а когда случаются — такие ошибки выявляется достаточно быстро, часто уже на следующем уровне детализации, но в любом случае еще при движении вниз (при реализации замоченного объекта выясняется, что он не в состоянии реализовать предъявляемые требования) — когда время на детальную проработку структуры системы еще не потрачено и менять ее относительно легко.

Тесты с моками далеко не всегда оказываются такими уж хрупкими — если их использовать как спецификацию выявленных требований, а не пытаться отразить все аспекты поведения проектируемой системы. Если не включать в них лишних деталей, то даже если при анализе ошибся (что не мудрено), то изменить тест для исправления оказывается не такой уж сложной/трудоемкой задачей. …Хотя это не всегда просто и (судя по всему) нужен опыт — шишок много уже набито, и они продолжают набиваться :) Средства разработки (фреймворки для mock-ования:) хоть и развиваются, но отстают существенно, по крайней мере, не помогая в той степени, в какой могли бы… а во многом и затрудняя, да…
Если «набросать структуру», то очень легко ошибиться
Стурктура как раз-таки должна быть легковесной (отсюда слово «набросать»), чтобы ее можно было легко менять. Итерировать дизайн нужно в любом случае, разница в том, есть ли у вас при этом тесты и если есть — насколько хрупкие они.
Тесты с моками далеко не всегда оказываются такими уж хрупкими
Тесты с моками не хрупки только когда они заменяют собой external systems (bus, БД и т.д). Если они мочат внутренности доменной модели — они завязываются на детали имплементации и значит становятся хрупкими. Сторонники mockist подхода (как минимум те, кого я встречал, включая авторов GOOS книги) не делают такого разделения и как правило мочат всё подряд — и внешние системы и внутренности самой доменной модели.
Тесты с моками не хрупки только когда они заменяют собой external systems (bus, БД и т.д).
Это как раз тот случай, когда моки использовать не следует! Если, конечно, вы о моках говорите, а не о Test Doubles в общем виде.

Стурктура как раз-таки должна быть легковесной (отсюда слово «набросать»), чтобы ее можно было легко менять.
Да понятно, что все должны быть богатыми и здоровыми. Вопрос в том, как этого добиться. Как работает TDD с моками и без, почему это так, где тут плюсы, а где минусы и т.д. — более-менее понятно (если этим долго и упорно заниматься). А вот откуда возьмется легковесная и легко изменяемая на протяжении всей жизни программной системы структура — не ясно. Может быть, она должна присниться (как якобы было с таблицей и Менделеевым)? Или нужно «съесть зубы» на архитектурах программного обеспечения? Или изучить все существующие архитектурные/дизайнерские паттерны? Развить в себе интуицию до уровня вангования чтобы наперед знать все возможные последствия того или иного принятого решения? Нужно познать смысл жизни? :) Я пока не увидел ответа, который мог бы работать для любого/обычного человека. Или, может быть, наоборот: не надо уделять этому вопросу лишнего внимания? «набросал» что первое пришло в голову и отдал кодерами — пусть сами мучаются со своей кодобазой, если через полгода выясниться, что эта простенькая структура не отвечает необходимым требованиям?

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

Если же относиться к TDD как к Test-Drivent Development-у (я выделил ключевое слово), то в идеале тесты являются формализованными (точно в необходимой степени) и автоматически проверяемыми спецификациями на код. Их поломка должна показывать, что изменились наши требования — и это очень важный момент. Если же наши тесты ломаются по каким-то другим причинам, и нам это не помогает обнаружить проблему, а мешает развивать систему — значит мы не умеем правильно писать тесты.
найди функциональность — найди (или создай) объект, в который эту функциональность надо поместить

Идеологи ООП с вами не согласны. «Правильно» все таки наоборот.
По поводу того, что предложенное вами альтернативное решение проще. Диаграмма действительно выглядит проще. Но это не значит, что вся система проще исходной. Можно же сделать диаграмму и из одного элемента, спрятав внутри тонны километровых спагетти :) Собственно, основной «гипотезой» ООП относительно борьбы со сложностью является (слегка утрирую) утверждение «много простых объектов — лучше, чем мало сложных».

Действительно ли ваша система проще? Мне пока вы не смогли это продемонстрировать. Понимаю, что для точного ответа на вопрос что проще, надо смотреть и сравнивать код. Для меня, как «стороннего» читателя выделить на это время проблематично. Может быть, вы могли бы предъявить какие-то более веские обоснования, что ваше решение действительно лучше?

Другая сторона вопроса: за счет чего достигается простота? Используете ли вы аналогичные внешние библиотеки/фреймворки или же часть «черной работы» за вас уже сделано и за счет этого удается упростить ваш код?

И есть еще третья сторона: почему авторы ввели все эти (как следует из вашей критики — ненужные/лишние) объекты? К примеру, за счет чего их «цепочка» из четырех классов приводящих к AuctionEvent у вас превращается всего лишь в одно такое событие? Это моки заставили авторов наделать столько классов (что сомнительно) или (что более вероятно) они принимали во внимание какие-то аспекты, которые вы по каким-то причинам откинули?
Действительно ли ваша система проще? Мне пока вы не смогли это продемонстрировать.
Мне кажется, это довольно легко увидеть глядя на диаграммы + код. В альтернативной версии в два раза меньше классов и интерфейсов (при этом код внутри самих классов либо такой же по размеру, либо меньше), плюс он не имеет недостатков, описанных в статье, таких как наличие циклических зависимостей.

Фукнциональность проекта идентична оригиналу, как по части самого кода так и по части тестов его покрывающих, фреймворки (кроме UI) не используются.

Это моки заставили авторов наделать столько классов (что сомнительно) или (что более вероятно) они принимали во внимание какие-то аспекты, которые вы по каким-то причинам откинули?
Опять же, это легко проверить посмотрев на код.

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

Откуда у авторов книги взялись циклические зависимости?

Если разобрать предлагаемый ими подход, то он состоит из двух этапов: разработка (как это раньше называлось) сверху-вниз (outside-in, если не ошибаюсь, в их терминологии?) и снизу-вверх (inside-out). Моки нужны на первом — чтобы выделять коллабораторов и иметь возможность написать работающий тест. И при этом, по идее, должны возникать только односторонние зависимости: от разрабатываемой системы (System Under Test …но лучше все же называть ее System Under Development?) к сотрудникам (collaborators), которые в будущем должны будут обеспечить требующуюся ей функциональность.

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

Без ответа на этот вопрос — опять же — возникает ощущение фокусничества: возможно, компоненты действительно должны взаимодействовать «в обе стороны», а вы просто прячете это за возможностями какого-нибудь фреймворка.
Чем руководствовались авторы сказать трудно. В книге кстати заметно как они испытывают трудности с циклами, т.к. возникают проблемы при «собирании» всех взаимодействующих классов воедино в composition root.

возможно, компоненты действительно должны взаимодействовать «в обе стороны», а вы просто прячете это за возможностями какого-нибудь фреймворка
Как я уже упомянул, код проекта довольно несложен, никаких фреймворков за исключением UI не используется. Должны или нет взаимодействовать в обе стороны — на мой вгляд неверная постановка вопроса. Нужно смотреть на то, можно ли сделать так, чтобы они работали только в одну сторону. Если можно — значит так и нужно делать.
Sign up to leave a comment.

Articles