Книга: «Простое объектно-ориентированное проектирование: чистый и гибкий код»

image Привет, Хаброжители!

Объектно-ориентированное проектирование (ООП) — не просто чисто инженерная задача; оно перерастает в искусство. Никакая заданная последовательность шагов не приведет к оптимальному проекту. Объектно-ориентированное проектирование требует творческого подхода.

В книге «Простое объектно-ориентированное проектирование: чистый и гибкий код» Маурисио Аниче рассматривает ООП с двух точек зрения: как предотвратить резкое увеличение сложности системы и как получить «достаточно хорошую» архитектуру.

Об авторе
imageВ настоящее время Маурисио является техлидом в компании Adyen и руководит различными инициативами по повышению квалификации инженеров, в том числе Технической академией Adyen — командой, занимающейся дополнительной подготовкой и образованием инженеров. Кроме того, Маурисио — доцент кафедры программной инженерии в Делфтском технологическом университете в Нидерландах. За свою преподавательскую деятельность в области тестирования программного обеспечения получил награду «Преподаватель информатики — 2021» и TU Delft Education Fellowship — престижную стипендию, присуждаемую преподавателям-новаторам. Маурисио — автор книги Effective Software Testing: A Developer«s Guide1 (Manning, 2022).

Миссия Маурисио Аниче — помогать инженерам-программистам улучшать их навыки и продуктивность.


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

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

Вы знакомы с литературой по ООП? Даже если вы опытный разработчик, в книге «Простое объектно-ориентированное проектирование: чистый и гибкий код» вы найдете знакомые принципы, которые были сформированы под влиянием работ признанных экспертам, таких как Дэйв Парнас, Гради Буч и Эрик Эванс. Однако книга Маурисио Аниче также предлагает новое видение, привнося свежие идеи и подходы, которые могут оказаться полезными даже для опытных разработчиков.

В книге подробно обсуждаются следующие темы:

  • сложность кода,
  • согласованность (консистентность) и инкапсуляция,
  • управление зависимостями
  • проектирование абстракций
  • работа с инфраструктурой и модульность.


Примеры кода написаны на псевдо-Java, но могут быть понятны разработчикам, знакомым с любым объектно-ориентированным языком программирования, таким как C#, Python или Ruby.

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

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

Предлагаем ознакомиться с отрывком «Управление зависимостями»

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

Мы уже обсуждали проблемы больших классов и преимущества маленьких. С одной стороны, то, что класс зависит от других классов, а не делает все сам, — это хорошо. С другой стороны, когда класс делегирует часть своей задачи другому классу, он должен «доверять» ему в том, что тот делает свою работу правильно. Если разработчик внесет в сущность ошибку, то она может распространиться на служебный класс и повредить его, даже не контактируя с его кодом.

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

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

Управление зависимостями сродни изготовлению многоярусного торта. Если вы не сделаете это хорошо, торт упадет. В этой главе я рассказываю о паттернах, которые помогут вам взять зависимости под контроль.

4.1. РАЗДЕЛЕНИЕ ВЫСОКО- И НИЗКОУРОВНЕВОГО КОДА


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

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

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

У следования этому паттерну есть свои преимущества. Во-первых, при сопровождении кода, начиная с высокоуровневого кода, мы быстрее понимаем функцию, поскольку код содержит только «что», но не «как». Мы изучаем реализацию низкоуровневых элементов только при необходимости. Сопровождать код гораздо проще, когда не нужно читать сотни строк, чтобы понять, что он делает. Скрывая подробности, разработчики могут сосредоточиться на главном.

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

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

Возможно, вы слышали о принципе инверсии зависимостей (dependency inversion principle, DIP). Это название описывает явление, о котором я только что сказал. Принцип гласит, что мы должны зависеть от абстракций, а не от деталей. Более того, классы более высокого и более низкого уровней должны зависеть только от абстракций, а не от других классов более низкого уровня. Я не столь строг в отношении зависимости исключительно от абстракций, однако разделение высоко- и низкоуровневых задач более критично, чем создание ненужных абстракций. Некоторые низкоуровневые компоненты стабильны настолько, что не нуждаются в дополнительных абстракциях.

4.1.1. Создавайте стабильный код


Когда мы пишем код более высокого уровня, мы обычно пишем «стабильный код», и это хорошо. Интерфейсы — это пример единиц кода, которые остаются стабильными со временем, поскольку определяют на высоком уровне, что компонент предлагает внешнему миру. Интерфейсы не берут в расчет внутренние элементы реализации и являются отличным средством для того, чтобы отделить высокоуровневый код от низкоуровневого.

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

