3. Качество кода в тестировании
• Многие не считают, что разработка
автотестов – это разработка.
• Иногда и разработчик автотестов не
считает, что он занимается разработкой
4. Что такое хороший код?
• Что такое код?
• Что такое хороший код?
• Почему важен хороший код?
5. Дизайн
Дизайн – это результат
процесса
проектирования :
декомпозиция системы,
определение поведения
и характеристики
отдельных компонент и
их взаимодействия.
6. Отсутствие гибкости (Rigidity)
Код тяжело поддается изменениям.
Изменения в одном месте, вызывают
изменения в других частях системы
приводя к эффекту «снежного кома».
7. Хрупкость (Fragility)
Код легко «ломается». Т.е. после
осуществления изменения код может
сломаться еще в нескольких других
местах.
9. Вязкость (Viscosity)
Сделать что-то хорошо, очень сложно.
Существующий код сам провоцирует на
дальнейшее увеличение количества
«хаков» и «костылей».
14. Что такое SOLID?
• S - Single responsibility principle
• O - Open/closed principle
• L - Liskov substitution principle
• I - Interface segregation principle
• D - Dependency inversion principle
33. Dependency inversion principle
Модули верхнего уровня не должны
зависеть от модулей нижнего уровня.
Оба должны зависеть от абстракции.
Абстракции не
должны зависеть от
деталей.
Детали должны
зависеть от
абстракций.
40. Не забываем!
• DRY – Don’t repeat yourself (не повторяй
себя)
• KISS – keep it simple stupid (делайте
вещи проще)
• YAGNI - You ain’t gonna need it – вам это
не понадобится
42. Вопросы?
Об авторе :
Игорь Горчаков
skype : igor-gorchakov
e-mail :
gorchakov.igiv@gmail.com
Литература : Принципы, паттерны и методики гибкой разработки на языке C#
(Agile Principles, Patterns and Practices in C#)
Editor's Notes
Поставить проблему и упомянуть о том, что доклад полезен будет тем, кто пишет на ОО ЯП.
Первая проблема : уменьшение сроков на разработку. Отсутствия понимания, что необходимо время на юнит тестирование, ревью кода, рефакторинг. Как вариант – неверное соотношение разработчиков и «тест-разработчиков».
Можно проводить беседы с руководителем, хорошо если компанию вам составят пара разработчиков самого объекта для тестирования. Можно объяснять почему это важно, приводить графики зависимости времени поддержки тест фреймворка, от длительности проекта и прочие атрибуты защиты разработки, как таковой.
Вторая проблема : не стараемся писать хороший код, который еще долго придется поддерживать. Не пишем юнит тесты и спустя рукава подходим к тестированию реализованных решений. Не занимаемся ревью кода, рефакторингом и прочими «гигиеническими» процедурами.
Нужно бороться за качество своего кода самостоятельно. Более того, как уверяют авторы трудов (к примеру, тот же Роб Мартин) – это всегда можно сделать. На последнем месте работы, несмотря на высокий темп разработки, у меня получалось выкраивать время (иногда по партизански завышая время на implementation ), объяснять руководителю о необходимости изменений в определенных местах и получении время и на рефакторинг и на код ревью. Но, главное в этом процессе – желание улучшать свой код. Как писал все тот же Роб Мартин, разработка – это ремесло 21-го века. И если вы хотите, чтобы подходить к «станку» было приятно, чтобы вы всегда могли найти нужный вам инструмент, не разбирая полчаса бардак и мусор - за своим рабочим местом нужно следить. В этом, на мой взгляд, кроется и удовольствие от программирования – в чистый и приятный код всегда легко и с удовольствием погружаешься. И испытываешь сильное отвращения погружаясь в запутанный, сложный «грязный» код. Это одна из причин, почему все так любят начинать новые проекты, пока код еще не захламлен «грязными» «быстрыми» изменениями уже в процессе его использования.
Что такое код?
Код - это то, что предоставлять клиенту сервис.
Проходят ли тесты, низка ли цикломатическая сложность – это важно, но главное – клиент должен получить услугу.
Таким образом, что такое хороший код?
Это код который работает.
Когда код просто для понимания мы легко можем его изменить, дополнить и реже ошибиться при изменении. Т.е. хороший код просто для изменений.
Когда код прост для изменения – мы можем быстрее среагировать на feedback клиента, исправить bugs и улучшить сервис.
Более того, когда код просто для понимания – мы и сами счастливее и получаем удовольствие от работы.
Именно поэтому хороший код важен.
Как же писать хороший код?
Дизайн – это результат процесса проектирования : декомпозиция системы, определение поведения и характеристики отдельных компонент и их взаимодействия.
Т.е. простыми словами, бизнес модель мы разбиваем на отдельные компоненты и определяем взаимодействие между ними.
Как сделать хороший дизайн, который и приведет нас к хорошему коду?
Давайте определимся, что точно мы бы хотели избежать в результате проектирования, а именно признаки плохого дизайна.
Отсутствие гибкости – код тяжело поддается изменениям. Изменения в одном месте, вызывают изменения в других частях системы приводя к эффекту «снежного кома».
Отсутствие гибкости проявляется в том случае, когда программа с трудом поддается даже простым изменениям. Дизайн перестает быть гибким, если одно изменение влечет за собой каскад последующих изменений в зависимых модулях. Чем больше модулей подвержено изменениям, тем менее гибким считается дизайн проекта.
Большинство разработчиков, так или иначе, сталкиваются с этой проблемой. Их просят сделать простые на первый взгляд изменения. Они внимательно изучают характер будущих изменений, а затем выполняют обоснованную оценку требуемого в этом случае объема работы. Позднее, в процессе реализации изменений, разработчики сталкиваются с непредвиденными последствиями этих изменений. В частности, подвергаются переработке огромные блоки кода, причем в процесс модификации вовлекается намного больше модулей, чем планировалось в результате первоначальной оценки. В конце концов, внедрение изменений занимает намного больше времени, чем планировалось изначально. Если спросить разработчиков о том, почему не оправдались их расчеты, они будут жаловаться на то, что задача оказалась намного сложнее, чем предполагалось изначально.
Хрупкость – Код легко «ломается». Т.е. после осуществления изменения код может сломаться еще в нескольких других местах.
Зачастую новые проблемы возникают в тех областях, которые казалось бы не связаны с изменяемым компонентом. В процессе исправления этих ошибок возникают новые ошибки, в результате чего команда разработчиков начинает походить на собаку, гоняющуюся за собственным хвостом.
По мере возрастания хрупкости программного модуля вероятность появления непредвиденных проблем приближается к 100%. Несмотря на всю кажущуюся абсурдность подобного утверждения, подобные модули встречаются нередко. Задачи переделки таких модулей могут бесконечно висеть в баг трекере, но никто из разработчиков не хочет за них браться.
Монолитность – компоненты настолько сильно связаны друг с другом, что их сложно разделить друг от друга и переиспользовать.
Дизайн проекта считается монолитным, если он содержит компоненты, которые могли бы применяться в других системах, однако усилия и степень риска, связанные с выделением этих компонентов из первоначальной системы, слишком велики. К сожалению, такое встречается весьма часто.
Вязкость – сделать что-то хорошо, очень сложно. Существующий код сам провоцирует на дальнейшее увеличение количества «хаков» и «костылей».
Вязкость может проявляться в двух формах: по отношению к ПО и к среде.В случае необходимости внесения изменений разработчики, как правило, видят несколько вариантов решения задачи. Некоторые из них сохраняют исходный дизайн проекта, а другие — нет (т.е. относятся к разряду хакерских приемов). Если предлагаемые дизайном методы сложнее в применении, чем хакерские приемы, то говорят, что вязкость проекта высока. В этом случае легко допустить ошибку, а правильные действия выполнить не так уж и просто. В идеале дизайн проекта должен быть таким, чтобы внесение изменений, не ухудшающих этот дизайну, осуществлялось легко и понятно.
Симптом вязкости среды наблюдается в случае, если среда разработки характеризуется словами “медленный” и “неэффективный”. Например, если компиляция занимает очень долгое время, у разработчика может возникнуть желание изменять программный код таким образом, чтобы избежать полной рекомпиляции (даже если изменение ухудшает дизайн проекта). Если системе управления исходным кодом требуется несколько часов, чтобы обновить в репозитории всего несколько файлов, то разработчики могут захотеть сделать изменения, требующие как можно меньше обновлений в репозитории, даже если это приведет к ухудшению дизайна проекта.
В обоих случаях вязкий проект представляет собой проект, в котором трудно сохранить качественный дизайн. Мы же хотим создавать такие системы, в которых можно без особых усилий сохранять качественный дизайн проекта и улучшать его.
Излишняя сложность (Overdesign) – код содержит в себе архитектурные решения, необходимость в которых еще не назрела.
Проект имеет неоправданную сложность, если содержит элементы, не использующиеся в настоящий момент времени. Это часто происходит в том случае, когда разработчики предсказывают изменения в требованиях и проводят мероприятия, направленные на то, чтобы справиться с этими потенциальными изменениями в будущем. На первый взгляд кажется, что это неплохо. В конце концов, подготовка к предстоящим изменениям должна сохранить наш код гибким и предотвратить кошмарные изменения, которые могут возникнуть впоследствии.
К сожалению, эффект от таких мероприятий может быть совсем противоположным. При подготовке к большому количеству возможных изменений в требованях и непредвиденных ситуаций, проект начинает “замусориваться” частями кода, которые не используются, и возможно никогда не понадобятся. На практике некоторые из таких подготовительных мероприятий оправдываются и окупаются, но большинство — нет. Между тем проект несет на себе груз неиспользуемых элементов, а программа получается сложной и трудно понимаемой.
Излишняя повторяемость – код содержит в себе дублирование, особенно такого, которое легко устраняется.
Операции “вырезки” и “вставки” могут быть полезными при редактировании текста, но в то же время они могут быть опасными операциями в случае редактирования кода. Очень часто программные системы выстраиваются на десятках или сотнях повторяющихся элементов кода. Это происходит примерно следующим образом:Предположим, что Ральфу необходимо написать код, выполняющий некие функции. Он просматривает другие части кода, где, по его мнению, такие функции уже выполнялись и обнаруживает подходящий фрагмент. Он копирует и вставляет этот код в свой модуль и производит необходимые изменения.Ральфу неизвестно, что этот фрагмент кода был изначально позаимствован Тодом из модуля, написанного Лили. Причем Лили было нужно, чтобы ее код выполнял поиск натуральных чисел. Она быстро обнаружила, что код, выполняющий поиск целых чисел, тоже может применяться в этой ситуации. Затем она просто вырезала последний фрагмент кода и включила его в свой модуль, изменив соответствующим образом последний.
Если один и тот же код появляется несколько раз в несколько различных формах, разработчики теряют абстракцию. Поиск всех повторений, а также их устранение с применением соответствующих абстракций может и не включаться в первые пункты списка приоритетов, но следует учитывать то, что в противном случае затрачивается немало усилий и времени для того, чтобы система получалась легкой для понимания и изменения в дальнейшем.
Когда в проекте есть дублирование кода, это может сильно усложнить работу по изменению системы. Ошибки, обнаруженные в одном из блоков повторяющегося кода должны быть исправлены во всех местах, содержащих этот код. Тем не менее, поскольку каждое повторение может в незначительной степени отличаться от всех остальных, то исправления не всегда могут быть одинаковыми, и в такой ситуации легко допустить ошибку.
Нечитабельность – код сложен для чтения и понимания.
Неясность программного модуля проявляется в сложности его понимания. Код может быть написан либо в четкой и выразительной манере, либо быть непонятным и запутанным. Код, эволюционирующий с течением времени, обычно становится все более неясным. Необходимо постоянно следить за тем, чтобы код оставался “прозрачным” и выразительным, сводя возможную неясность к минимуму.
В начале написания нового модуля его код может казаться разработчикам достаточно “прозрачным” и понятным, т.к. они, погружаяются в проблему с головой, и код может казаться проще и понятнее, чем есть на самом деле. Позднее они могут вернуться к модулю и удивиться, что могли написать нечто настолько ужасное. Чтобы это предотвравить, разработчики должны представлять себя на месте будущих читателей этого кода и приложить усилия для необходимого рефакторинга кода, так чтобы будущие читатели этого кода смогли его понять. Кроме того, желательно, чтобы кто-то еще просмотрел составленный ими код (code review).
Таким образом, после того, как мы перечислили признаки плохого дизайна, мы можем инвертировать их и получить признаки хорошего дизайна.
Гибкость – система легко поддается изменениям
Прочность – единичное изменение не приводит к появлению неожиданных ошибок в зависимых модулях
Переиспользуемость – систему легко разделить на компоненты, которые могут быть повторно использованы в других местах
Отсутствие вязкости - легко внести изменения, не ухудшая дизайн проекта, не используя хаки
Простота – система не содержит частей, которые просто усложняют и замусоривают код и скорее всего никогда не понадобятся
Отсутствие дублирования кода – код легко изменяется
Читабельность - архитектура и код проекта легки для понимания
Как же добиться, отсутствия признаков плохого дизайна и увидеть только признаки хорошего?
Есть несколько практик, которые помогут в этом.
SOLID - это аббревиатура пяти основных принципов дизайна классов в ООП.
Понятие ввел Роберт Мартин в 2006 году в книге : «Принципы, паттерны и методики гибкой разработки на языке C#»
S – Принципе единственной ответственности
O – Принцип открытости/закрытости
L - Принцип замещения Лисков
I - Принцип разделения интерфейса
D - Принцип инверсии зависимости
Формулировка: не должно быть больше одной причины для изменения класса
Что является причиной изменения логики работы класса? Видимо, изменение отношений между классами, введение новых требований или отмена старых. Вообще, вопрос о причине этих изменений лежит в плоскости ответственности, которую мы возложили на наш класс. Если у объекта много ответственности, то и меняться он будет очень часто. Таким образом, если класс имеет больше одной ответственности, то это ведет к хрупкости дизайна и ошибкам в неожиданных местах при изменениях кода.
Формулировка: программные сущности (классы, модули, функции и т.д.) должны быть открыты для расширения, но закрыты для изменения
Какую цель мы преследуем, когда применяем этот принцип? Как известно программные проекты в течение свой жизни постоянно изменяются. Изменения могут возникнуть, например, из-за новых требований заказчика или пересмотра старых. В конечном итоге потребуется изменить код в соответствии с текущей ситуацией.
С одной стороны внесение изменений требует времени программистов и тестировщиков, которое является очень дорогим ресурсом в производстве ПО. С другой, бизнес должен достаточно быстро реагировать на рыночные изменения и время здесь представляется очень важным конкурентным преимуществом.
Отсюда можно сделать вывод, что нашей целью является разработка системы, которая будет достаточно просто и безболезненно меняться. Другими словами, система должна быть гибкой. Например, внесение изменений в библиотеку общую для 4х проектов не должно быть долгим («долгим» является разным промежутком времени для конкретной ситуации) и уж точно не должно вести к изменениям в этих 4х проектах.
Принцип открытости/закрытость как раз и дает понимание того, как оставаться достаточно гибкими в условиях постоянно меняющихся требований.
У нас есть иерархия объектов с абстрактным родительским классом Entity и класс Repository, который использует абстракцию. При этом вызывая метод Save у Repositoryмы строим логику в зависимости от типа входного параметра.
Из кода видно, что объект Repository придется менять каждый раз, когда мы добавляем в иерархию объектов с базовым классом AbstractEntity новых наследников или удаляем существующих. Условные операторы будут множится в методе save() и тем самым усложнять его.
Конкретизируя классы методом instanceof мы должны сразу понять, что что-то с нашим кодом не то… Чтобы решить данную проблему, необходимо логику сохранения конкретных классов из иерархии Entity вынести в конкретные классы Repository. Для этого мы должны выделить интерфейс Repository и создать хранилища BookRepository и AuthorRepository:
Объекты в программе могут быть заменены их наследниками без изменения свойств программы
Наследующий класс должен дополнять, а не замещать поведение базового класса.
Я уже приводил код проверки абстракции на тип на примере нарушения принципа открытости/закрытости. Теперь мы видим, что класс Repository нарушает еще и принцип замещения Лисков. Дело в том, что внутри класса Repository мы оперируем не только абстрактной сущностью AbstractEntity, но и унаследованными типами. А это значит, что в данном случае подтипы AccountEntity и RoleEntity не могут быть заменены типом, от которого они унаследованы. По определению имеем нарушение.
Надо заметить, что принципы проектирования взаимосвязаны. Нарушение одного из принципов скорее всего приведет к нарушению одного или нескольких других принципов.
У нас есть класс Rectangle (Прямоугольник). Мы можем задать/узнать ширину и высоту. А так же есть метод для получения площади прямоугольника.
И есть частный случай прямоугольника – квадрат. Тут ширина и высота одинакова. Реализовать его предлагается через переопределение методов задания высоты и ширины, таким образом гарантируется, что фигура всегда будет квадратом.
Давайте рассмотрим тест. Тест работает с прямоугольником, но при инициализации, мы все таки видим, что это частный случай прямоугольника – квадрат. При попытке подсчитать его площадь мы получим ошибку. Хотя, тест делает вполне разумные действия с прямоугольником, в рамках его контракта.
Мы видим, что в данном случае, принцип Liskov не выполняется, нельзя будет в системе заменить Прямоугольник – Квадратом, без изменения свойств системы.
У нас есть класс Rectangle (Прямоугольник). Мы можем задать/узнать ширину и высоту. А так же есть метод для получения площади прямоугольника.
И есть частный случай прямоугольника – квадрат. Тут ширина и высота одинакова. Реализовать его предлагается через переопределение методов задания высоты и ширины, таким образом гарантируется, что фигура всегда будет квадратом.
Давайте рассмотрим тест. Тест работает с прямоугольником, но при инициализации, мы все таки видим, что это частный случай прямоугольника – квадрат. При попытке подсчитать его площадь мы получим ошибку. Хотя, тест делает вполне разумные действия с прямоугольником, в рамках его контракта.
Мы видим, что в данном случае, принцип Liskov не выполняется, нельзя будет в системе заменить Прямоугольник – Квадратом, без изменения свойств системы.
Принцип разделения интерфейса
Много специализированных интерфейсов лучше, чем один универсальный.
Или клиенты не должны зависеть от методов, которые они не используют.
Как и при использовании других принципов проектирования классов мы пытаемся избавиться от ненужных зависимостей в коде, сделать код легко читаемым и легко изменяемым.
Формулировка:
Модули верхнего уровня не должны зависеть от модулей нижнего уровня. Оба должны зависеть от абстракции.
Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Сейчас мы создадим и отрефакторим приложение. Будем двигаться по шагам и я покажу применение принципа инверсии зависимостей в действии.
Наша система будет консольным приложением, которое занимается рассылкой отчетов.
<Program class>
Главный объект в нашей бизнес-логике – Reporter.
<Reporter class>
Устроен Reporter очень просто. Он просит CustomReportBuilder создать список отчетов, а потом один за другим отсылает их с помощью объекта EmailReportSender.
Есть ли в этом коде проблемы? В подавляющем большинстве случаев зависит от того, кто и как этот код будет использовать, как часто он будет меняться и т.д. Но есть проблемы, которые очевидны уже сейчас.
Тестируемость
Как протестировать функцию SendReports? Давайте проверим поведение функции, когда CustomReportBuilder не создал ни одного отчета. В этом случае она должна создать исключение NoReportsException
<Report Test>
Как в этом случае задать поведение объектов, которые использует Reporter? Мы же должны «сказать» CustomReportBuilder'у вернуть пустой список, и тогда функция sendReports выбросит исключение. Но в текущей реализации Reporter'а сделать мы этого не можем. Получается, мы не можем задать такие входные данные, при которых sendReports выкинет исключение. Значит в данной реализации объект Reporter очень плохо поддается тестированию.
Связанность
Дело в том, что функция sendReports, кроме своей прямой обязанности, слишком много знает и умеет:
знает, что именно CustomReportBuilder будет создавать отчеты
знает, что все отчеты надо отсылать через email с помощью EmailReportSender
умеет создавать объект CustomReportBuilder
умеет создавать объект EmailReportSender
Здесь нарушается принцип единственности ответственности. Проблема заключается в том, что в данный момент внутри функции sendReports объект CustomReportBuilder создается оператором new. А если у него появятся обязательные параметры в конструкторе? Нам придется менять код в классе Reporter да и во всех других классах, которые использовали оператор new для CustomReportBuilder'а.
К тому же, первые пункты нарушают принцип открытости/закрытости. Дело в том, что если мы захотим с помощью нашей утилиты отсылать сообщения через SMS, то придется изменять код класса Reporter. Вместо EmailReportSender мы должны будем написать SmsReportSender. Еще сложнее ситуация, когда одна часть пользователей класса Reporter захочет отправлять сообщения через emal, а вторая через SMS.
Обратите внимание, что наш объект Reporter зависит не от абстракций, а от конкретных объектов CustomReportBuilder и EmailReportSender. Можно сказать, что он "сцеплен" с этими классами. Это и объясняет его хрупкость при изменениях в системе. Может оказаться, что Reporter жестко зависит от двух классов, эти два класса зависят еще от 4х других. Получится, что вся система – это клубок из стальных ниток, который нельзя ни изменить, ни протестировать. Этот пример наглядно показывает нарушение принципа инверсии зависимостей.
Наша система будет консольным приложением, которое занимается рассылкой отчетов.
<Program class>
Главный объект в нашей бизнес-логике – Reporter.
<Reporter class>
Устроен Reporter очень просто. Он просит CustomReportBuilder создать список отчетов, а потом один за другим отсылает их с помощью объекта EmailReportSender.
Есть ли в этом коде проблемы? В подавляющем большинстве случаев зависит от того, кто и как этот код будет использовать, как часто он будет меняться и т.д. Но есть проблемы, которые очевидны уже сейчас.
Тестируемость
Как протестировать функцию SendReports? Давайте проверим поведение функции, когда CustomReportBuilder не создал ни одного отчета. В этом случае она должна создать исключение NoReportsException
<Report Test>
Как в этом случае задать поведение объектов, которые использует Reporter? Мы же должны «сказать» CustomReportBuilder'у вернуть пустой список, и тогда функция sendReports выбросит исключение. Но в текущей реализации Reporter'а сделать мы этого не можем. Получается, мы не можем задать такие входные данные, при которых sendReports выкинет исключение. Значит в данной реализации объект Reporter очень плохо поддается тестированию.
Связанность
Дело в том, что функция sendReports, кроме своей прямой обязанности, слишком много знает и умеет:
знает, что именно CustomReportBuilder будет создавать отчеты
знает, что все отчеты надо отсылать через email с помощью EmailReportSender
умеет создавать объект CustomReportBuilder
умеет создавать объект EmailReportSender
Здесь нарушается принцип единственности ответственности. Проблема заключается в том, что в данный момент внутри функции sendReports объект CustomReportBuilder создается оператором new. А если у него появятся обязательные параметры в конструкторе? Нам придется менять код в классе Reporter да и во всех других классах, которые использовали оператор new для CustomReportBuilder'а.
К тому же, первые пункты нарушают принцип открытости/закрытости. Дело в том, что если мы захотим с помощью нашей утилиты отсылать сообщения через SMS, то придется изменять код класса Reporter. Вместо EmailReportSender мы должны будем написать SmsReportSender. Еще сложнее ситуация, когда одна часть пользователей класса Reporter захочет отправлять сообщения через emal, а вторая через SMS.
Обратите внимание, что наш объект Reporter зависит не от абстракций, а от конкретных объектов CustomReportBuilder и EmailReportSender. Можно сказать, что он "сцеплен" с этими классами. Это и объясняет его хрупкость при изменениях в системе. Может оказаться, что Reporter жестко зависит от двух классов, эти два класса зависят еще от 4х других. Получится, что вся система – это клубок из стальных ниток, который нельзя ни изменить, ни протестировать. Этот пример наглядно показывает нарушение принципа инверсии зависимостей.
Наша система будет консольным приложением, которое занимается рассылкой отчетов.
<Program class>
Главный объект в нашей бизнес-логике – Reporter.
<Reporter class>
Устроен Reporter очень просто. Он просит CustomReportBuilder создать список отчетов, а потом один за другим отсылает их с помощью объекта EmailReportSender.
Есть ли в этом коде проблемы? В подавляющем большинстве случаев зависит от того, кто и как этот код будет использовать, как часто он будет меняться и т.д. Но есть проблемы, которые очевидны уже сейчас.
Тестируемость
Как протестировать функцию SendReports? Давайте проверим поведение функции, когда CustomReportBuilder не создал ни одного отчета. В этом случае она должна создать исключение NoReportsException
<Report Test>
Как в этом случае задать поведение объектов, которые использует Reporter? Мы же должны «сказать» CustomReportBuilder'у вернуть пустой список, и тогда функция sendReports выбросит исключение. Но в текущей реализации Reporter'а сделать мы этого не можем. Получается, мы не можем задать такие входные данные, при которых sendReports выкинет исключение. Значит в данной реализации объект Reporter очень плохо поддается тестированию.
Связанность
Дело в том, что функция sendReports, кроме своей прямой обязанности, слишком много знает и умеет:
знает, что именно CustomReportBuilder будет создавать отчеты
знает, что все отчеты надо отсылать через email с помощью EmailReportSender
умеет создавать объект CustomReportBuilder
умеет создавать объект EmailReportSender
Здесь нарушается принцип единственности ответственности. Проблема заключается в том, что в данный момент внутри функции sendReports объект CustomReportBuilder создается оператором new. А если у него появятся обязательные параметры в конструкторе? Нам придется менять код в классе Reporter да и во всех других классах, которые использовали оператор new для CustomReportBuilder'а.
К тому же, первые пункты нарушают принцип открытости/закрытости. Дело в том, что если мы захотим с помощью нашей утилиты отсылать сообщения через SMS, то придется изменять код класса Reporter. Вместо EmailReportSender мы должны будем написать SmsReportSender. Еще сложнее ситуация, когда одна часть пользователей класса Reporter захочет отправлять сообщения через emal, а вторая через SMS.
Обратите внимание, что наш объект Reporter зависит не от абстракций, а от конкретных объектов CustomReportBuilder и EmailReportSender. Можно сказать, что он "сцеплен" с этими классами. Это и объясняет его хрупкость при изменениях в системе. Может оказаться, что Reporter жестко зависит от двух классов, эти два класса зависят еще от 4х других. Получится, что вся система – это клубок из стальных ниток, который нельзя ни изменить, ни протестировать. Этот пример наглядно показывает нарушение принципа инверсии зависимостей.
Применяем принцип инверсии зависимостей
Сейчас несколькими простыми действиями мы решим наши проблемы с Reporter'ом.
Для начала вынесем интерфейсы ReportSender из EmailReportSender и ReportBuilder из CustomReportBuilder.
Теперь вместо того, чтобы создавать объекты в функции SendReports, мы передами их объекту Reporter в конструктор
Применяем принцип инверсии зависимостей
Сейчас несколькими простыми действиями мы решим наши проблемы с Reporter'ом.
Для начала вынесем интерфейсы ReportSender из EmailReportSender и ReportBuilder из CustomReportBuilder.
Теперь вместо того, чтобы создавать объекты в функции SendReports, мы передами их объекту Reporter в конструктор
Теперь у нас есть возможность передавать в конструктор Reporter'а объекты, которые реализуют нужные интерфейсы. Давайте подставим mock-объекты и зададим нужное нам поведение.
Тест прошел! Мы отлично справились. Теперь есть возможность задавать поведение объектов, с которыми работает наш Reporter. И в данном случае нам не важно, что где-то есть EmailReportSender, SmsReportSender или еще какой-то *ReportSender. Тесты Reporter'а не зависят от других реализаций, мы используем только интерфейсы. Это делает тесты более устойчивыми к изменениям в системе.