Написать программное обеспечение легко. Что сложно, так это написать программное обеспечение, которое легко понять и легко изменить через шесть месяцев другим человеком.

Один из способов помочь вам достичь этого — следовать принципу единой ответственности, который был представлен Робертом «дядей Бобом» Мартином в его книге Agile Software Development — Principles, Patterns and Practices в 2002 году.

Определение

Принцип единой ответственности гласит, что ваш код должен иметь одну и только одну ответственность.

Ответственность — это красивое слово, означающее, что у вас есть причина измениться.

Причины для изменения

Когда вы думаете о коде. Что может быть причиной его замены?

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

«Нам нужен веб-сайт для отображения всех данных о наших клиентах в виде таблицы».

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

Вот краткий обзор того, что вы могли бы придумать.

Класс контроллера предоставляет конечную точку и запрашивает у службы поддержки обработанные данные и отправляет их клиенту.

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

Следовательно, код, который имеет дело с логикой базы данных, необходимо изменить. Он находится в CustomerService.

Через несколько дней ваша компания решает, что простого веб-сайта уже недостаточно, поскольку менеджеры по работе с клиентами хотят использовать приложение на iPad и хранить данные для использования в автономном режиме. Вам необходимо предоставить API для представления данных в формате JSON. Поскольку код для получения данных уже находится в CustomerService, вы решили добавить в этот класс метод getCustomerJSON.

Оба эти события являются поводом для перемен. Обратите внимание, что эти изменения не связаны между собой и произошли в разное время.

Базу данных нужно было изменить из соображений производительности, API нужно было добавить из-за бизнес-решения.

Тем не менее, как ни странно, в обоих случаях необходимо было изменить CustomerService. Почему? Потому что он отвечает за получение данных и рендеринг данных. У него больше одной ответственности.

Рендеринг HTML и получение данных из базы данных не связаны друг с другом — они не связаны.

Проблемы мультиответственности

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

Сложно понять.

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

Во-первых, название довольно обманчиво. Очевидно, в нем есть нож, но на самом деле в нем есть много других вещей, таких как отвертка, ножницы, консервный нож и много чего еще.

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

Если вы готовы принять вызов, я рекомендую вам вспомнить все инструменты, которые предоставляет гигантский нож Wenger 2007. Кстати, у него 81.

Не позволяйте вашим занятиям превратиться в Гигантский нож Венгера. Если у вашего класса несколько обязанностей, трудно найти для него краткое название, не говоря уже о кратком описании.

Давайте еще раз посмотрим на наш класс CustomerService. Является ли Service именем, которое сразу говорит вам, что делает этот вызов? Можете ли вы описать, что он делает, не используя такие слова, как И и ИЛИ?

Нет, потому что он делает слишком много вещей.

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

Трудно изменить

Единственное, что постоянно, — это изменение — Гераклит.

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

Представьте себе снова свой швейцарский армейский нож. Допустим, отвертка может работать только с винтами Phillips, но теперь вам нужно работать с винтами со шлицами. Вам нужно заменить отвертку. Удачи в разборке всего этого, замене нужных вещей и сборке обратно, ничего не сломав.

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

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

Также сложнее добавлять новые функции или заменять их.

Служба CustomerService имеет дело со всевозможными вещами. Он получает данные из базы данных, поэтому ему необходимо обработать свое соединение. Он также отображает HTML, возможно, для этого используется механизм шаблонов.

Класс довольно раздут, и довольно сложно отличить, какие частные функции и свойства относятся к какой ответственности. Кажется, некоторые даже общие.

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

Эти две обязанности, соединенные вместе, делают очень трудным изменение, не влияя на другое.

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

Сотрудник делает это раньше вас и отправляет код в систему контроля версий. Поскольку у него осталось немного времени, он решает сделать дополнительную работу.

Когда ты закончишь, уже вечер пятницы. Вы с нетерпением ждете заслуженных выходных и у вас прекрасное настроение, потому что новый код базы данных работает без сбоев. Вам просто нужно отправить свой код и отправиться домой, но подождите… последнее сообщение о коммите вашего коллеги было небольшой рефакторинг. маленький оказался полным редизайном класса, и у вас большие проблемы, потому что теперь вам нужно объединить свои вещи. Прощай, хорошее настроение. Да, кстати, рендеринг HTML снова сломался. Прощальные выходные.

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

Эмпирическое правило: если вам не нужно прикасаться к классу, вы его не сломаете.

Трудно проверить

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

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

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

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

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

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

Другой фактор заключается в том, что класс с несколькими обязанностями, как правило, имеет много зависимостей. Служба CustomerService взаимодействует с базой данных, поэтому, вероятно, для этого у нее есть какая-то библиотека. Он также отображает HMTL и сериализует JSON. Вероятно, они добавляют другие зависимости.

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

Трудно использовать повторно

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

Возможно, нужная вам функциональность находится в классе с неоднозначным названием. Во-первых, вам нужно знать, какая функциональность там есть (неоднозначно названные классы затрудняют понимание приложения, не так ли?). Во-вторых, вам нужно иметь дело с тем фактом, что даже если другая ответственность говорит об изменении класса, ваш код может сломаться. Это связано с тем, что несколько обязанностей в одном классе связывают их, а значит, и все зависимые классы. (Очень сложно сменить класс, имея несколько обязанностей, верно?)

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

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

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

Заворачивать

Поскольку мы уже знаем, какую пользу может принести нам разделение наших обязанностей, давайте еще раз вернемся к нашему примеру. Проблема, похоже, в том, что мы не можем повторно использовать логику базы данных, потому что она связана с рендерингом html. Наш единственный способ предоставить JSON API без дублирования кода — поместить его в тот же класс и, следовательно, связать обработку JSON с визуализацией HTML.

Это не только усложняет понимание класса, но и делает его ненадежным, поскольку любое изменение в HTML-рендеринге может нарушить наш JSON API.

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

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