Просто для уточнения. А у Вас чем от "простого проксирования" отличается? Тем, что есть преобразование данных? Ну, я опять же в начале статьи указал, что оставим это за скобками, чтобы не скрыть суть. Но в некоторых примерах в комментах подписано, что собственно вот там и можно осуществлять преобразование, а где-то оно даже есть (из свойства с одним названием, в свойство с другим, но того же типа).
Я, в принципе, много про стирание типов разбирался прежде, чем это опубликовать. Кажется, это в чистом виде оно. Но поправьте меня, почему Вы считает, что это не оно.
Вам спасибо, за хороший вопрос и детальный пример. Это мне и нужно было, чтобы люди делились тем, как делают они. Хочется разобраться, какой же вариант самый популярный, кто пользуется непопулярными, в каким случаях, есть ли еще какие-то способы...
Вам тоже отвечу по частям, постараюсь максимально кратко.
О сложностях какого способа Вы говорите? Способ 1 очень даже простой. Там кроме протокола вообще больше ничего нет. На мой взгляд даже проще, чем тот, что описали Вы. Сложно судить по тексту без кода, что Вы описываете, но кажется, что Вы говорите как раз о том, что привел уважаемый @house2008. Если так, то это в чистом виде способ 5 – обертка, стирающая типы. В этом способе появляется 2 новых сущности (а не одна) – протокол и класс. А для класса еще и объекты надо создавать и потом управлять их жизненным циклом. В первом случае это не нужно совсем.
"Основной концепт подхода в том, что при изменении данных/стейта внутри ViewModel автоматически обновляется View." Не стану с этим спорить, но не зря я ссылку дал на Википедию же. Она утверждает, что это не главное и вообще опциональное. Это лишь третья ответственность из трех возможных, называемая биндинг. Автоматического обновления можно и без биндинга добиться, например, шаблоном "Наблюдатель"/"Observer". Или даже простым делегированием или замыканиями (в терминологии андроида, кажется, это колбеки), но я крайне не рекомендую делать ни первое, ни второе – других проблем огребете. А вот важна во вьюмодели как раз абстракция, и Вы её добиваетесь оберткой, стирающей типы. Сравните внимательно.
Я смог ответить на Ваши вопросы?
ПС: не стесняйтесь, отмечайте тогда вариант 5 в опросе. Это он!
В вашей реплике сразу несколько вопросов и неточностей. По очереди и отвечу.
Именно для того, чтоб не перегружать примеры, в начале статьи и делается сноска, что инжектирование специально остается за скобками. Иначе она спрячет от Вас суть. Но Вы можете самостоятельно убедиться, что ни один из способов не изменится, если добавить в него инжектирование. Они (в смысле инжектирование и способы) независимы друг от друга. Освоив любой из способов (или даже несколько, или даже все) Вы легко сможете использовать их с инжектированием. Отдельно в статье отмечено (ближе к концу), что ни с одним из перечисленных способов не должно быть проблем с тестированием именно потому, что сама реализация уже содержит абстракцию, а значит, может легко позволить подменить вью для модели или модель для вью.
Не во всех перечисленных примерах вьмодели скрыты протоколами. Абстракция – это не только протоколы. Статья пытается это показать. В частности, Ваш пример – это вариант 5, обертка, стирающая тип. Не самый простой вариант (есть проще), но самый популярный, как и отмечено в статье. И при этом мало известный :) Не удивительно, что он первый же в комментариях и проявился. Вам осталось проголосовать в опросе, не стесняйтесь, пожалуйста! Давайте создадим статистику!
Что касается "по самому паттерну верно делать интерфейсы для vm, но на практике это очень избыточно и даже вредно." Так статья именно это и пытается показать! Еще раз: далеко не во всех перечисленных примерах вьюмодель спрятана за протоколом. Более того, я тоже считаю, что иногда (если это не варианты 1, 2.1, где вьюмодель – это и есть протокол и ничего более, или, может быть, еще какие-то) это вредно! К тому же, в самом шаблоне нет ни слова про протокол! Там лишь написано, что вьюмодель должна быть абстракцией! А уж какой способ реализации абстракции Вы выберете - неважно, это все равно будет MVVM. Проблема будет только если а) Вы вообще забудете абстракцию; б) нагородите их слишком много.
А можете кратенько описать, что это и как? Я слегка погуглил. Получается, это возможно только в облаках сторонних провайдеров? Да и то, только потому, что они тестовые среды не предоставляют? Т.е. если я захочу такое у себя, то мне надо отобрать доступы у разработчиков и тестировщиков к тестовым средам? или как мне этого serverless добиться?
Тимофей, спасибо за хорошо проработанный материал. Возможно будет интересно. Вам или Вашим читателям. Кросс-ссылочка. Мы тоже пытались показать, как автотесты экономят именно время разработчика. Не так красиво получилось, как у Вас, зато есть конкретные цифры, не сильно отличающиеся от Ваших.
И снова соглашусь с первым комментарием. Захардкоженная навигация только у Вас. У меня вот проблем нет. И вся статья дальше тоже теряет актуальность в связи с этим.
Почему нет проблемы? Все просто. Перенесите первый же Ваш код сниппет (с экшеном кнопки) в расширение UINavigationController. Все. Ни один Ваш унаследованный контроллер не знает ни об этом экшене, ни о других контроллерах, ни об их взаимосвязи...
Попробуйте, это крайне просто. Сильно проще, чем координатор. И эстетичнее. Как команды в системном меню...
Сам тоже сейчас редактирую статью про "правильную готовку" MVVM. Думал уже не публиковать, но, видимо, теперь придется :)
В статье Вики про MVVM указано, что вьюмодель содержит всего 3 ответственности:
абстракцию для разделения вьюхи и модели (слабая связность).
преобразование данных из вида модели к виду отображения (и возможно, но необязательно, обратно).
биндинг - вариация абстрагирования и/или преобразования данных.
Вот, в принципе и все, что имеет право быть во вьюомдели. Все остальное, что вы перечислили, это уже либо модель, либо представление. Без вариантов! Например, навигация – ответственность представления. Если Вы меняете систему представления (например, с iOS переезжаете на мак, часы или телевизор), то в Вашем приложении не нужно бы менять ни вьюмодель, ни модель... а если Ваша модель зависит от роутера или координатора, то вам этого не избежать... а вот если роутер или координатор дергаются напрямую из вью, то остальные два слоя вашего приложения изменений не требуют – достаточно поменять только представление...
Unidirectional Data Flow – это пункт 2 из обязательных ответственностей. Преобразование данных. В одну и в другую сторону. Они в 142% случаев всегда независимы друг от друга, да. Поэтому реализовать их в двух разных классах не представляет сложности никогда. Но при этом оба класса все же остаются внутри слоя вьюмодели. Это слой. Он не обязан состоять из одного класса... В вашем примере Вы просто лишь одну часть этой ответственности осуществляете через биндинг. Но можно и обе, проблем не будет, это все еще будет UDF, если вью будет писать в одно свойство, а модель в другое (в одном или разных объектах – уже неважно)...
Но в принципе биндинг для преобразования данных не обязателен, как и для абстракции. Но с его помощью можно реализовать и то, и другое. А можно и не реализовывать. Биндинг для MVVM не обязателен, но тогда оно выродится в MVP или MVC с пассивной моделью.
В остальном, Вы молодец. Копайте глубже и проблем у Вас не будет ни с какой архитектурой. Еще и на лету их сможете менять, если ответственности представления (типа навигации) не будете опускать ниже.
ПС: неплохо Вы статьи генерите – по одной в день – ИИ помогает?
Соглашусь с первым комментарием по поводу состояния.
"Состояние"("State") – это прям отдельный шаблон из книжки Банды Четырех, чтобы с ним работа была комфортна, он должен реализовываться через набор классов с одним и тем же интерфейсом. Иначе, рано или поздно столкнетесь с проблемами. Например, enum'ы не расширяемые. Добавить состояние без нарушения OCP у вас не выйдет. В мелких проектах это и не важно, но в крупных...
Ну, и Вы тут много говорите о том, что не нужно смешивать ответственности, а "Состояние" (с переходами между ними, которые довольно часто не тривиальные! и не все вообще разрешены!) – вполне себе цельная отдельная ответственность...
Попробуйте её инкапсулировать по шаблону Банды Четырех. Не с первого раза, но получится всю прелесть прочувствовать...
Подскажите, пожалуйста, а зачем инъекции осуществлять через всю цепочку зависимостей? Очевидно же, что описанная проблема с "прокидыванием зависимостей" возникает исключительно из-за этого. Серьезно, ну зачем?
У Гугла, например, библиотеки инициируются как-то так:
let myLogger = MyLogger(bla-bla)
Firebase(logger: myLogger)
При таком подходе вы убираете проброс из всех промежуточных зависимостей и инжектируете конкретно туда, куда нужно. Реализация - простейшая, тестируется - элементарно.
Сначала сами себе создаем лютейшую связность (high coupling) там, где её никогда не было, а потом героически своих же действий последствия преодолеваем.
Если смущает, что в реализации будет Singletone, то вспомните про шаблон "Strategy" + "Фабричный метод" той же Банды Четырех, которые делегирует создание зависимости кому-то стороннему. Прямая цель первого шаблона, кстати, и есть подмена алгоритма без увеличения связности, а второго – как раз подмена создания нужного объекта без увеличения связности. Как-будто судя по цели - то, что Вам нужно. Дешево и сердито. Нет? Не подходит "Фабричный метод"? – возьмите любой другой пораждающий шаблон. 🤷
У Мартина вообще 3 уровня, и четвертого, как-будто, и не нужно никогда (если вспомнить о назначении каждого слоя). Как-будто логика приложения – это и есть то, как бизнес-модель проецируется в представление и обратно. Как-будто логика приложения – это содержимое уровня контроллеров.
Вообще, конечно, узнать бы у автора, что подразумевается под логикой приложения. @Ognev_Anton Определения в статье нет, термин используется только на картинке. Но можно предположить, что под этим уровнем имеется ввиду вот это из статьи: "Настройка окружения, заголовки, работа с запросами и сессией, кодирование и декодирование пишутся один раз и являются общими для всего приложения."
В таком случае, это действительно уровень адаптера и преобразования данных – только с другой стороны кольца – не со стороны представления, а со стороны сервера. По Мартину входов в эти кольца может быть сколько угодно.
Но так исторически сложилось, что в иос (да и андроиде, и вообще в мобилах) – общение с сервером воспринимается, как самая главная часть бизнес логики, на которую и идет все равнение (т.е.от нее зависит все). Сервер для мобильной разработки – главный закон, решения сервера не оспариваются, а выполняются... У нас в приложении так же. Поэтому это более глубокий уровень, что, похоже, и отображено на диаграмме.
Но вот вопрос: какой из подходов лучше и почему (традиционный используется уже лет 15)? Стоит ли нам инвертировать зависимости между бизнес-логикой и адаптером к серверу (то, что в статье названо логикой приложения)? От чего это зависит? Как это замерить?
Что думаете? Сможете нам помочь в ответах на эти важные для нас вопросы?
Зря я "мегатест" упомянул в вопросе. Давайте забудем про него.
Конкретные примеры:
Вот у вас последний тест - там в 1 тесте сразу 2 скриншота генерируется.
Пример с "all" - там в 1 тесте вообще 4 скриншота проверяется.
Если падает 1 тест, в котором сразу с десяток (или 2 или 4, неважно, главное – что не одна) картинок генерируется, насколько просто искать потом место, которое нужно править?
______________
Кстати, "не больше 130 картинок на компонент", а тестов при этом сколько на компонент?
Скриншот-тесты - это не юниты, хоть они и близки к юнитам. Для юнитов необходимое условие – это изоляция. Если для скриншотов по аналогии использовать изоляцию, то в 1 тесте должна быть 1 картинка. Но мне, например, понятно, что скриншоты – все же выше юнитов, и поэтому, возможно, это правило не должно выполняться прям полностью. Вы его не выполняете. Но мне хочется понять, какие это издержки привносит...
Пока вы отметили, что приходится копаться в именах, но это происходит нечасто... Может быть что-то еще?
Вы в одном тесте пишите сразу кучу модификаторов состояний. В итоге количество скринов возрастает экспоненциально. В примере у вас лишь удвоение дважды, но как в итоге после полугода использования? Наверняка ж есть и компоненты у которых и самих изменяющихся свойств сильно больше, чем 2 и состояний у каждого из них – тоже может быть сильно больше чем 2. Писать явно проще, но добавление 1 свойства и всех его значений может сильно замедлить тесты. Можете подсказать по практике, сколько у вас больших компонентов, сколько в них вариантов, сколько в итоге времени гоняются тесты?
И еще вопрос: насколько удобно держать столько вариантов в 1 тесте? в итоге получается по сути 1 мегатест на 1 компонент. Если что-то падает, то насколько легко разобраться, куда идти и что править?
простите, но Вы забыли постусловие с переполнением и тест на него. а еще тесты бы граничными условиями дополнить, чего не написано даже в ТЗ (а должно быть, как программа себя ведет в граничных случаях?). sum(-MAX_INT, -MAX_INT) - это переполнение или успех? а в остальном - все отлично!
Но все же пирамида тестирования утверждает, что тест должен быть реализован на самом нижнем уровне, на котором это возможно. (Иначе пирамиды не выйдет, очевидно же?)
Пример. Валидация введенной суммы перевода.
Можно проверять прям от UI через сервер и до появления красной картинки обратно на UI. Получится сложный, медленный, дорогой, хрупкий e2e-тест, который тестирует поведение.
Но самый ли нижний это из возможных уровней, где проводится валидация?
Нет, конечно!
Мы можем убрать UI и проверять, допустим, что вход в функцию валидации какого-то значения, возвращает или не возвращает ошибку (которая потом бы в UI превратился в красную картинку). Так мы опустили проверки на уровень ниже. Проверяется ли при этом нужное поведение? Да, если к нему добавить 2 интеграционных теста, которые проверят, что значение из UI корректно в функцию записывается, а её возвращаемое значение корректно в UI устанавливается.
Что мы получили? Интеграция проверена 1 раз. Валидация введенного знания - 1 раз. Многократного покрытия нет.
Что касается TDD, то, наверное, Вы правы, что сначала будет написан один высокоуровневый e2e тест. Но ничто не мешает Вам на стадии рефакторинга рефакторить не только код, но и тест (или мешает? вопрос автору статьи, кстати @iv660 подразумевает ли рефакторинг и рефакторинг ранее написанного теста? ), разбивая 1 большой тест на несколько меньших, опуская все возможные проверки на более низкие уровни. Так вы избежите многократного тестирования и получите надежные, дешевые, быстрые тесты, которые не боятся рефакторинга, ибо каждый ваш тест будет соответствовать тому уровню, на котором реализовано соответствующее поведение.
А может, лучше "суп отдельно, а мухи отдельно" - в смысле спецификация отдельно, а проверка ее отдельно? Потому что объективно нужны эти вещи в разные моменты времени: спецификация - в процессе создания программы, а тесты - уже на этапе контроля, когда программа создана.
2 момента:
Тесты нужны ДО создания программы и ВО ВРЕМЯ её создания тоже. Об этом вся статья про TDD, в комментах к которой мы общаемся (например, пп.1,6,7). Об этом же и Ваш пункт про СПЕЦИФИКАЦИЮ, который мы сейчас обсуждаем. СПЕЦИФИКАЦИЯ подразумевает, что она должна быть написана ДО. Значит, и тесты должны быть написаны ДО. Также Вы выше пишите, что тесты нужны только на стадии ФИНАЛЬНОГО контроля. Но вот теория менеджмента подразумевает, что контроль бывает не только финальный. Он еще бывает входящий (ДО начала выполнения задачи, т.е.разработки программы), промежуточный и переодический (последние 2 - ВО ВРЕМЯ разработки программы). Отсюда снова следует, что тесты должны быть созданы как минимум ДО начала работы программы, если мы их хотим использовать для входного контроля, или ВО ВРЕМЯ, если хотим использовать промежуточный и/или переодический. Автору в п.4 и отдельным пунктом можно добавить про типы контроля и как TDD помогает про них не забывать...
Конечно нужно разделять! Разделение мух от котлет выглядит так: фиксируем мы требования и поведение через написание теста, а контролируем потом через его исполнение. Вас не устраивает такое разделение?
Код по сравнению с текстом слишком многословен, то есть имеет больший объем, причем объем кода тестов (в строках и знаках) обычно в разы больше объема покрываемого ими кода приложения (правда сложность - цикломатическая и т.п. - кода тестов обычно меньше). В коде, опять же, надо уделять внимание соблюдению церимониалов используемых языков/библиотек/фреймворков (boilerplate),. Код, даже "самодокументируемый", труднее читать, чем текстовое описание, наконец.
Ниже в этой же ветке уже ответили, что это не всегда так. Я лишь добавлю, что перечисленные Вами "недостатки" давно придумано как нивелировать и устранять. Учитывая это я бы даже сказал, что запись спецификации в виде тестов ДОВОЛЬНО ЧАСТО получается даже дешевле и проще, чем обычным человеческим текстом. Отсюда мы возвращаемся к тезису про более дешевую сумму записи спецификации и её автоматизации и получаем, что автотесты становятся практически самым дешевым и эффективным вариантом, как бы со стороны не казалось иначе. Такой вот парадокс, да.
А она точно нужна, эта автоматизация?
Про это п.4. И мои комментарии выше. Я тоже LLM не использовал для генерации тестов, но прекрасно знаю некоторые своды правил, которые позволяют сделать так, что требования из ТЗ, записанные в виде кода теста, получаются проще, чем в тексте самого ТЗ. Возможно, Вы умеете писать ТЗ круче наших аналитиков (в этот вопрос я с вами в дискуссию вступать не стану).
А забыли про овраги: в процессе начальной разработки - всего приложения или какой-то отноительно самостоятельной его функции, проводимом без предварительного детального проектирования (то есть - в обычном практическом случае) меняться может многое, почти всё. В том числе и поведение, и разбиение его на единицы. И уж всяко - интерфейсы, через которые взаимодействуют модули. И чем менее тривильна разрабатываемая программа, тем чаще такие изменения происходят.
Ну, я же не зря Вам целую книгу порекомендовал, наверное? Вот Вы спорите даже не прочитав, похоже. Собственно, как и всю статью... че уж я про книгу-то сетую...
Пожалуй, на этом я остановлюсь, на дальнейшие Ваши комментарии отвечать не стану. Спасибо Вам за содержательную дискуссию.
Ну, Вы сами написали, что TDD можно писать с любыми тестами. Но пирамида тестирования утверждает, что модульных (юнитов) должно быть больше всего. Поэтому замените в названии статьи модульные тесты на TDD - логическая цепочка не нарушится, смысл не поменяется.
Просто для уточнения. А у Вас чем от "простого проксирования" отличается? Тем, что есть преобразование данных? Ну, я опять же в начале статьи указал, что оставим это за скобками, чтобы не скрыть суть. Но в некоторых примерах в комментах подписано, что собственно вот там и можно осуществлять преобразование, а где-то оно даже есть (из свойства с одним названием, в свойство с другим, но того же типа).
Я, в принципе, много про стирание типов разбирался прежде, чем это опубликовать. Кажется, это в чистом виде оно. Но поправьте меня, почему Вы считает, что это не оно.
Вам спасибо, за хороший вопрос и детальный пример. Это мне и нужно было, чтобы люди делились тем, как делают они. Хочется разобраться, какой же вариант самый популярный, кто пользуется непопулярными, в каким случаях, есть ли еще какие-то способы...
Вам тоже отвечу по частям, постараюсь максимально кратко.
О сложностях какого способа Вы говорите? Способ 1 очень даже простой. Там кроме протокола вообще больше ничего нет. На мой взгляд даже проще, чем тот, что описали Вы. Сложно судить по тексту без кода, что Вы описываете, но кажется, что Вы говорите как раз о том, что привел уважаемый @house2008. Если так, то это в чистом виде способ 5 – обертка, стирающая типы. В этом способе появляется 2 новых сущности (а не одна) – протокол и класс. А для класса еще и объекты надо создавать и потом управлять их жизненным циклом. В первом случае это не нужно совсем.
"Основной концепт подхода в том, что при изменении данных/стейта внутри ViewModel автоматически обновляется View." Не стану с этим спорить, но не зря я ссылку дал на Википедию же. Она утверждает, что это не главное и вообще опциональное. Это лишь третья ответственность из трех возможных, называемая биндинг. Автоматического обновления можно и без биндинга добиться, например, шаблоном "Наблюдатель"/"Observer". Или даже простым делегированием или замыканиями (в терминологии андроида, кажется, это колбеки), но я крайне не рекомендую делать ни первое, ни второе – других проблем огребете.
А вот важна во вьюмодели как раз абстракция, и Вы её добиваетесь оберткой, стирающей типы. Сравните внимательно.
Я смог ответить на Ваши вопросы?
ПС: не стесняйтесь, отмечайте тогда вариант 5 в опросе. Это он!
В вашей реплике сразу несколько вопросов и неточностей. По очереди и отвечу.
Именно для того, чтоб не перегружать примеры, в начале статьи и делается сноска, что инжектирование специально остается за скобками. Иначе она спрячет от Вас суть. Но Вы можете самостоятельно убедиться, что ни один из способов не изменится, если добавить в него инжектирование. Они (в смысле инжектирование и способы) независимы друг от друга. Освоив любой из способов (или даже несколько, или даже все) Вы легко сможете использовать их с инжектированием. Отдельно в статье отмечено (ближе к концу), что ни с одним из перечисленных способов не должно быть проблем с тестированием именно потому, что сама реализация уже содержит абстракцию, а значит, может легко позволить подменить вью для модели или модель для вью.
Не во всех перечисленных примерах вьмодели скрыты протоколами. Абстракция – это не только протоколы. Статья пытается это показать. В частности, Ваш пример – это вариант 5, обертка, стирающая тип. Не самый простой вариант (есть проще), но самый популярный, как и отмечено в статье. И при этом мало известный :) Не удивительно, что он первый же в комментариях и проявился. Вам осталось проголосовать в опросе, не стесняйтесь, пожалуйста! Давайте создадим статистику!
Что касается "по самому паттерну верно делать интерфейсы для vm, но на практике это очень избыточно и даже вредно." Так статья именно это и пытается показать! Еще раз: далеко не во всех перечисленных примерах вьюмодель спрятана за протоколом. Более того, я тоже считаю, что иногда (если это не варианты 1, 2.1, где вьюмодель – это и есть протокол и ничего более, или, может быть, еще какие-то) это вредно! К тому же, в самом шаблоне нет ни слова про протокол! Там лишь написано, что вьюмодель должна быть абстракцией! А уж какой способ реализации абстракции Вы выберете - неважно, это все равно будет MVVM. Проблема будет только если а) Вы вообще забудете абстракцию; б) нагородите их слишком много.
Я смог ответить на Ваши вопросы?
Звучит крайне интересно.
А можете кратенько описать, что это и как? Я слегка погуглил. Получается, это возможно только в облаках сторонних провайдеров? Да и то, только потому, что они тестовые среды не предоставляют? Т.е. если я захочу такое у себя, то мне надо отобрать доступы у разработчиков и тестировщиков к тестовым средам? или как мне этого serverless добиться?
Не по теме: воспитательную работу с товарищем, напрямую оперирующим слоями, провели? :)
Кросс-ссылка на схожий материал о пользе автотестов.
Тимофей, спасибо за хорошо проработанный материал.
Возможно будет интересно. Вам или Вашим читателям. Кросс-ссылочка. Мы тоже пытались показать, как автотесты экономят именно время разработчика. Не так красиво получилось, как у Вас, зато есть конкретные цифры, не сильно отличающиеся от Ваших.
И снова соглашусь с первым комментарием. Захардкоженная навигация только у Вас. У меня вот проблем нет. И вся статья дальше тоже теряет актуальность в связи с этим.
Почему нет проблемы? Все просто. Перенесите первый же Ваш код сниппет (с экшеном кнопки) в расширение UINavigationController. Все. Ни один Ваш унаследованный контроллер не знает ни об этом экшене, ни о других контроллерах, ни об их взаимосвязи...
Попробуйте, это крайне просто. Сильно проще, чем координатор. И эстетичнее. Как команды в системном меню...
Сам тоже сейчас редактирую статью про "правильную готовку" MVVM. Думал уже не публиковать, но, видимо, теперь придется :)
В статье Вики про MVVM указано, что вьюмодель содержит всего 3 ответственности:
абстракцию для разделения вьюхи и модели (слабая связность).
преобразование данных из вида модели к виду отображения (и возможно, но необязательно, обратно).
биндинг - вариация абстрагирования и/или преобразования данных.
Вот, в принципе и все, что имеет право быть во вьюомдели. Все остальное, что вы перечислили, это уже либо модель, либо представление. Без вариантов! Например, навигация – ответственность представления. Если Вы меняете систему представления (например, с iOS переезжаете на мак, часы или телевизор), то в Вашем приложении не нужно бы менять ни вьюмодель, ни модель... а если Ваша модель зависит от роутера или координатора, то вам этого не избежать... а вот если роутер или координатор дергаются напрямую из вью, то остальные два слоя вашего приложения изменений не требуют – достаточно поменять только представление...
Unidirectional Data Flow – это пункт 2 из обязательных ответственностей. Преобразование данных. В одну и в другую сторону. Они в 142% случаев всегда независимы друг от друга, да. Поэтому реализовать их в двух разных классах не представляет сложности никогда. Но при этом оба класса все же остаются внутри слоя вьюмодели. Это слой. Он не обязан состоять из одного класса... В вашем примере Вы просто лишь одну часть этой ответственности осуществляете через биндинг. Но можно и обе, проблем не будет, это все еще будет UDF, если вью будет писать в одно свойство, а модель в другое (в одном или разных объектах – уже неважно)...
Но в принципе биндинг для преобразования данных не обязателен, как и для абстракции. Но с его помощью можно реализовать и то, и другое. А можно и не реализовывать. Биндинг для MVVM не обязателен, но тогда оно выродится в MVP или MVC с пассивной моделью.
В остальном, Вы молодец. Копайте глубже и проблем у Вас не будет ни с какой архитектурой. Еще и на лету их сможете менять, если ответственности представления (типа навигации) не будете опускать ниже.
ПС: неплохо Вы статьи генерите – по одной в день – ИИ помогает?
Соглашусь с первым комментарием по поводу состояния.
"Состояние"("State") – это прям отдельный шаблон из книжки Банды Четырех, чтобы с ним работа была комфортна, он должен реализовываться через набор классов с одним и тем же интерфейсом. Иначе, рано или поздно столкнетесь с проблемами. Например, enum'ы не расширяемые. Добавить состояние без нарушения OCP у вас не выйдет. В мелких проектах это и не важно, но в крупных...
Ну, и Вы тут много говорите о том, что не нужно смешивать ответственности, а "Состояние" (с переходами между ними, которые довольно часто не тривиальные! и не все вообще разрешены!) – вполне себе цельная отдельная ответственность...
Попробуйте её инкапсулировать по шаблону Банды Четырех. Не с первого раза, но получится всю прелесть прочувствовать...
Даже художники и писатели "струячат" свои шедевры, как на конвейере, чтобы выжить. Стиг Ларсон, Ю Несбё и прочие...
Есть ли где-то не конвейер в этой жизни? :) наверное, только у бомжа в зимнем московском трамвае - вот где истинная свобода...
Или достаточно просто написать несколько интеграционных тестов. 🤷
Подскажите, пожалуйста, а зачем инъекции осуществлять через всю цепочку зависимостей? Очевидно же, что описанная проблема с "прокидыванием зависимостей" возникает исключительно из-за этого. Серьезно, ну зачем?
У Гугла, например, библиотеки инициируются как-то так:
При таком подходе вы убираете проброс из всех промежуточных зависимостей и инжектируете конкретно туда, куда нужно. Реализация - простейшая, тестируется - элементарно.
Сначала сами себе создаем лютейшую связность (high coupling) там, где её никогда не было, а потом героически своих же действий последствия преодолеваем.
Если смущает, что в реализации будет Singletone, то вспомните про шаблон "Strategy" + "Фабричный метод" той же Банды Четырех, которые делегирует создание зависимости кому-то стороннему. Прямая цель первого шаблона, кстати, и есть подмена алгоритма без увеличения связности, а второго – как раз подмена создания нужного объекта без увеличения связности. Как-будто судя по цели - то, что Вам нужно. Дешево и сердито. Нет? Не подходит "Фабричный метод"? – возьмите любой другой пораждающий шаблон. 🤷
У Мартина вообще 3 уровня, и четвертого, как-будто, и не нужно никогда (если вспомнить о назначении каждого слоя). Как-будто логика приложения – это и есть то, как бизнес-модель проецируется в представление и обратно. Как-будто логика приложения – это содержимое уровня контроллеров.
Вообще, конечно, узнать бы у автора, что подразумевается под логикой приложения. @Ognev_Anton Определения в статье нет, термин используется только на картинке. Но можно предположить, что под этим уровнем имеется ввиду вот это из статьи: "Настройка окружения, заголовки, работа с запросами и сессией, кодирование и декодирование пишутся один раз и являются общими для всего приложения."
В таком случае, это действительно уровень адаптера и преобразования данных – только с другой стороны кольца – не со стороны представления, а со стороны сервера. По Мартину входов в эти кольца может быть сколько угодно.
Но так исторически сложилось, что в иос (да и андроиде, и вообще в мобилах) – общение с сервером воспринимается, как самая главная часть бизнес логики, на которую и идет все равнение (т.е.от нее зависит все). Сервер для мобильной разработки – главный закон, решения сервера не оспариваются, а выполняются... У нас в приложении так же. Поэтому это более глубокий уровень, что, похоже, и отображено на диаграмме.
Но вот вопрос: какой из подходов лучше и почему (традиционный используется уже лет 15)? Стоит ли нам инвертировать зависимости между бизнес-логикой и адаптером к серверу (то, что в статье названо логикой приложения)? От чего это зависит? Как это замерить?
Что думаете? Сможете нам помочь в ответах на эти важные для нас вопросы?
Спасибо за подмеченный нюанс!
хорошо понятно.
Зря я "мегатест" упомянул в вопросе. Давайте забудем про него.
Конкретные примеры:
Вот у вас последний тест - там в 1 тесте сразу 2 скриншота генерируется.
Пример с "all" - там в 1 тесте вообще 4 скриншота проверяется.
Если падает 1 тест, в котором сразу с десяток (или 2 или 4, неважно, главное – что не одна) картинок генерируется, насколько просто искать потом место, которое нужно править?
______________
Кстати, "не больше 130 картинок на компонент", а тестов при этом сколько на компонент?
Скриншот-тесты - это не юниты, хоть они и близки к юнитам. Для юнитов необходимое условие – это изоляция. Если для скриншотов по аналогии использовать изоляцию, то в 1 тесте должна быть 1 картинка. Но мне, например, понятно, что скриншоты – все же выше юнитов, и поэтому, возможно, это правило не должно выполняться прям полностью. Вы его не выполняете. Но мне хочется понять, какие это издержки привносит...
Пока вы отметили, что приходится копаться в именах, но это происходит нечасто... Может быть что-то еще?
Вы в одном тесте пишите сразу кучу модификаторов состояний. В итоге количество скринов возрастает экспоненциально. В примере у вас лишь удвоение дважды, но как в итоге после полугода использования? Наверняка ж есть и компоненты у которых и самих изменяющихся свойств сильно больше, чем 2 и состояний у каждого из них – тоже может быть сильно больше чем 2. Писать явно проще, но добавление 1 свойства и всех его значений может сильно замедлить тесты. Можете подсказать по практике, сколько у вас больших компонентов, сколько в них вариантов, сколько в итоге времени гоняются тесты?
И еще вопрос: насколько удобно держать столько вариантов в 1 тесте? в итоге получается по сути 1 мегатест на 1 компонент. Если что-то падает, то насколько легко разобраться, куда идти и что править?
простите, но Вы забыли постусловие с переполнением и тест на него.
а еще тесты бы граничными условиями дополнить, чего не написано даже в ТЗ (а должно быть, как программа себя ведет в граничных случаях?).
sum(-MAX_INT, -MAX_INT)- это переполнение или успех?а в остальном - все отлично!
Вы правы, подходов много.
Но все же пирамида тестирования утверждает, что тест должен быть реализован на самом нижнем уровне, на котором это возможно. (Иначе пирамиды не выйдет, очевидно же?)
Пример. Валидация введенной суммы перевода.
Можно проверять прям от UI через сервер и до появления красной картинки обратно на UI. Получится сложный, медленный, дорогой, хрупкий e2e-тест, который тестирует поведение.
Но самый ли нижний это из возможных уровней, где проводится валидация?
Нет, конечно!
Мы можем убрать UI и проверять, допустим, что вход в функцию валидации какого-то значения, возвращает или не возвращает ошибку (которая потом бы в UI превратился в красную картинку). Так мы опустили проверки на уровень ниже. Проверяется ли при этом нужное поведение? Да, если к нему добавить 2 интеграционных теста, которые проверят, что значение из UI корректно в функцию записывается, а её возвращаемое значение корректно в UI устанавливается.
Что мы получили? Интеграция проверена 1 раз. Валидация введенного знания - 1 раз. Многократного покрытия нет.
Что касается TDD, то, наверное, Вы правы, что сначала будет написан один высокоуровневый e2e тест. Но ничто не мешает Вам на стадии рефакторинга рефакторить не только код, но и тест (или мешает? вопрос автору статьи, кстати @iv660 подразумевает ли рефакторинг и рефакторинг ранее написанного теста? ), разбивая 1 большой тест на несколько меньших, опуская все возможные проверки на более низкие уровни. Так вы избежите многократного тестирования и получите надежные, дешевые, быстрые тесты, которые не боятся рефакторинга, ибо каждый ваш тест будет соответствовать тому уровню, на котором реализовано соответствующее поведение.
У нас вроде работает \Разведенные в сторону руки/
2 момента:
Тесты нужны ДО создания программы и ВО ВРЕМЯ её создания тоже. Об этом вся статья про TDD, в комментах к которой мы общаемся (например, пп.1,6,7). Об этом же и Ваш пункт про СПЕЦИФИКАЦИЮ, который мы сейчас обсуждаем. СПЕЦИФИКАЦИЯ подразумевает, что она должна быть написана ДО. Значит, и тесты должны быть написаны ДО.
Также Вы выше пишите, что тесты нужны только на стадии ФИНАЛЬНОГО контроля. Но вот теория менеджмента подразумевает, что контроль бывает не только финальный. Он еще бывает входящий (ДО начала выполнения задачи, т.е.разработки программы), промежуточный и переодический (последние 2 - ВО ВРЕМЯ разработки программы).
Отсюда снова следует, что тесты должны быть созданы как минимум ДО начала работы программы, если мы их хотим использовать для входного контроля, или ВО ВРЕМЯ, если хотим использовать промежуточный и/или переодический.
Автору в п.4 и отдельным пунктом можно добавить про типы контроля и как TDD помогает про них не забывать...
Конечно нужно разделять!
Разделение мух от котлет выглядит так: фиксируем мы требования и поведение через написание теста, а контролируем потом через его исполнение. Вас не устраивает такое разделение?
Ниже в этой же ветке уже ответили, что это не всегда так. Я лишь добавлю, что перечисленные Вами "недостатки" давно придумано как нивелировать и устранять. Учитывая это я бы даже сказал, что запись спецификации в виде тестов ДОВОЛЬНО ЧАСТО получается даже дешевле и проще, чем обычным человеческим текстом.
Отсюда мы возвращаемся к тезису про более дешевую сумму записи спецификации и её автоматизации и получаем, что автотесты становятся практически самым дешевым и эффективным вариантом, как бы со стороны не казалось иначе. Такой вот парадокс, да.
Про это п.4. И мои комментарии выше. Я тоже LLM не использовал для генерации тестов, но прекрасно знаю некоторые своды правил, которые позволяют сделать так, что требования из ТЗ, записанные в виде кода теста, получаются проще, чем в тексте самого ТЗ. Возможно, Вы умеете писать ТЗ круче наших аналитиков (в этот вопрос я с вами в дискуссию вступать не стану).
Ну, я же не зря Вам целую книгу порекомендовал, наверное? Вот Вы спорите даже не прочитав, похоже. Собственно, как и всю статью... че уж я про книгу-то сетую...
Пожалуй, на этом я остановлюсь, на дальнейшие Ваши комментарии отвечать не стану. Спасибо Вам за содержательную дискуссию.
Ну, Вы сами написали, что TDD можно писать с любыми тестами. Но пирамида тестирования утверждает, что модульных (юнитов) должно быть больше всего. Поэтому замените в названии статьи модульные тесты на TDD - логическая цепочка не нарушится, смысл не поменяется.