Комментарии 107
— Какова область применения методологии с точки зрения эффективности? (Есть большие сомнения на счет эффективности данной методологии в стартапах)
— Насколько качественно нужно писать тесты, используя данную методологию. Приведу простой пример: у нас есть функция, которая получает на входе имя и фамилию, а в результате выдает фамилию и имя одной строкой. Допустим эту функцию мы используем для генерации какого-то клиентского отчета. Вот мы написали тест, все отрабатывает на ура. Дальше мы выкатываем в прод все это дело и у нескольких людей возникает ошибка при сохранении отчета из-за превышения количества символов для поля(в бд). Отсюда вопрос, нужно ли было изначально писать тест для проверки количества символов в результате?
Unitесты и тдд экономят время на нескольких этапах:
1)на этапе разработки: сидеть в отладке получается гораздо быстрее чем раз за разом запускать систему и подводить к нужному состоянию.
2) на этапе украшательства: когда работающий лапшекод хочется немного подписать перед коммитом.
3) мягко подталкивают мамкиных архитекторов к более прагматичным решениям.
Так что да — это приносит пользу и в стартапах.
Важно только чтобы тот человек в команде кто исполняет роль Лида/навязывает всем свое мнение по любому решению — умел в solid in action, а не только на собеседованиях спрашивать. Без этого — это будет путь боли.
Про кейс с бд — это уже другой класс тестов и там вопрос уже сложнее: сильно зависит от того сколько надёжности вы готовы оплачивать и да — для стартапа действительно вопрос открытый.
Вопрос тестов на слишком длинные имена — это вопрос понимания: должен этот модуль о слишком длинных именах заботится… или это задача какого-то другого модуля?
Ответьте для себя на этот вопрос — и вы поймёте в тестах какого модуля этот вариант должен быть.
В стартапах TDD тоже отлично работает если понять принцип: тесты пишутся, в первую очередь, тогда, когда задавали себе ответ на вопрос «а что, если X» — и отвечали на него. Как-то. Неважно как. Важно что ответ не был однозначен. Если не задавали или ответ был очевиден — тесты можно писать по желанию.
Касательно же стартапов, а все зависит от качества команды. Если команда привыкла работать в методологии TDD, то нет никакой проблемы, по срокам сильно это не ударит. Другое дело что собрать такую команду в стартап достаточно затратно. Если же в команде только лид понимает что это, то применять в стартапе конечно нельзя.
Зачем писать никому ненужные тесты на CRUD?
Пишите ровно те тесты, от которых есть польза заинтересованным лицам.
Зачем всегда продумывать реализацию заранее?
Двигайтесь от простого к сложному, по мере погружения в детали.
Зачем вообще следовать каким-то правилам, если от соблюдения этих правил нет пользы? Все что не работает лично у вас и у команды, на свалку.
И не нужно продумывать мельчайшие подробности реализации, нужно продумывать контракты реализаций и проверять последние на соблюдение первых.
Тут уже страх не 3 дня дебажить, а 3 дня править тесты и контракты.
Чем модуль у вас от компонента отличается?
Тут я расписывал подробно: habr.com/ru/post/351430
Пропустил как-то. Мне кажется или классификация несколько оригинальна?
В моём мире "модуль" и "компонент" — это практически синонимы. Каждый модуль или компонент ответственен за одну единицу функциональности. Тестируются они, прежде всего, максимально независимо от остальных модулей или компонентов.
Классификация — результат гугления и общения с сертифицированными тестировщиками.
Модуль, изолированный от зависимостей, не реализует ту функциональность для которой предназначен. А вот вместе со всеми своими зависимостями он уже представляет из себя компонент.
Он реализует ровно ту, для которой предназначен. Если не предназначен он для работы с СУБД или http сервером, то он єто не реализует. Если же данных с них ему нужны, то он обращается к другими модулям, которые для этого предназначены. У одного модуля функциональность — показ данных из абстрактного источника пользователю, у другого — быть таким абстрактным источником. Причём первому модулю может передаваться по конфигу разные источники, а второй может служить источником для разных модулей.
В первом случае я как-то могу ещё представить компонентное (в вашей терминологии) тестирование, как тестирование презентационного модуля со всеми имеющимися модулями источниками данных (пускай и шарятся они по всему приложению), а второй как? Со всеми презентационными модулями, которые его используют?
Пишутся тесты на абстрактный источник данных. Все источники данных проверяются этими тестами. Далее, презентационные модули тестируются с любым удобным источником данных, так как источники данных эквивалентны друг другу для внешнего наблюдателя.
Так где границы компонентов, если конкретный источник данных шарится между многими презентационными, а презентационные "выбирают" каким конкретным источником данных пользоваться? Каждый конкретный источник данных, соблюдающий некий контракт и ничего не знающий о потребителях — это компонент? Презентационный модуль, знающий только о контракте зависимости, передаваемой извне — компонент? Или компонент — эта вся совокупность этих модулей и их зависимостей, которая очень легко может составлять всё приложение и даже больше (внешние сервисы).
Границы компонентов не в коде, а в функциональности. От того, что вы поменяли одну зависимость на другую эквивалентную функциональность не меняется.
И да, приложение, сервис, кластер — всё это тоже компоненты.
Так вот же функциональность: предоставлять данные и показывать данные. Где тут различие между модулем и компонентом?
"Предоставлять данные" и "Показывать предоставленные данные".
Так где в этом случае компонент, если у нас 10 источников данных, реализующих один контракт, и 10 потребителей эти данные потребляющие (для простоты — только их). 100 компонентов "каждый с каждым" или 2, зависящих от одного контракта?
Модуль — это просто кусок исходного кода, не более.
Функционально законченный кусок исходного кода, а компонент — модуль (то есть тоже кусок исходного кода), предназначенный для повторного использования и развёртывания, так?
Википедия:
Рефа́кторинг (англ. refactoring), или перепроектирование кода, переработка кода, равносильное преобразование алгоритмов — процесс изменения внутренней структуры программы, не затрагивающий её внешнего поведения и имеющий целью облегчить понимание её работы[1][2]. В основе рефакторинга лежит последовательность небольших эквивалентных (то есть сохраняющих поведение) преобразований.
Под рефакторингом Вы похоже подразумеваете частичную или полную переделку неудачной архитектуры модуля, с которым таки надо как-то взаимодействовать другим. И вот здесь тесты как раз помогут понять, насколько изменилось поведение в остальных и в чем именно. ИМХО даже метод, который якобы просто всегда возвращает константное значение (сферический случай в вакууме), нужно тестировать, так как она (константа) может быть кем-то изменена и притом повлиять на функционирование других модулей.
И это одно из преимуществ модульных тестов — мы меняем код, но его поведение (внешнее) не меняется, и с помощью тестов мы это проверяем, получая информацию о том, если сломали и что сломали в логике модуля.Это как раз если тесты модульные. А если их писали особо не думая, то в результате оно могут оказаться в виде: передать mock-объект и проверить, что два раза будет вызвана функции foo (с такими-то аргументами) и три раза функция bar (с другими аргументами).
Фактически код функции переписан в тест и размножен «для надёжности» с коэффициентом 10.
Разумеется в таком случае от тестов никакого проку, а любой рефакторинг требует в 10 раз больше времени на то, чтобы все 10 копий кода исправить.
Зато «покрытие тестами» 100%.
Ок, давайте читать Википедию вместе :-)
Рефа́кторинг (англ. refactoring), или перепроектирование кода, переработка кода, равносильное преобразование алгоритмов — процесс изменения внутренней структуры программы, не затрагивающий её внешнего поведения и имеющий целью облегчить понимание её работы[1][2]. В основе рефакторинга лежит последовательность небольших эквивалентных (то есть сохраняющих поведение) преобразований.
P.S. Кстати, почти не встречал чистого (сначала тесты, потом код) TDD по работе, обычно и код и тесты приходилось писать одновременно. Но они действительно неслабо помогают при изменениях, в т.ч. самых минимальных, когда кажется что ничего просто не может поломаться.
А страх появляется из-за чрезмерного покрытия.Слово «чрезмерное» тут лишнее.
Если вы пишите тесты для того, чтобы что-то там «покрыть» — то с огромной вероятностью вы породите тесты, которые просто дублируют код. Даже если вы сами удержитесь — джуниоры помогут.
Тесты должны дополнять (и отчасти — заменять) документацию! А сколько кода они при этом покроют — не так важно. Если документация будет хорошей — скорее всего будет и покрытие хорошее.
Недавно же я сам перешел с проекта на Kotlin на проект на Node.js, со значительно меньшей и более простой кодовой базой. И теперь сам испытываю на себе тот же стресс перед мерджем в мастер — а не сломался ли undefined? Работает null? Удаленное неиспользуемое свойство в объекте — вдруг где-то было использовано? Помог ли линтер, некоторые тесты, ревью? Вполне вероятно, что нет. Линтер проверяет всякую фигню, тесты часто могут быть в стиле ASSERT_TRUE(true), а человек на код-ревью — человек. Вот бы иметь такую прогу, которая могла бы проверять семантические ошибки, опечатки, и попытки распаковать undefined зараз. И потом еще подключить эту прогу к IDE, чтобы работала в режиме real-time, вот было бы круто!
А если всерьез, то вот мысль про качественный и быстрый код с TDD. Обдумывание будь тестов или типов перед написанием кода имеет общую природу — программист думает перед тем, как писать код :)
Я понимаю разницу между TDD и статической типизацией, и хочу обратить внимание на их схожие стороны. В конце-концов, при наиболее полной системе типов (в языке Idris или в Haskell на стероидах), любые (поправьте меня, пожалуйста, если ошибаюсь) юнит-тесты выражаются через систему типов (dependent types).
Существует еще контрактное программирование, которое как раз частично покрывает кейсы с проверкой бизнес-логики прямо в коде приложения и тестов придется писать еще меньше. Но, насколько я понимаю, проверки контрактов сродни ассертам и отключаются в продакшене, то есть, по сути, являются своеобразными юнит-тестами.
Так что никакая сильная статическая типизация и система типов не отменяют необходимость в юнит-тестировании (Хотя справедливо, что тестов скорее всего понадобится писать меньше, чем для динамически-типизированных языков).На самом деле тут не зря приводили в пример Idris и Haskell. В них можно выразить в типах как раз бизнес-логику. Правда не всегда это разумно.
И применение TDD вкупе со статической типизацией получается более эффективным, потому что на оставшиеся 20% багов бизнес-логики можно выделить уже 100% тестов (подходя к их написанию с бóльшей внимательностью и затрачивая то время, которое мы сэкономили на багах типов).
В условиях отсутствия статической типизации
На проектах на PHP, JS (node, vue, vanilla+JQuery
Упреждая вопрос: нет, я не проводил замеры, и да, 80/20 — не точные цифры, и критерии оценки тоже сугубо субъективные (я бы даже сказал, сенсорные).
Ну вот почти год работаю на проекте, где на бэке PHP, а на фронте TypeScript + vue.d.ts и не могу сказать, что на бэке ошибок значительно больше. Притом тайпхинтами почти не пользуемся, за актуальностью phpdoc слабо следим, из статанализа только кодстайл и php --lint и то совсем недавно. И бэк не просто REST CRUD, скорее совсем не REST и совсем не CRUD, а ближе к SOAP.
P.s. я работаю с JS на FrontEnd. В работе использую ReactTS, но все равно не понял что вы сказали, а мысль, как я понял, частично решается подключением Typescript и линтерами. Остальное свойственно и другим языкам.
Что значит сломался undefined?
(Runtime) Type error: foo is undefined.
Что значит распаковать undefined?
(Runtime) Reference error: cannot get property bar of undefined.
Что значит работает ли null?
Например, что строгая проверка на значение === null не обломается о значение === undefined.
частично решается подключением Typescript
Именно так! Причем, насколько мне известно, TS поддерживает strictly nullable types, что делает его (статическую) систему типов даже сильнее чем в C# и Java.
В конце-концов, при наиболее полной системе типов (в языке Idris или в Haskell на стероидах), любые (поправьте меня, пожалуйста, если ошибаюсь) юнит-тесты выражаются через систему типов (dependent types).Теоретически — да, можно всё перенсти в систему типов. Практически — это неудобно.
Но замечание по поводу того, что статическая типизация и тесты — это из одной оперы верное. Я не всегда пишу тесты и не всегда использую языки со статической типизацией. Но я (как, похоже, и вы) придерживаюсь мнения: вначале — вводим типы, потом — думаем о том, как бы «закрыть» пропущенные ограничения тестами.
Больше всего удивляют фанаты TDD, рекомендующие, при этом, динамически типизированные языки… вот что у них в голове творится — я даже представить себе не могу.
Толку от того, что вы на 100% уверены, что в функцию pow(a, b) передаются два числа, если внутри они перемножаются, а не возведение в степень происходит?Если у вас все функции имеют сложность как у функции
pow
— то вы и без тестов и типизации их без ошибок напишите (или, наоборот: если вы можете в pow
вместо возведения в степень написать умножение — то вам и TDD не поможет).Однако более сложные функции вызывает, знаете ли, другие функции. А те — третьи. И четвёртые. А ещё параметр можно в контейнер сложить, в JSON засунуть и распарсить (да-да, в каком-нибудь Go вы можете описать тип того, что вы ожидаете в JSON увидеть) — в общем между тем местом где вы породили строку вместо числа и тем местом, где у вас случилась ошибка может быть очень много посредников.
Главное: эти посредники могут быть написаны разными людьми, а то и разными организациями. Историю про Mars Climate Orbiter помните, да? Все компоненты были офигительно покрыты тестами: и в космическом аппарате и в наземной части. Вот только типы данных они использовали разные…
Вот с типизацией и тестами — ровно такая же история. Тесты — позволяют удостовериться что у вас всё правильно реализовано внутри модулей, а типы (при их грамотном использовании, разумеется) — гарантируют, что на стыке модулей тоже всё будет в порядке. То есть тесты и статическая типизация не перекрывают, а дополняют друг друга.
Однако для этого типы должны быть достаточно сильными. К сожалению в самых распространённых языках, который сразу вспоминают, когда кто-то вспоминает статическую типизацию — типы довольно-таки слабые. Отсюда родился такой костыль, как Венгерская нотация.
Современные компиляторы позволяют эту проблему обойти (см., например, std::chrono), а в TypeScript'е типы изначально довольно сильные, так что это уже сейчас — скорее вопрос привычки…
Я тоже считаю, что типы и тесты дополняют друг друга, немного перекрываясь. Но я не понимаю, почему именно сначала сильную статтипизацию, потом тесты. Переход на другой язык может быть очень дорогим (кроме случаев типа JS->TS), а постепенно с монолитной архитектурой перейти будет ещё труднее. Тестами же можно начать покрывать постепенно.
Переход на другой язык может быть очень дорогим (кроме случаев типа JS->TS), а постепенно с монолитной архитектурой перейти будет ещё труднее.И вот именно поэтому — сначала типы, потом тесты. Если у вас простенький скрипт строк на 100 — то пофиг, на чём этот скрипт.
Если же задачи, возлагаемые на скрипт, растут и возникает желание «покрыть его тестами» — то это значит, что у вас уже не скрипт, а что-то большее.
Обычно это происходит, когда пересечена граница 300-500 строк (по разному в зависимости от используемого языка).
Так вот: обычно компонент такого размера — ещё можно переписать на типизированный язык… И ровно этот и нужно сделать.
А тесты… Тесты можно и потом.
Есть существующая кодовая база, есть заточенный под язык и фреймворк процессы CI/CDЕсли вы уже развели кучу кода и даже дошли до фреймворков и CI/CD без использования типов — то момент перехода на язык с типизацией, скорее всего упущен.
И да — в этом случае вам придётся мириться с этим болотом.
При том, что никаких ошибок типов ещё не было, а баги на поведение уже есть.А вы хотя бы AFL пробовли на ваш код напустить? И как вы узнаёте какие из «багов на поведение» связаны с типами, какие нет? «А не сломался ли undefined? Работает null? Удаленное неиспользуемое свойство в объекте — вдруг где-то было использовано?» — это ведь всё «закрывается» типами… а подобные вопросы я наблюдал от людей — фанатов нетипизированных языков частенько.
Мне не придётся мириться с отсутствием статических типов. Я мирюсь с их наличием :) И да, практика использования показывает, что TS не защищает от ошибок типа Uncaught TypeError: Cannot read property 'entity' of undefined
(вот прямо сейчас скопировал из консоли после успешной рекомпиляции, и any практически не используется), а времени на описание типов и приведения к нужному тратится прилично.
Историю про Mars Climate Orbiter помните, да? Все компоненты были офигительно покрыты тестами: и в космическом аппарате и в наземной части. Вот только типы данных они использовали разные…
Я думаю, что типы данных скорее всего использовали одинаковые. Одна сторона отправляла что то вроде Decimal, другая принимала Decimal. Вот только смысл значений каждая сторона трактовала по разному.
Считать ли единицу измерения частью типа данных — отдельный, очень интересный вопрос.
Я тут ещё вспомнил, что проблема с тем зондом была не во внутреннем софте зонда, а в протоколе передачи. Если две системы общаются через (де)сериализацию, то строгость типизации при разработке каждой из них не очень поможет. Если приходит "4.806", то можно только предполагать единицу измерения. Можно, конечно, присылать и значение и какой-нибудь код единицы измерения, но это чрезмерная гибкость для подобной системы.
Если две системы общаются через (де)сериализацию, то строгость типизации при разработке каждой из них не очень поможет.Нужен общий IDL для двух систем. И да, там должны быть разные типы для дюймов и сантиметров.
Тут сразу видно и как типизация может помочь и как она не может помочь: если у вас есть типы «число» и «строка», то типизация не поможет, если есть тип «длина» (который можно получить из типа «число» указав единицу длины) — тогда поможет. В C++14 появились зачатки этого подхода, скажем 1s — это одна секунда.
К сожалению дюймов/сатиметров и килограммов — там пока нету…
Возникает ощущение, что тут куда больше кода, чем в этой строчке:
Там не просто больше кода, он ещё и куда более сложный для понимания.
Как методология TDD научила меня писать более качественный код?
Не научила. Вместо одной простой строчки, которой даже тестирование не требуется(clip.stopTime = video.currentTime
), он понаписал экшенов, редьюсеров, селекторов и прочей ахинеи, и накрутил сверху ещё столько же кода для тестирования. При этом самый шик в том, что в тесте он фактически скопипастил код из редьюсера ({ ...clipState, stopTime }
), чтобы этот самый редьюсер протестировать.
Я установил в программу точку останова для того чтобы разобраться в том, что происходит.
Почему он не сделал это сразу остаётся загадкой.
Смысл не в том, сколько времени занимает ввод этого кода. Смысл в том, сколько времени занимает отладка в том случае, если что-то идёт не так. Если код окажется неправильным, тест выдаст отличный отчёт об ошибке. Я сразу же буду знать о том, что проблема заключается не в обработчике события. Я буду знать о том, что она либо в setClipStopTime(), либо в clipReducer(), где реализовано изменение состояния. Благодаря тесту я знал бы о том, какие функции выполняет код, о том, что он выводит на самом деле, и о том, что от него ожидается. И, что более важно, те же самые знания будут и у моего коллеги, который, через полгода после того, как я написал код, будет внедрять в него новые возможности.
Буду честен — я прочитал все его статьи, и продолжаю почитывать как они выходят (это не так просто, так как он постоянно перепубликовывает старые). Ничего полезного за последний год там не было.
Я скопировал слово timeupdate из документации к API для того чтобы быть абсолютно уверенным в том, что, вводя его, не допустил ошибку.
Жду статью, где вы расскажите как открыли для себя языки со статической типизацией.
Писал по TDD, эта методология действительно позволяет писать код с минимумом ошибок. Но TDD не бесплатен, написание большого кол-ва тестов требует времени, еще больше времени требуется на обновление написанных тестов при крупных рефакторингах. TDD тесты, как правило, активно используют моки и поэтому хрупкие: чтобы проверить одну строчку кода, часто необходимо написать два мока.
Попытки писать так, чтобы в тестах моки не были нужны, усложняет код. В то же время, интеграционный TDD тест, это, как правило, хорошая идея.
еще больше времени требуется на обновление написанных тестов при крупных рефакторингах. TDD тесты, как правило, активно используют моки и поэтому хрупкие:
Массовое обновление тестов требуется при изменении API. При измененнии внутренней реализации (что происходит чаще всего) требуется точечное изменение тестов.
Активное юзание моков для тестов — это потенциальная проблема, которая рано или поздно выстрелит. Активно юзают моки либо из-за неправильной архитектуры (границы ответственности размыты), либо из-за ограничений конкретных инструментов (в С следовать классическому TDD сложнее чем в джаве).
В целом же, голова всему голова. Юзать надо все что в конкретном контексте полезно (интеграционные и прочие тесты).
что есть API? Если мы говорим про публичное API модуля, то его тесты, скорее, интеграционные.
Если мы говорим про API отдельных классов, то оно может меняться часто и быстро.
Массовое обновление тестов требуется при изменении API. При измененнии внутренней реализации (что происходит чаще всего) требуется точечное изменение тестов.
А почему нельзя написать новые тесты для нового API и реализовать этот новый API не трогая старый?
Например, потому что в некоторых проектах есть принцип: удалять неиспользуемый код. Плюс новый и старый API могут конфликтовать, а если старому делать префикс/суффикс legacy, то надо будет менять все тесты.
Например, потому что в некоторых проектах есть принцип: удалять неиспользуемый код.
Я всегда удаляю неиспользуемый код. И что? Удалили неиспользуемый код вместе с его тестами. Что тут изменять то точечно?
Плюс новый и старый API могут конфликтовать, а если старому делать префикс/суффикс legacy, то надо будет менять все тесты.
Ну понятное дело. Новый API отличается от старого, поэтому мы сначала пишем тесты для классов нового API, потом реализуем классы нового API. Зачем старый то код трогать? Зачем какие-то префиксы/суффиксы? Зачем при изменении внутренней реализации, изменять тесты? У нас же требования к поведению не изменились. Реализация только изменилась.
Точечно тесты изменяются при рефакторинге типа переименования или разделения. По сути поведение не изменилось, но тесты будут падать, а то и вообще даже запустить не получится, потому что сборка упадёт из-за изменившихся сигнатур.
А при новых требованиях к API, как вы будете делать, например, изменения класса User, чтобы и старые и новые тесты проходили на нём. Не, можно, конечно, иногда, но обычно это усложняет работу.
… но тесты будут падать, а то и вообще даже запустить не получится, потому что сборка упадёт...
Есть смысл вообще обсуждать этот случай? Или в вашем процессе допускается сломать тесты и оставить так?
А при новых требованиях к API...
Подождите. Мы обсуждали случай, когда старый API оставляется как есть, а новые требования реализуются в виде нового API.
И кстати при чем здесь класс User? Если судить по имени, то это чистой воды dto
. Какое поведение может быть инкапсулировано в dto
? Там в принципе не должно быть ничего, кроме свойств с методами геттерами и сеттерами.
Есть смысл вообще обсуждать этот случай? Или в вашем процессе допускается сломать тесты и оставить так?
Конечно нет. Но точечные изменения обычно в таких кейсах вносятся.
Подождите. Мы обсуждали случай, когда старый API оставляется как есть, а новые требования реализуются в виде нового API.
Нет :) Это вы предложили решить задачу сильного изменения API путём создания нового и оставления старого.
И кстати при чем здесь класс User? Если судить по имени, то это чистой воды dto. Какое поведение может быть инкапсулировано в dto? Там в принципе не должно быть ничего, кроме свойств с методами геттерами и сеттерами.
Если судить по имени, то обычно это Entity, со своим идентификатором, состоянием и поведением. Например, user.ban(), или user.changePassword(), или user.follow()
Нет :) Это вы предложили решить задачу сильного изменения API путём создания нового и оставления старого.
Хм… а как еще можно подойти к решению, если у меня в моменте скажем 12 тысяч коммерческих пользователей моего API? Предложите что-нибудь пожалуйста. Только так, чтобы моим 12 тысячам пользователей не пришлось тратить деньги на переделку их систем.
Если судить по имени, то обычно это Entity, со своим идентификатором, состоянием и поведением. Например, user.ban(), или user.changePassword(), или user.follow()
user.ban()
и user.changePassword()
разве не обычные сеттеры? А метод user.follow()
что делает?
[TestMethod]
public void follow_should_pass()
{
User user = new User("jack");
user.follow();
// Как проверим что вызов follow сработал ожидаемо?
Assert.AreEqual(???);
}
Хм… а как еще можно подойти к решению, если у меня в моменте скажем 12 тысяч коммерческих пользователей моего API?
Не у всех проектов так. Не всем требуется обратная совместимость с предыдущим API.
user.ban() и user.changePassword() разве не обычные сеттеры?
Может да, а может нет, например ban может добавлять username в список забаненных. Или вообще всё на Event Sourcing и любой мутатор лишь эммитирует событие по коду.
А метод user.follow() что делает?
Полная сигнатура follow(User anotherUser) — добавляет текущего юзера в списки followers другого, а его в список following текущего.
Не всем требуется обратная совместимость с предыдущим API.
Как так? У API вообще нет пользователей?
Все пользователи внутренние, клиенты обновляются одновременно с сервисом.
Все пользователи внутренние, клиенты обновляются одновременно с сервисом.
Хорошо. Давайте предполагать что у нас 12 тысяч внутренних клиентов, использующих старый API. С таким я тоже встречался в жизни. Вам не кажется что ситуация при этом не радикально улучшается? Вы готовы легким движением руки убить старый API, вынудив 12 тысяч клиентов перейти на новый одномоментно?
P.S. При этом я все равно не понимаю зачем мы лезем в код старого API, если предполагается что новый у нас радикально другой? В чем целесообразность такого решения?
Радикально улучшается, если версия клиентов под нашим контролем на 99,999%. Не суть веб-приложение у нас, или централизованная система принудительных обновлений.
API может и радикально другой, но вот входная точка у него та же самая. Просто по семантике, предметная область не изменилась, пользователь так и остался User, никаких UserNew, UserOld или UserV2 в предметной области нет.
Радикально улучшается, если версия клиентов под нашим контролем на 99,999%. Не суть веб-приложение у нас, или централизованная система принудительных обновлений.
Я понял ваш подход.
API может и радикально другой, но вот входная точка у него та же самая. Просто по семантике, предметная область не изменилась, пользователь так и остался User, никаких UserNew, UserOld или UserV2 в предметной области нет.
Таки я не понимаю. У нас API радикально поменялся? Имена и сигнатуры методов радикально другие? Мы же не предметную область обсуждаем. Понятное дело что если мы автоматизировали управление пользователями, то в предметной области у нас пользователи и будут.
Беспредметный разговор в общем. Без примеров кода — это пустая трата времени.
Ну представьте, что был классический HTTP REST API с ендпоинтом /user(/{id})
. Внутри системы представлен как анемичная модель (публичные свойства или тупые геттеры/сеттеры) User. Принято решение о создании более бизнес-ориентированного HTTP RESTish API, то есть не PATCH /user/1 {isBanned: true}
, а POST /user/1/ban
(без тела), и соответствующей поддержки его на уровне модели, превращении DTO в полноценную Entity. Класс User уже есть, ендпоинт /user(/{id})
уже есть, делать никому не нужное версионирование и ендпоинта и модели предлагаете? Да даже если версионирование API нужно, то предалагете создать рядом с User, который DTO, User'a который Entity и маппить их на однцу и туже таблицу? Или просто добавить методов, оставив публичные свойства или тупые геттеры/сеттеры, через которые кто-то рано или поздно получит "несанкционированный доступ" к кишкам сущности, обойдя заложенные в её нормальных методы гарантии инвариантов?
Класс User уже есть, ендпоинт /user(/{id}) уже есть, делать никому не нужное версионирование и ендпоинта и модели предлагаете?Почему «никому не нужное»? Вам же нужное. Чтобы хоть куда-то сдвинуться.
Ибо у нас один такой же быстрый, как вы, попробовал один из «старых» классов, сидящих в «неправильном» неймспейсе передвинуть.
Ну, в общем, ему объяснили, что CL, меняющий одномоментно 284 тысячи файлов он не зальёт никогда. Даже если получит на это разрешение. Потому как к моменту, когда он отсылает этот CL на ревью кто-то где-то как-то уже успевает поиспользовать его тип «неправильным» (и неучтённым им) образом.
А на тему перехода — это штука творческая, может быть много разных подходов… одномоментное переключение — тоже хорошо работает… до определённых масштабов.
Тут как раз проблема в том, что в правильном неймспейсе класс сидит и называется правильно, только сигнатуры его методов должен быть изменены глобально.
Максимум, что я видел — где-то чуть больше 10 тысяч файлов одномоментно. И то — для этого пришлось долго готовиться и время выбирать.
Может да, а может нет, например ban может добавлять username в список забаненных. Или вообще всё на Event Sourcing и любой мутатор лишь эммитирует событие по коду.
Это что ж? Пользователь получается должен знать о существовании списка забаненых пользователей?
[TestMethod]
public void should_ban()
{
HashSet<User> banned = new HashSet<User>();
User user = new User(banned);
user.ban();
Assert.IsTrue(banned.Contains(user));
}
Так что ли?
типа того, хорошо что DI есть, а не синглтон :) Не придирайтесь к примерам из головы.
Не придирайтесь к примерам из головы.
Ну давайте пример не из головы. Только чтобы это был кусок кода. Потому что обсуждать программирование умозрительно, жонглируя многозначительными терминами, бесполезная трата времени.
хорошо что DI есть, а не синглтон :)
То есть в остальном, вы считаете что такого рода поведение удобно размещать в классе User
?
[TestMethod]
public void should_add_Kent_to_followers()
{
FollowersUsers followers = new FollowersUsersImpl();
BannedUsers banned = new BannedUsersImpl();
User kent = new User(followers, banned);
kent.Name = "Kent";
User bob = new User();
bob.Name = "Bob";
kent.follow(bob);
Assert.IsTrue(followers.Contains(bob));
}
final class User
{
private UuidInterface $id;
private string $name;
/** @var Collection|User[] */
private Collection $followers;
/** @var Collection|User[] */
private Collection $following;
public function __construct(UuidInterface $id, string $name)
{
$this->id = $id;
$this->name = $name;
$this->followers = new Collection();
$this->following = new Collection();
}
public function follow(User $that): void
{
if ($this === $that) {
throw new \InvalidArgumentException("User can't follow himself/herself");
}
$this->following->push($that);
$that->followers->push($this);
}
public function id(): UuidInterface
{
return $this->id;
}
public function name(): string
{
return $this->name;
}
/**
* @return Collection|User[]
*/
public function followers()
{
return $this->followers;
}
/**
* @return Collection|User[]
*/
public function following(): Collection
{
return $this->following;
}
}
final class UserTest extends TestCase {
public function testSuccessFollow(): void
{
$user = new User(Uuid::uuid4(), 'Вася');
$anotherUser = new User(Uuid::uuid4(),'Петя');
$user->follow($anotherUser);
$this->assertContains($user->following(), $anotherUser);
$this->assertContains($user->following(), $anotherUser);
$this->assertNotContains($user->followers(), $anotherUser);
$this->assertNotContains($anotherUser->following(), $user);
}
}
То есть когда пользователь заведет себе банковский счет и начнет делать платежи, у вас в классе User
появится коллекция BankAccounts
и коллекция Payments
?
Скорее да, чем нет. Может ограничусь BankAccountIds. Может только в BankAccount будет свойство User $owner
, причём не факт, что тот же класс реально будет, может только строковое представление UserId будет совпадать, а может и оно не будет, а где-то будет карта соответствий пользователей и счетов, и не факт что 1:1, может 0..1:0..1, а может N:N. Всё от задачи зависит, от границ разделения ответственностей. А скорее всего эти варианты будут шагами развития системы.
например, вместо
public void transfer(Producer p, Publisher c) {
c.publish(p.produce());
}
Приходится писать:
public void transfer(Producer p, Publisher c) {
doTransfer(c::publsh, p::produce)
}
@VisibleForTesting
void doTransfer(Supplier<Data> p, Consumer<Data> c) {
c.accept(p.get())
}
Пользуетесь ли вы методикой TDD при работе над своими проектами?Опрос-то где?
Уважаемые читатели! Пользуетесь ли вы методикой TDD при работе над своими проектами?
Я всегда сначала пишу тесты, потом пишу код. Только так и делаю. Ни разу еще я не пожалел, о том что выработал навык разработки через тестирование.
Я перестал запускать отладчики. Мой код не боится изменения требований. Мне не нужно писать документацию к коду, мне достаточно открыть файл с тестами и я понимаю как я предполагал пользоваться реализованным API-ем, своим или чужим.
Если в моем коде обнаруживается ошибка, мне особо не о чем беспокоиться, это просто означает что я не описал какой-то из тестовых сценариев и мне достаточно просто добавить его и исправить код.
TDD: методология разработки, которая изменила мою жизнь