4.1.2. Разрабатывайте интерфейсы


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

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

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

ПРИМЕЧАНИЕ

В книге Growing Object-Oriented Systems, Guided by Tests Стива Фримена (Steve Freeman) и Ната Прайса (Nat Pryce) (Addison-Wesley Professional, 2009) есть отличное описание того, как разработка интерфейсов может помочь создать удобный в сопровождении объектно-ориентированный проект. Эта книга сто́ит того, чтобы ее прочитать.

4.1.3. Когда не стоит отделять высший уровень от низшего


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

Единственное, что я советую никогда не смешивать, независимо от сложности функции, — это инфраструктура и бизнес-код. Вы же не хотите, чтобы ваша бизнес-логика смешивалась с SQL-запросами или HTTP-вызовами для получения информации из веб-сервиса. В таких случаях всегда следует иметь интерфейс более высокого уровня, который описывает «что», а детали реализации поместить в классы более низкого уровня. Подробнее об этом я рассказываю в главе 6.

4.1.4. Пример: работа с сообщениями


В PeopleGrow! есть фоновая задача, которая запускается каждые 5 секунд и отправляет сообщения пользователям. Код получает неотправленные сообщения, извлекает внутренний идентификатор пользователя из его электронной почты, отправляет сообщение с помощью внутреннего коммуникатора и отмечает сообщение как отправленное (листинг 4.1).

image


Обратите внимание, насколько высокоуровневым является этот код. Он описывает только то, что ожидается от задачи, и не содержит низкоуровневых деталей реализации любой из этих частей. Мы знаем, что интерфейс MessageRepository возвращает список сообщений для отправки, но не знаем, каким образом. Мы знаем, что UserDirectory получает идентификатор пользователя на основе электронной почты, но не знаем, как это делается. То же самое касается интерфейса Bot; мы знаем, что он делает, но не знаем как.

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

image


На рис. 4.1 видно, что MessageSender зависит от интерфейсов, которые, скорее всего, будут стабильными, таких как MessageRepository, UserDirectory и Bot. Эти интерфейсы реализуются низкоуровневым кодом, который обеспечивает выполнение задач. Например, интерфейс MessageRepository реализуется классом MessageHibernateRepository, который использует Hibernate (библиотеку для языка программирования Java) для доступа к базе данных. Интерфейс UserDirectory реализуется классом CachedLdapServer, который кэширует информацию, извлекаемую сервером LDAP (Lightweight Directory Access Protocol — упрощенный протокол доступа к каталогам) компании, а Bot реализуется классом HttpBot, который выполняет HTTP-вызов API. Мы узнаем детали реализации только после того, как перейдем к классам нижнего уровня.

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

Что касается непосредственно кода, то разработчик, реализовавший этот класс, последовал рекомендации сначала реализовать код более высокого уровня (или, другими словами, следовать подходу «сверху вниз», а не «снизу вверх»). Таким образом, он сделал следующее:

  1. Начал писать весь класс MessageSender, не обращая внимания на детали.
  2. В какой-то момент понадобился список сообщений для отправки. MessageRepository уже существовал, поэтому разработчик добавил новый метод в интерфейс.
  3. Разработчику нужно было получить идентификатор пользователя по его электронной почте. Это был первый случай, когда требовалась такая информация. Разработчик создал интерфейс UserDirectory и продолжил работу с MessageSender.
  4. Пришло время отправить сообщение боту. Это также был первый раз, когда потребовался бот, поэтому был написан интерфейс Bot.
  5. После того как MessageSender был завершен, разработчик переключился на классы нижнего уровня.
  6. Он реализовал функцию getMessagesToBeSent() в репозитории.
  7. Разработчик провел исследование и узнал, что идентификатор пользователя должен поступать с сервера LDAP. Он нашел библиотеку, взаимодействующую с сервером LDAP, и написал класс.
  8. Разработчик прочитал документацию к инструменту чата и понял, что простого HTTP-поста с сообщением будет достаточно. Затем он написал код.


Как видите, начав с кода более высокого уровня, разработчик смог реализовать всю бизнес-логику, не перескакивая с общего на частное. Затем оставалось только написать классы нижнего уровня. Отличная работа!

Более подробно с книгой можно ознакомиться на сайте издательства:

» Оглавление
» Отрывок

По факту оплаты бумажной версии книги на e-mail высылается электронная книга.
Для Хаброжителей скидка 25% по купону — Код

© Habrahabr.ru