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

Core dump

Деконструкция TDD

23.09.2020 20:15:33 | Автор: admin

Здравствуйте, меня зовут Дмитрий Карловский. А вы на канале "Core Dump", где мы берём разные темы из компьютерной науки и деконструируем их по полочкам. Начнём мы с разработки через тестирование.


Test Driven Development

Суть этого подхода заключается в ритуализации процесса разработки. То есть в некритическом безусловном выполнении определённых простых действий.


Этот ритуал сделает ваш код красивым и надёжным. Поддерживать его будет легко и просто. А разработка будет простой и быстрой. Так во всяком случае настоятельно убеждают нас проповедники TDD.


Видео запись этого разбора.


Суть TDD


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


Pure TDD


И тут сразу возникает вопрос вопрос на миллион...


Что делать, когда тест изначально зелёный?


Варианты ответов...


  • Сломать код
  • Удалить тест
  • Это невозможно

Если сломать код, то тесты естественным образом покраснеют. А после того как мы откатим изменение, тесты снова станут зелёными.


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


Наконец, моё любимое: по TDD такого быть не должно. Где-то ты накосячил, что у тебя так получилось. Покайся, грешник.


А теперь, внимание, правильный ответ: все эти варианты это полная беспросветная чушь. Хотя именно один из них, как правило, слышишь от адептов TDD.


Парадокс воронов


Говоря про ломание кода, нельзя не упомянуть про парадокс воронов. Суть его в том, что задаётся вопрос: "Все ли вороны чёрные?". И для ответа на него берутся нечёрные предметы. Например красные яблоки. И каждое такое яблоко как бы подтверждает тезис о том, что "все вороны чёрные", ведь яблоки не чёрные и при этом не вороны. Что-то тут не так с логикой, не правда ли?



Так же как в парадоксе воронов, адепт TDD нередко думает, что падение теста на явно некорректном коде может хоть что-то сказать о том, будет ли тест падать на коде, который похож на корректный. Однако, тут нет никакой связи. Это просто два разных кода.


Поломка и починка кода это не более чем бессмысленное действие, необходимое только для того, чтобы формально пройти 2 обязательных шага ритуального цикла. Но насколько вероятна эта зелёная ситуация?


Изначально зелёные тесты неизбежны


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


  1. R G
  2. R G
  3. R G
  4. G ?
  5. G ?
  6. G ?
  7. G ?
  8. G ?

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


То есть, для обеспечения качества мы вынуждены явно нарушать основную идею TDD: сначала тест, потом код.


Правильный TDD


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


Fixed TDD


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


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


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


В такой форме TDD уже можно применять с пользой. Однако...


TDD приводит к куче лишней работы


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


Давайте рассмотрим типичный сценарий написания простой функции...


Итерация В начале В процесссе В результате
1 R R G
2 GR RR GG
3 GGR RRR GGG
4 GGGR GGRR GGGG
5 GGGGR GGGGR GGGGG
6 GGGGGR RRRRRR GGGGGG

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


Но всё не так плохо, с какого-то момента мы перестанем выбрасывать код полностью, и добавление нового теста уже не будет приводить нас к существенному переписыванию кода. То есть цикл TDD наконец-то начнёт работать как задумывался.


Однако, всё ещё остаётся довольно высокая вероятность в любой момент столкнуться с такой ситуацией, что очередное изменение кода сломает вообще все тесты. Ну или как минимум большую их часть. А значит мы снова возвращаемся к ситуации "все тесты уже написаны, нужно лишь написать правильно код". А все те осторожные шажки по одному тесту, что мы делали ранее, просто обесцениваются.


Когда TDD полезен


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


  • Исправление дефектов
  • Заранее известный контракт
  • Не заставить себя писать тесты

Если к вам пришёл баг, то вы его можете сперва формализовать в виде теста, а далее исправлять код, пока все тесты не позеленеют.


Кроме того, в ситуации, когда контракт вам известен заранее, вы можете сразу написать все тесты, а потом уже весь код, который им соответствует. Формально, это будет не TDD, так как вы не будете менять код после добавления каждого теста. Однако, это самый что ни на есть Test Driven, так как тесты будут диктовать вам реализацию.


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


Программировать ли по TDD?


