Электронная библиотека » Роберт Мартин » » онлайн чтение - страница 5


  • Текст добавлен: 28 мая 2019, 13:20


Автор книги: Роберт Мартин


Жанр: Программирование, Компьютеры


Возрастные ограничения: +16

сообщить о неприемлемом содержимом

Текущая страница: 5 (всего у книги 24 страниц) [доступный отрывок для чтения: 6 страниц]

Шрифт:
- 100% +
Регистрация событий

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

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

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

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

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

Эта идея положена в основу технологии регистрации событий (event sourcing)[19]19
  Спасибо Грегу Янгу, что объяснил мне суть этого понятия.


[Закрыть]
. Регистрация событий (event sourcing) – это стратегия, согласно которой сохраняются транзакции, а не состояние. Когда требуется получить состояние, мы просто применяем все транзакции с самого начала.

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

Теперь представьте хранилище данных, которое потребуется для поддержки этой схемы: оно должно быть о-о-очень большим. В настоящее время объемы хранилищ данных растут так быстро, что, например, триллион байтов мы уже не считаем большим объемом – то есть нам понадобится намного, намного больше.

Что особенно важно, никакая информация не удаляется из такого хранилища и не изменяется. Как следствие, от набора CRUD-операций[20]20
  CRUD – аббревиатура, обозначающая набор основных операций с данными: Create (создание), Read (чтение), Update (изменение) и Delete (удаление). – Примеч. пер.


[Закрыть]
в приложениях остаются только CR. Также отсутствие операций изменения и/или удаления с хранилищем устраняет любые проблемы конкурирующих обновлений.

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

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

Заключение

Итак:

• Структурное программирование накладывает ограничение на прямую передачу управления.

• Объектно-ориентированное программирование накладывает ограничение на косвенную передачу управления.

• Функциональное программирование накладывает ограничение на присваивание.

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

Фактически последние полвека мы учились тому, как не надо делать.

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

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

III. Принципы дизайна

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

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

Цель принципов – создать программные структуры среднего уровня, которые:

• терпимы к изменениям;

• просты и понятны;

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

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

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

Принципы SOLID имеют долгую историю. Я начал собирать их в конце 1980-х годов, обсуждая принципы проектирования программного обеспечения с другими пользователями USENET (ранняя разновидность Facebook). На протяжении многих лет принципы смещались и изменялись. Некоторые исчезали. Другие объединялись. А какие-то добавлялись. В окончательном виде они были сформулированы в начале 2000-х годов, хотя и в другом порядке, чем я представлял.

В 2004 году или около того Майкл Физерс прислал мне электронное письмо, в котором сообщил, что если переупорядочить мои принципы, из их первых букв можно составить слово SOLID[21]21
  В данном случае слово «SOLID» можно перевести как «прочный», «надежный», «основательный». – Примеч. пер.


[Закрыть]
– так появились принципы SOLID.

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

 SRP: Single Responsibility Principle – принцип единственной ответственности.

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

 OCP: Open-Closed Principle – принцип открытости/закрытости.

Этот принцип был сформулирован Бертраном Мейером в 1980-х годах. Суть его сводится к следующему: простая для изменения система должна предусматривать простую возможность изменения ее поведения добавлением нового, но не изменением существующего кода.

 LSP: Liskov Substitution Principle – принцип подстановки Барбары Лисков.

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

 ISP: Interface Segregation Principle – принцип разделения интерфейсов.

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

 DIP: Dependency Inversion Principle – принцип инверсии зависимости.

Код, реализующий высокоуровневую политику, не должен зависеть от кода, реализующего низкоуровневые детали. Напротив, детали должны зависеть от политики.

Эти принципы детально описаны во множестве публикаций[22]22
  Например, Agile Software Development, Principles, Patterns, and Practices, Robert C. Martin, Prentice Hall, 2002 (Роберт С. Мартин. Гибкая разработка программ на Java и C++: принципы, паттерны и методики. М.: Вильямс, 2017. – Примеч. пер.), http://www.butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod и https://en.wikipedia.org/wiki/SOLID_(object-oriented_design) (можно просто выполнить поиск в Google по слову «SOLID»).


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

7. Принцип единственной ответственности


Из всех принципов SOLID наиболее трудно понимаемым является принцип единственной ответственности (Single Responsibility Principle, SRP). Это, вероятно, обусловлено выбором названия, недостаточно точно соответствующего сути. Услышав это название, многие программисты решают: оно означает, что каждый модуль должен отвечать за что-то одно.

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

Традиционно принцип единственной ответственности описывался так:

Модуль должен иметь одну и только одну причину для изменения.

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

Модуль должен отвечать за одного и только за одного пользователя или заинтересованное лицо.

К сожалению, слова «пользователь» и «заинтересованное лицо» не совсем правильно использовать здесь, потому что одного и того же изменения системы могут желать несколько пользователей или заинтересованных лиц. Более правильным выглядит понятие группы, состоящей из одного или нескольких лиц, желающих данного изменения. Мы будем называть такие группы акторами (actor).

Соответственно, окончательная версия принципа единственной ответственности выглядит так:

Модуль должен отвечать за одного и только за одного актора.

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

Слово «связный» подразумевает принцип единственной ответственности. Связность – это сила, которая связывает код, ответственный за единственного актора.

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

Признак 1: непреднамеренное дублирование

Мой любимый пример – класс Employee из приложения платежной ведомости. Он имеет три метода: calculatePay(), reportHours() и save() (рис. 7.1).


Рис. 7.1. Класс Employee


Этот класс нарушает принцип единственной ответственности, потому что три его метода отвечают за три разных актора.

• Реализация метода calculatePay() определяется бухгалтерией.

• Реализация метода reportHours() определяется и используется отделом по работе с персоналом.

• Реализация метода save() определяется администраторами баз данных.

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

Например, представьте, что функции calculatePay() и reportHours() используют общий алгоритм расчета не сверхурочных часов. Представьте также, что разработчики, старающиеся не дублировать код, поместили реализацию этого алгоритма в функцию с именем regularHours() (рис. 7.2).


Рис. 7.2. Общий алгоритм


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

Разработчик, которому было поручено внести изменение, заметил, что функция regularHours() вызывается методом calculatePay(), но, к сожалению, не заметил, что она также вызывается методом reportHours().

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

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

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

Признак 2: слияния

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

Например, представим, что коллектив администраторов баз данных решил внести простое исправление в схему таблицы Employee. Представим также, что сотрудники отдела по работе с персоналом пожелали немного изменить формат отчета, возвращаемого функцией reportHours().

Два разных разработчика, возможно, из двух разных команд, извлекли класс Employee из репозитория и внесли изменения. К сожалению, их изменения оказались несовместимыми. В результате потребовалось выполнить слияние.

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

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

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

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

Решения

Существует много решений этой проблемы. Но каждое связано с перемещением функций в разные классы.

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

Недостаток такого решения – разработчик теперь должен создавать экземпляры трех классов и следить за ними. Эта проблема часто решается применением шаблона проектирования «Фасад» (Facade), как показано на рис. 7.4.

Класс EmployeeFacade содержит очень немного кода и отвечает за создание экземпляров трех классов и делегирование вызовов методов.


Рис. 7.3. Три класса, не зависящих друг от друга


Рис. 7.4. Шаблон «Фасад»


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


Рис. 7.5. Наиболее важные методы остаются в оригинальном классе Employee и используются как фасад для низкоуровневых функций


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

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

Заключение

Принцип единственной ответственности (Single Responsibility Principle; SRP) касается функций и классов, но он проявляется в разных формах на еще двух более высоких уровнях. На уровне компонентов он превращается в принцип согласованного изменения (Common Closure Principle; CCP), а на архитектурном уровне – в принцип оси изменения (Axis of Change), отвечающий за создание архитектурных границ. Все эти идеи мы обсудим в последующих главах.

8. Принцип открытости/закрытости


Принцип открытости/закрытости (Open-Closed Principle; OCP) был сформулирован Бертраном Мейером в 1988 году[23]23
  Bertrand Meyer. Object Oriented Software Construction, Prentice Hall, 1988, p. 23 (Бертран Мейер. Объектно-ориентированное конструирование программных систем. Русская редакция, 2005. – Примеч. пер.).


[Закрыть]
. Он гласит:

Программные сущности должны быть открыты для расширения и закрыты для изменения.

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

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

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

Увидеть это поможет простой мысленный эксперимент.

Мысленный эксперимент

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

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

Очевидно, что для этого придется написать новый код. Но как много старого кода придется изменить?

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

Как? Правильно разделяя сущности, которые изменяются по разным причинам (принцип единственной ответственности), и затем правильно организуя зависимости между этими сущностями (принцип инверсии зависимостей).

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


Рис. 8.1. Результат применения принципа единственной ответственности


Самое важное, что нужно понять, – в данном примере в создание отчета вовлечены две отдельные ответственности: вычисление данных для отчета и представление этих данных в форме веб-отчета или распечатанного отчета.

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

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

Классы, отмеченные символами <I>, – это интерфейсы; отмеченные символами <DS> – это структуры данных (data structures). Простые стрелки соответствуют отношениям использования. Стрелки с треугольным наконечником соответствуют отношениям реализации или наследования.

Первое, на что следует обратить внимание, – все зависимости определены на уровне исходного кода. Стрелка, направленная от класса A к классу B, означает, что в исходном коде класса A упоминается имя класса B, но в коде класса B не упоминается имя класса A. Так, на рис. 8.2 диспетчер финансовых данных знает о существовании шлюза через отношение реализации, а шлюз финансовых данных ничего не знает о диспетчере.


Рис. 8.2. Выделение процессов в классы и выделение классов в компоненты


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

Позволю себе повторить еще раз: если компонент A требуется защитить от изменений в компоненте B, компонент B должен зависеть от компонента A.

Нам нужно защитить контроллер от изменений в презентаторах. Нам нужно защитить презентаторы от изменений в представлениях. Нам нужно защитить интерактор от изменений в… во всех остальных компонентах.

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


Рис. 8.3. Отношения компонентов однонаправленны


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

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

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

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


Страницы книги >> Предыдущая | 1 2 3 4 5 6 | Следующая
  • 3.6 Оценок: 5

Правообладателям!

Данное произведение размещено по согласованию с ООО "ЛитРес" (20% исходного текста). Если размещение книги нарушает чьи-либо права, то сообщите об этом.

Читателям!

Оплатили, но не знаете что делать дальше?


Популярные книги за неделю


Рекомендации