Если кто-то вам скажет, что он "программирует по TDD", то можете быть уверены, что он попросту не ведает, что творит. У TDD есть ряд фундаментальных проблем, поэтому его применение оправдано лишь в весьма ограниченном числе случаев. И то, не в той форме в которой ему как правило учат многочисленные коучи.


- Ритуализация :-(- Явно некорректный код :-(- Бесполезная работа :-(- Там, где это уместно :-)- Не зацикливаться :-)

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


Что ещё посмотреть по TDD?


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



Продолжение следует..


  • Лайк
  • Подписка
  • Комментарий
  • Поделись-ка

Если данный разбор показался вам полезным, то дайте мне об этом знать посредством лайка. А так же поделитесь ссылкой на него со своими коллегами.


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


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


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


На этом пока что всё. С вами был боевой программер Дмитрий Карловский.

Подробнее..

Деконструкция LSP

29.09.2020 20:11:58 | Автор: admin

Здравствуйте, меня зовут Дмитрий Карловский. А вы на канале Core Dump, где мы берём разные темы из компьютерной науки и деконструируем их по полочкам. А на этот раз мы начнём деконструировать принципы SOLID начиная с наиболее конкретного.


В далёком 1987 году Барбара Лисков сформулировала принцип разработки имени себя.


Liskov Substitution Principle

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


Видео запись этого разбора.


Отношение "супертип-подтип"


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



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


Отношение "супертип-подтип" является транзитивным, то есть если один тип является подтипом другого, а другой третьего, то и первый является подтипом третьего.


Сильная и слабая типизация


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



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


Полиморфизм


Полиморфизм это способность одного и того же кода работать с аргументами разных типов.



В данном примере, процедура draw у нас принимает на вход произвольную фигуру. И какую бы фигуру мы ей ни передали процедура её всё равно нарисует.


Суть LSP повсеместная ковариантность


Наконец, теперь мы можем сформулировать принцип подстановки Барбары Лисков: "любые функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом и не нарушая желаемых свойств программы".



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


Звучит вроде бы логично, однако...


Контравариантность не вписывается в LSP


Давайте рассмотрим функцию, которая принимает на вход коробку для питомца и засовывает в неё какого-то, нам не известного, питомца.



Если мы позволим передавать в неё клетку для собак, то вскоре можем столкнуться с абсурдной ситуацией, когда вытащим оттуда казалось бы собаку и скажем "Апорт", а она замяукает и нассыт вам в тапки. То есть подтип передавать в эту функцию нельзя. А вот супертип очень даже можно, ведь в клетку для животных можно засовывать хоть домашнего, хоть дикого животного. Получается своего рода инверсия LSP. То есть иерархия типов ограничивается не сверху, а снизу. Такое ограничение имеет название "контравариантность".


Виды вариантностей


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


  1. Только чтение ковариантность (ограничение сверху)
  2. Только запись контравариантность (ограничение снизу)
  3. Чтение и запись:
    • инвариантность (ограничение снизу и сверху)
    • бивариантность (без ограничений)

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


Применимость LSP


Все эти разные вариантности появляются лишь когда мы изменяем какое-то состояние. Но если мы работаем лишь с чистыми функциями, которые ничего не изменяют, то ковариантность всех параметров и, как следствие, LSP мы получаем автоматически. Хотим мы того или нет от нас это не зависит.


  • Функциональное Программирование :-)

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


Правильный LSP


Ладно, давайте пофантазируем и попробуем сформулировать LSP здорового человека, учитывающего все озвученные ранее нюансы...


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


Звучит с одной стороны всё ещё так же размыто, как и оригинальная формулировка, а с другой самоочевидно. Понятное дело, что если есть возможность правильно написать типы, то стоит это делать правильно.


  • Статическая типизация :-)

Следовать ли LSP?


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


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


  • LSP :-(
  • Вариантность :-)

Что ещё почитать о вариантности?


Для лучшего понимания вопроса вариантности рекомендую свою статью на эту тему. Там я доступным языком рассказываю всю эту сложную теорию, и привожу примеры кода на разных языках программирования, иллюстрирующие разные типы вариантности.


Теория программирования: Вариантность

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


Продолжение следует..


Лайк
Подписка
Комментарий
Поделись-ка

Если данный разбор показался вам полезным, то дайте мне об этом знать посредством лайка. А так же поделитесь ссылкой на него со своими коллегами.


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


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


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


На этом пока что всё. С вами был немножко программер Дмитрий Карловский.

Подробнее..

Категории

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

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