Гид Java-разработчика : проектно-ориентированный подход [Рауль-Габриэль Урма] (pdf) читать онлайн

Книга в формате pdf! Изображения и текст могут не отображаться!


 [Настройки текста]  [Cбросить фильтры]

МИРОВОЙ КОМПЬЮТЕРНЫЙ БЕСТСЕЛЛЕР

O'REILLY

Raoul-Gabriel Urma
Richard Warburton

Real-World
Software
Development
A Project-Driven Guide
to Fundamentals in Java

O’REILLY

Рауль-Габриэль Урма

Ричард Уорбертон

ГИД JAVA
разработчика
Проектно-ориентированный подход

& БОМБОРА
ИЗДАТЕЛЬСТВО

Москва 2022

УДК 004.43
ББК 32.973.26-018.1
У69

Real-World Software Development
Raoul-Gabriel Urma and Richard Warburton

© 2021 Eksmo Publishing Company Authorized Russian translation of the English edition
of Real-World Software Development ISBN 9781491967171
© 2020 Functor Ltd. and Monotonic Ltd. This translation is published
and sold by permission of O’Reilly Media, Inc., which owns or controls all rights
to publish and sell the same.

У69

Урма, Рауль-Габриэль.
Гид Java-разработчика : проектно-ориентированный подход / РаульГабриэль Урма, Ричард Уорбертон ; [перевод с английского М. А. Райтман]. — Москва : Эксмо, 2022. — 224 с.: ил. — (Мировой компьютерный
бестселлер).
ISBN 978-5-04-094955-7

На примере реальных проектов авторы разбирают все наиболее популярные
приемы объектно-ориентированного программирования, такие как разработка
через тестирование или функциональное программирование. В этом руковод­
стве представлен проектно-ориентированный подход к разработке программно­
го обеспечения на языке Java, позволяющий освоить ключевые навыки, необхо­
димые каждому эффективному программисту.
УДК 004.43
ББК 32.973.26-018.1

ISBN 978-5-04-094955-7

© Райтман М.А., перевод на русский язык, 2022
© Оформление. ООО «Издательство «Эксмо», 2022

ОГЛАВЛЕНИЕ

https://t.me/it_boooks
Предисловие ......................................................................................................
Почему мы написали эту книгу ...............................................................
Подход, ориентированный на разработчика ........................................
Что в этой книге? .......................................................................................
Для кого эта книга? ....................................................................................
Условные обозначения, используемые в книге ....................................
Использование примеров кода ................................................................

11
11
12
12
13
14
15

Глава 1. Начало путешествия ........................................................................
Темы .............................................................................................................
Особенности Java .................................................................................
Разработка программного обеспечения и архитектура ................
SOLID ....................................................................................................
Тестирование ........................................................................................
Структура глав ...........................................................................................
Самостоятельная работа ...........................................................................

16
16
16
17
17
18
18
20

Глава 2. Анализатор банковских операций ...............................................
Задача ...........................................................................................................
Цель ..............................................................................................................
Требования к анализатору банковских операций ................................
Принцип KISS .............................................................................................
Переменные final ..................................................................................
Обслуживаемость кода и антишаблоны ................................................
Класс-бог ...............................................................................................
Дублирование кода ..............................................................................
Принцип единственной ответственности .............................................
Связность .....................................................................................................
Внутриклассовая связность ...............................................................
Функциональная ..............................................................
Информационная .............................................................

21
21
21
22
22
25
25
26
26
27
32
35
36
36

Служебная ........................................................................
Логическая ........................................................................
Последовательная ............................................................
Временная .........................................................................
Связность методов .............................................................................
Связанность ...............................................................................................
Тестирование .............................................................................................
Автоматизированное тестирование ................................................
Доверие ..............................................................................
Устойчивость к изменениям ..........................................
Понимание программы ...................................................
Использование JUnit ...........................................................................
Объявление метода теста ................................................
Операторы контроля .......................................................
Покрытие кода .....................................................................................
Выводы ........................................................................................................
Самостоятельная работа ..........................................................................
В завершение ...............................................................................................

37
37
38
39
39
40
42
43
43
43
44
44
44
46
47
48
49
49

Глава 3. Расширяем анализатор банковских операций ..........................
Задача ...........................................................................................................
Цель ..............................................................................................................
Требования к расширенному анализатору банковских операций ....
Принцип открытости/закрытости .........................................................
Создание экземпляра функционального интерфейса ...................
Лямбда-выражения .............................................................................
Подводные камни интерфейсов ..............................................................
Интерфейс-бог .....................................................................................
Слишком мизерный ............................................................................
Явный API против неявного ....................................................................
Доменный класс или примитив? .......................................................
Множественный экспорт ..........................................................................
Знакомство с доменным объектом ..................................................
Объявление и реализация соответствующего интерфейса .........
Обработка исключений ............................................................................
Для чего нужны исключения? ...........................................................
Шаблоны и антишаблоны для исключений ....................................
Выбор между проверяемыми и непроверяемыми
исключениями ..................................................................
Слишком специфические ................................................

50
50
50
51
51
55
55
56
57
58
59
61
62
62
64
65
66
68
68
69

Слишком однообразные ...................................................
Шаблон уведомления .......................................................
Методика применения исключений .................................................
Не игнорируйте исключение ..........................................
Не перехватывайте «общие» исключения ....................
Документируйте исключения ........................................
Будьте осторожны с исключениями, связанными
с конкретной реализацией ..............................................
Исключения против управляющего потока ................
Альтернативы исключениям ..............................................................
Использование null ..........................................................
Шаблон null-объекта .......................................................
Optional .........................................................................
Тгу ..................................................................................
Использование сборщиков .......................................................................
Зачем нужны сборщики .....................................................................
Работа с Maven .....................................................................................
Структура проекта ............................................................
Пример сборочного файла .............................................
Команды Maven ................................................................
Использование Gradle .........................................................................
Пример сборочного файла .............................................
Команды Gradle ................................................................
Выводы ........................................................................................................
Самостоятельная работа ...........................................................................
В завершение ...............................................................................................

71
72
74
74
74
74

Глава 4. Система управления документами ...............................................
Задача ...........................................................................................................
Цель ..............................................................................................................
Требования к системе управления документами .................................
Воплощение идеи .......................................................................................
Импортеры ...........................................................................................
Класс Document ......................................................................................
Атрибуты и иерархия Documents ........................................................
Реализация и регистрация импортеров ...........................................
Принцип подстановки Дисков ................................................................
Альтернативные подходы .........................................................................
Поместить импортер в класс .............................................................
Область действия и инкапсуляция ..................................................

87
87
87
88
89
90
91
94
95
97
99
100
100

75
75
76
76
76
77
77
77
77
78
79
80
81
82
83
84
84
85
86

Расширение и повторное использование кода ...................................
Гигиена тестов ....... ...................................................................................
Именование тестов ............................................................................
Поведение, а не реализация .............................................................
Не повторяйтесь ................................................................................
Хорошая диагностика .......................................................................
Тестирование ошибочных ситуаций ..............................................
Константы ...........................................................................................
Выводы ......................................................................................................
Самостоятельная работа .........................................................................
В завершение .............................................................................................

101
107
108
110
112
113
116
117
118
118
118

Глава 5. Движок бизнес-правил .................................................................
Задача .........................................................................................................
Цель ............................................................................................................
Требования к движку бизнес-правил ...................................................
Разработка через тестирование .............................................................
Зачем нужен TDD? .............................................................................
Цикл TDD ...........................................................................................
Мокинг ................................................................................................
Добавление условий ................................................................................
Моделирование состояния ...............................................................
Вывод типа локальной переменной ................................................
Switch-выражения ..............................................................................
Принцип разделения интерфейса ..................................................
Разработка текучего интерфейса (Fluent API) ....................................
Что такое Fluent API? ........................................................................
Моделирование домена ....................................................................
Шаблон Builder ...................................................................................
Выводы ......................................................................................................
Самостоятельная работа .........................................................................
В завершение .............................................................................................

119
119
119
120
121
122
123
125
127
127
131
132
135
139
139
139
141
144
145
145

Глава 6. Twootr ................................................................................................
Задача .........................................................................................................
Цель ............................................................................................................
Требования к Twootr ...............................................................................
Обзор разработки ....................................................................................
Технология Pull ..................................................................................
Технология Push ................................................................................

146
146
146
147
148
149
149

От событий к разработке ........................................................................
Связь ....................................................................................................
GUI — графический интерфейс пользователя .............................
Продолжаем ........................................................................................
Гексагональная архитектура ............................................................
С чего начать .............................................................................................
Пароли и безопасность ............................................................................
Подписчики и твуты ................................................................................
Моделирование ошибок ....................................................................
Твутинг ................................................................................................
Создание моков ..................................................................................
Верификация при помощи моков ....................................................
Библиотеки для мокинга ...................................................................
SenderEndPoint ....................................................................................
Позиции .....................................................................................................
Методы equals и hashcode .................................................................
Взаимосвязь между equals и hashCode ............................................
Выводы .......................................................................................................
Самостоятельная работа .........................................................................
В завершение .............................................................................................

150
151
152
153
153
155
160
162
163
166
167
168
169
170
172
176
177
179
179
179

Глава 7. Расширение Twootr ........................................................................
Задача .........................................................................................................
Цель ............................................................................................................
Резюме ........................................................................................................
Живучесть и шаблон «Репозиторий» ....................................................
Проектирование репозиториев .......................................................
Объекты-запросы ..............................................................................
Функциональное программирование ...................................................
Лямбда-выражения ...........................................................................
Ссылки на методы .............................................................................
Execute Around ...................................................................................
Потоки .................................................................................................
map () ..................................................................................
forEachf) ...........................................................................
filter!) ...............................................................................
reduced .............................................................................
Optional ................................................................................................
Пользовательский интерфейс ................................................................
Инверсия зависимости и внедрение зависимости .............................

180
180
180
181
181
182
184
189
190
192
193
195
195
196
196
198
200
203
204

Пакеты и сборочные системы ...............................................................
Ограничения и упрощения ....................................................................
Выводы ......................................................................................................
Самостоятельная работа ........................................................................
В завершение .............................................................................................

207
209
210
210
211

Глава 8. Заключение ......................................................................................
Проектно-ориентированная структура ...............................................
Самостоятельная работа .........................................................................
Сознательная практика ..........................................................................
Следующие шаги и дополнительные ресурсы ....................................

212
212
212
213
214

Об авторах ....................................................................................................... 216
В завершение ............................................................................................. 217
Предметный указатель ................................................................................ 218

Предисловие

Совершенствование навыков разработки программного обеспечения вклю­
чает в себя изучение целого пула разрозненных концепций. Если вы — на­
чинающий разработчик или даже имеете определенный опыт, это может
показаться непреодолимо сложной задачей. Стоит ли потратить время на
изучение основных положений объектно-ориентированного мира, таких
как принципы SOLID, шаблоны проектирования или разработка через тес­
тирование? Нужно ли применять такие набирающие популярность техноло­
гии, как, например, функциональное программирование?

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

Почему мы написали эту книгу
За многие годы обучения программированию мы накопили богатый опыт.
Мы оба написали книги по Java 8 и проводим курсы по профессиональной
разработке программного обеспечения. В процессе мы были признаны JavaПредисловие

|

11

чемпионами и стали востребованными спикерами на различных междуна­
родных конференциях.

За все это время мы поняли, что многим разработчикам достаточно лишь
введения или краткого описания некоторых основных тем. Шаблоны про­
ектирования, функциональное программирование, принципы SOLID и тес­
тирование — это методы, которые хороши сами по себе, но редко можно
встретить демонстрацию того, как соединить их вместе. Иногда програм­
мисты перестают развивать свои навыки лишь потому, что не могут опреде­
литься, что же изучать дальше. Мы хотим не просто дать вам определенные
базовые навыки, наша цель — сделать обучение легким, интересным и даже
веселым.

Подход, ориентированный на разработчика
Данная книга предоставляет возможность познакомиться с ориентиро­
ванным на разработчика подходом. В ней вы найдете множество примеров
кода. В каждой теме рассматриваются реальные программы и проекты. Все
они даются в полном объеме, так что на каждом этапе вы можете прове­
рять код в своей интегрированной среде разработки (Integrated Development
Environment, IDE) и запускать программы, чтобы оценить их в действии.

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

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

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

12

|

Предисловие

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

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

Для кого эта книга?
Мы уверены, что разработчики из самых разных областей найдут для себя
что-то полезное и интересное в этой книге. Среди них будут и те, кто сможет
использовать ее максимально эффективно.
На наш взгляд, основная аудитория книги — это начинающие разработчи­
ки, только окончившие университет, либо имеющие за плечами пару лет
опыта работы. Мы познакомим вас с основными темами, которые, как мы
считаем, пригодятся вам в работе. Для взаимодействия с данной книгой не
нужно иметь ученую степень, но для лучшего усвоения материала необхо­
димо обладать базовыми знаниями в программировании. К примеру, здесь
мы не будем объяснять, что такое оператор if или циклы.

Для того чтобы приступить, вам необязательно иметь глубокие познания
в объектно-ориентированнОлМ или функциональном программировании.

Предисловие

|

13

Например, в главе 2 от вас не потребуется ничего сверх знания о том, что та­
кое класс, и умения работать с базовыми типами (такими как List).
Только самые основы.
Данная книга может также представлять интерес и для разработчиков, при­
шедших в Java из других языков программирования, таких как С#, C++ или
Python. Она поможет вам быстро освоить необходимые конструкции языка,
принципы, методы и ключевые моменты, необходимые для написания хо­
рошего кода на Java.

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

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

Условные обозначения, используемые в книге
В книге используются следующие типографские условные обозначения:
Курсив

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

Моноширинный полужирный
Обозначает команды или другой текст, который должен быть введен поль­
зователем.

Моноширинный курсивный
Обозначает текст, который должен быть заменен пользовательскими значе­
ниями или значениями, уточняемыми в контексте.
14

|

Предисловие

Данный символ обозначает примечание.

Использование примеров кода
Дополнительные материалы (такие как примеры программ, упражнения
и т. д.) доступны для скачивания на https://github.com/Iteratr-Learning/RealWorld-Software-Development.

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

ГЛАВА 1

Начало путешествия

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

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

Особенности Java
Структурирование кода при помощи классов и интерфейсов обсужда­
ется в главе 2. Исключения и пакеты рассматриваются в главе 3, из ко­
торой вы также узнаете о лямбда-выражениях. Глава 5 познакомит вас
с выводом типа переменной и switch-выражениями. И, наконец, в главе 7

16

|

Глава 1

уже более детально рассмотриваются лямбда-выражения и ссылки на
методы.
Большое количество программных продуктов написаны на Java, поэтому
очень важно знать особенности этого языка и понимать, как он работает.
Многие из этих особенностей характерны и для других языков програм­
мирования, таких как С#, C++, Ruby или Python. Несмотря на то что пе­
речисленные языки имеют различия, понимание того, как использовать
классы и основные принципы ООП, будут полезны при работе с любым
из них.

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

В главе 2 представлены концепции связанности и связности. Шаблон
уведомления рассматривается в главе 3. Как построить «дружелюбный»
Fluent API и шаблон Builder, описано в главе 5. В главе 6 рассматривается
концепция «целостной картины» событийно-ориентированной и гекса­
гональной архитектур, а в главе 7 представлен шаблон Repository. Нако­
нец, также в главе 7 вы познакомитесь с функциональным программи­
рованием.

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

Начало путешествия

|

17

Итак, принципы SOLID и главы, в которых они рассматриваются:



Принцип единственной ответственности (SRP — Single Resposibility
Principle) — глава 2.



Принцип открытости/закрытости (ОСР — Open/Closed Principle) —
глава 3.



Принцип подстановки Барбары Дисков (LSP — Liskov Substitution Prin­
ciple) — глава 4.



Принцип разделения интерфейса (ISP — Interface Segregation Princi­
ple) — глава 5.



Принцип инверсии зависимостей (DIP — Dependency Inversion Princi­
ple) — глава 7.

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

Базовые знания о создании тестов вы получите в главах 2 и 4. В главе 5 мы по­
говорим о разработке через тестирование (TDD — Test-driven Development).
И наконец, в главе 6 мы рассмотрим применение «тестовых двойников»
(дублеров), включая Mocks и Stubs.

Структура глав
Краткое содержание глав.
Глава 2. Анализатор банковских операций

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

В этой главе вы узнаете, как можно доработать код, написанный в главе 2,
добавив дополнительные возможности, использовав шаблон Strategy Design,
18

|

Глава 1

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

Глава 4. Система управления документами
В этой главе мы поможем одному успешному доктору систематизировать
карточки пациентов. Вы познакомитесь с наследованием, с принципом под­
становки Барбары Дисков и компромиссом между созданием и наследова­
нием. Вы также узнаете, как создавать более надежное программное обеспе­
чение за счет применения автоматизированного тестирования.

Глава 5. Движок бизнес-правил
Вы узнаете о создании базового механизма бизнес-правил — гибкого и про­
стого в обслуживании способа определения бизнес-логики. Эта глава по­
знакомит вас с разработкой через тестирование, разработкой Fluent API
и принципом разделения интерфейсов.
Глава 6, Twootr

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

Глава 7. Расширяем Twootr
В последней главе, посвященной проектам, мы займемся развитием плат­
формы Twootr. В этой главе объясняется принцип инверсии зависимостей,
а также представлены архитектурные решения «целостной картины» вроде
событийно-ориентированной или гексагональной архитектуры. Вы сможе­
те расширить свои знания в области автоматизированного тестирования за
счет изучения «тестовых двойников» и техник функционального програм­
мирования.

Глава 8. Заключение
В финальной главе кратко рассматриваются основные темы и концепции,
изложенные в книге. Читателю предлагаются дополнительные ресурсы, ко­
торые могут пригодиться ему в дальнейшей работе.

Начало путешествия

|

19

Самостоятельная работа
Будучи разработчиком, вы должны подходить к проектам итеративно. Что
это значит? Выберите для себя одну или две наиболее приоритетные задачи,
реализуйте их, а затем, используя обратную связь и отзывы, определите сле­
дующие задачи. На наш взгляд, это отличный способ оценить свои навыки.
В конце каждой главы вы найдете небольшой раздел под названием «Само­
стоятельная работа», содержащий ряд предложений, которые могут помочь
вам лучше усвоить пройденный материал.
Теперь, когда вы знаете, чего ожидать от этой книги, приступим к работе!

ГЛАВА 2

Анализатор банковских операций

Задача
В компании FinTech сейчас реально жарко. Марк Эрбергцук понимает, что
тратит много денег на различные закупки, и хочет, чтобы все его расходы
подсчитывались автоматически. Конечно, он получает ежемесячные отчеты
из банка, но они его не устраивают. Поэтому он просит вас разработать про­
граммный продукт, который мог бы автоматически вести учет всех его бан­
ковских операций, чтобы иметь возможность лучше контролировать свои
финансы. Задание принято!

Цель
В этой главе вы познакомитесь с основами «правильной» разработки про­
граммного обеспечения, прежде чем пойти дальше и приступить к изуче­
нию более продвинутых технологий в следующих главах.
Вы начнете с реализации решения задачи при помощи одного класса. Затем
узнаете, в чем заключаются недостатки такого подхода с точки зрения адап­
тации к меняющимся требованиям и обслуживания проекта.
Но не переживайте! Вы освоите принципы и техники разработки, которые
помогут вам убедиться, что ваш код соответствует указанным критериям.
Прежде всего, вы узнаете о принципе единственной ответственности
(SRP), который позволяет разрабатывать более удобное в обслуживании
и более «прозрачное» программное обеспечение, а также снижает вероят­
ность возникновения новых багов. По пути вы также познакомитесь с та­
кими концепциями, как связность и связанность, которые помогут вам оце­
нить качество кода и разрабатываемого вами программного продукта.

Анализатор банковских операций

|

21

В этой главе мы пользуемся библиотеками и свойствами Java версии 8
и выше, включая новые библиотеки для работы с датами и временем.
Если в какой-то момент вы захотите посмотреть исходный код, вы моk жете найти его в репозитории книги /com/iteratrlearning/shu_book/

chapter_02.

Требования к анализатору банковских операций
Вы встретились с Марком Эрбергцуком, чтобы обсудить требования к про­
грамме и выпить по чашечке настоящего хипстерского латте (без сахара).
Поскольку Марк технически подкован, он объяснил вам, что анализатор
банковских операций должен просто считывать текстовый файл, в котором
содержится список транзакций. Он заранее скачал этот файл из своего ин­
тернет-банкинга. Файл выполнен в виде значений, разделенных запятыми
(Comma-Separated Values — CSV формат). Вот пример транзакций:
30-01-2017,-100,Deliveroo

30-01-2017,-50,Tesco
01-02-2017,6000,Salary
02-02-2017,2000,Royalties
02-02-2017,-4000,Rent
03-02-2017,3000,Tesco
05-02-2017,-30,Cinema

Марк пояснил, что хочет получить ответы на следующие вопросы:



Какова общая сумма начислений и списаний по списку операций? Она
отрицательная или положительная?



Сколько транзакций было в конкретном месяце?



10 самых затратных операций.



На что было потрачено больше всего денег?

Принцип KISS
Начнем с простого, а именно — с первого вопроса: «Какова общая сумма
начислений и списаний по списку операций?» Итак, вам нужно обработать
CSV-файл и посчитать сумму всех операций. Поскольку больше ничего не

22

|

Глава 2

требуется, вороятно, вы решите, что не имеет смысла создавать очень слож­
ное приложение.
Вы можете «сделать его коротким и простым» (Keep It Short and Simple —
KISS), уместив все приложение в одном классе, как показано в примере 2-1.
Обратите внимание, что пока вам не нужно задумываться об обработке ис­
ключений (например, если файл не существует или если возникла ошибка
при считывании файла). С этой темой вы познакомитесь в главе 3.

S

Формат CSV не полностью стандартизирован. Чаще всего для разделения
значений в нем используются запятые. Однако многие считают, что в ка­
честве разделителя могут выступать также и другие символы, такие как
точка с запятой или символ табуляции. Данные особенности способны
привести к усложнению программы-парсера. Поэтому в этой главе мы
будем считать, что значения разделяются только запятыми (,).

Пример 2-1. Вычисление суммы операций
public class BankTransactionAnalyzerSimple {
private static final String RESOURCES = "src/main/resources/";
public static void main(final String... args) throws IOException {
final Path path = Paths.get(RESOURCES + args[0]);
final List lines = Files.readAllLines(path) ;
double total = Od;
for(final String line: lines) {
final String[] columns = line.split(",”);
final double amount = Double.parseDouble(columns[1]);

total += amount;
}

System.out.printin("The total for all transactions is " + total);

}
}

Итак, что происходит? Вы загружаете CSV-файл, переданный приложению
в виде аргумента командной строки. Класс Path представляет собой путь
в файловой системе. Затем вы используете метод Files.readAllLines(), ко­
торый возвращает список строк. После прочтения всех строк файла можно
приступить к их разделению:

Анализатор банковских операций

|

23



Разделяем столбцы при помощи запятых.



Извлекаем сумму.



Преобразуем сумму в тип double.

После получения суммы в виде double для данной конкретной операции
можно добавить ее к общей сумме. В конце вычислений вы получаете ито­
говую сумму.
Код из примера 2-1 вполне рабочий, однако в нем упущены некоторые клю­
чевые моменты, о которых всегда стоит помнить при написании кода:



Что, если файл окажется пустым?



Что, если не получится посчитать сумму из файла из-за поврежденных
данных?



Что, если в строке пропущены какие-либо данные?

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

Что насчет решения второй задачи: «Сколько транзакций было в конкрет­
ном месяце?» Что мы можем сделать? Самое простое решение — скопиро­
вать и вставить, верно? Вы можете просто скопировать и вставить тот же
код, изменив логику так, чтобы он выбирал нужный месяц, как показано
в примере 2-2.
Пример 2-2. Вычисление суммы за январь
final Path path = Paths.get(RESOURCES + args[0]);
final List lines = Files.readAllLines(path);
double total = Od;
final DateTimeFormatter DATE_PATTERN = DateTimeFormatter.ofPattern("dd-MM-yyyy");
for(final String line: lines) {
final String[] columns = line.split (",");
final LocalDate date = LocalDate.parse(columns[0], DATE_PATTERN);
if(date.getMonth() == Month.JANUARY) {
final double amount = Double.parseDouble(columns[1]);

total += amount;

)
}

System.out.printin("The total for all transactions in January is " + total);

24

|

Глава 2

Переменные final
В качестве краткого экскурса мы объясним использование ключевого слова
final в наших примерах, тем более что мы будем пользоваться им доволь­
но часто на протяжении всей книги. Пометка final у поля или переменной
означает, что они не могут быть повторно определены. Использовать final
или нет — решение исключительно ваше и вашей команды, поскольку оно
имеет как положительные, так и отрицательные стороны. Мы для себя уста­
новили, что, помечая ключевым словом final как можно больше перемен­
ных, мы четко определяем, какое состояние жестко зафиксировано, а какое
может изменяться в процессе жизни объекта.
С другой стороны, использование final не может однозначно гарантиро­
вать неизменность объекта. Например, поле final может ссылаться на из­
меняемый объект. Подробнее о неизменяемости или иммутабельности мы
поговорим в главе 4. Забегая вперед, можно сказать, что применение final
привносит в код больше шаблонности. Некоторые команды разработчиков
прибегают к компромиссной позиции, используя final в параметрах мето­
дов, чтобы убедиться, что они точно не переопределены и не являются ло­
кальными переменными.
Также не имеет особого смысла использовать ключевое слово final, хотя
Java это допускает, в параметрах абстрактных методов. Например, в интер­
фейсах. Причина — в отсутствии тела метода. Вероятно, использование
final сильно сократилось после введения ключевого слова var в Java 10. Мы
обсудим это позже в примере 5-15.

Обслуживаемость кода и антишаблоны
Как вы считаете, «копипаст», к которому мы прибегли в примере 2-2, — это
хороший подход? Настало время сделать шаг назад и подумать об этом.
Когда вы программируете, ваша задача — постараться сделать код хорошо
обслуживаемым. Что это значит? Лучше всего объяснить это при помощи
списка свойств, которыми должен обладать ваш код.



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



Должно быть понятно, как код работает.



Должно быть просто убрать или добавить новую опцию.

Анализатор банковских операций

|

25



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

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

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


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



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

Давайте рассмотрим эти два антишаблона более подробно.

Класс-бог
Поместив весь свой код в одном файле, вы придете к одному огромному клас­
су, с которым невероятно сложно разобраться, потому что он делает абсо­
лютно все! Если вам понадобится обновить логику существующего кода (на­
пример, изменить алгоритм синтаксического анализа — парсинга — слов),
вы не сможете оперативно найти нужный участок кода и внести изменения.
Данная проблема вызвана использованием антишаблона под названием
«класс-бог». По сути, у вас есть класс, который делает все. Такой практики,
безусловно, следует избегать. В следующем разделе вы познакомитесь с прин­
ципом единственной ответственности, который является «проводником»
в разработке простого для понимания и обслуживаемого кода.

Дублирование кода
Для каждой задачи вы дублируете алгоритм чтения и анализа входных дан­
ных. А что если формат входных данных изменится с CSV на JSON? Что
если понадобится поддержка нескольких форматов? Добавление этой опции
26

|

Глава 2

станет сущим адом, так как ваш код перегружен одним и тем же решением,
продублированным много раз в разных местах. Таким образом, вам придет­
ся вносить изменения везде, повышая вероятность возникновения багов.
Bbi будете часто слышать о принципе «Не повторяйся» (DRY — Don’t
Repeat Yourself). Его идея заключается в том, что, если успешно избегать
дублирования кода, внесение изменений в логику приложения не потре­
бует многочисленных изменений в коде.

S

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

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

Принцип единственной ответственности
Принцип единственной ответственности (SRP) — главный принцип, ко­
торого стоит придерживаться при разработке программного обеспечения,
если вы хотите создавать простой и удобный в обслуживании код.

Данный принцип можно охарактеризовать при помощи двух утверждений:


Один класс несет ответственность за одну функцию приложения.



Никогда не должно быть больше одной причины для внесения измене­
ний в класс1.

Обычно SRP применяется для классов и методов. Он связан с одной кон­
кретной концепцией, категорией или с каким-то определенным поведением.
Благодаря данному принципу создается более устойчивый код, поскольку
1 Это определение введено Робертом Мартином. — Прим. авт.

Анализатор банковских операций

|

27

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

Как же нам применить SRP в коде, приведенном в примере 2-2? Очевидно,
что главный класс выполняет сразу несколько задач, которые можно раз­
бить на подзадачи:

1.

Считывание входных данных.

2.

Синтаксический анализ данных в заданном формате.

3.

Обработка результата.

4.

Выдача результатов суммирования.

В этой главе мы сфокусируем свое внимание на синтаксическом анализе
данных. В следующей главе рассмотрим, как модернизировать анализатор
банковских операций таким образом, чтобы приложение стало полностью
модульным.
Вернемся к нашему примеру. В первую очередь необходимо создать отдель­
ный класс, предназначенный для извлечения данных из CSV-файла. Таким
образом, вы сможете многократно использовать его в различных задачах.
Давайте назовем его BankStatementCSVParser, чтобы по названию сразу по­
нимать его предназначение (пример 2-3).
Пример 2-3, Помещаем алгоритм парсинга в отдельный класс
public class BankStatementCSVParser (

private static final DateTimeFormatter DATE_PATTERN

--= DateTimeFormatter. of Pattern ("dd-MM-yyyy") ;
private BankTransaction parseFromCSV(final String line) {
final String[] columns = line.split(",");

final LocalDate date = LocalDate.parse(columns[0], DATE_PATTERN);
final double amount = Double.parseDouble(columns[1]);
final String description = columns[2];
return new BankTransaction(date, amount, description);

)

28

|

Глава 2

public List parseLinesFromCSV(final List lines) {
final List bankTransactions = new ArrayListo();
for(final String line: lines) {

bankTransactions.add(parseFromCSV(line));

)
return bankTransactions;

)
}

Как видите, класс BankStatementCSVParser объявляет два метода:
parseFromCSV () и parseLinesFromCSV (), которые создают объекты
BankTransaction, являющиеся экземплярами доменного класса, моделирую­
щего банковские операции (смотрите его объявление в примере 2-4).
Что значит доменный! Это означает использование слов и терминологии,
соответствующих решаемой бизнес-задаче.

Класс BankTransaction очень полезен, так как различные части нашего при­
ложения могут иметь одинаковое представление о том, что такое банков­
ская операция. Класс реализует методы equals и hashcode. Назначение этих
методов и правила их использования описаны в главе 6.

Пример 2-4, Доменный класс для банковских операций
public class BankTransaction (
private final LocalDate date;
private final double amount;
private final String description;
public BankTransaction(final LocalDate date, final double amount, final String
description) {
this.date = date;
this.amount = amount;
this.description = description;
)

public LocalDate getDateO {
return date;

)

Анализатор банковских операций

|

29

public double getAmountO {
return amount;

}
public String getDescription() {
return description;

}

(^Override
public String toStringO (
return '’BankTransaction!" +

"date=" + date +
", amount=" + amount +
", description^" + description + '\" +

^Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClassO != o.getClass()) return false;

BankTransaction that = (BankTransaction) o;
return Double.compare(that.amount, amount) == 0 &&
date.equals(that.date) &&
description.equals(that.description);

}
@0vernde
public int hashcode () {
return Objects.hash(date, amount, description);

Теперь вы можете изменить приложение, чтобы использовать свой класс
BankStatementCSVParser, а точнее, его метод parseLinesFromCSVO, как пока­
зано в примере 2-5.

Пример 2-5. Использование CSV-napcepa
final BankStatementCSVParser bankStatementParser = new BankTransactionCSVParser();

final String fileName = args[0];

30

|

Глава 2

final Path path = Paths.get(RESOURCES + fileName);
final List lines = Files.readAllLines(path);

final List bankTransactions

= bankStatementParser.parseLinesFromCSV(lines) ;

System.out.printin("The total for all transactions is " + calculateTotalAmount(bank
Transactions));
System.out.printin("Transactions in January " + selectlnMonth(BankTransactions,
Month.JANUARY));

Таким образом, вам больше не нужно знать для решения различных за­
дач, как именно работает функция парсинга. Достаточно просто исполь­
зовать объекты BankTransaction, чтобы получить требуемую информа­
цию. В примере 2-6 показано, как объявить методы calculateTotalAmount ()
и selectlnMonth)), предназначенные для обработки списка транзакций
и выдачи соответствующего результата. В главе 3 вы познакомитесь с лямб­
да-выражениями и Streams API, которые помогут вам в дальнейшем сделать
ваш код более аккуратным.
Пример 2-6. Обработка списков транзакций
public static double calculateTotalAmount(final List

bankTransactions) (
double total = Od;
for(final BankTransaction bankTransaction: bankTransactions) {
total += bankTransaction.getAmount ();
)
return total;
}
public static List selectlnMonth(final List
bankTransactions, final Month month) {
final List bankTransactionsInMonth = new ArrayListo();
for(final BankTransaction bankTransaction: bankTransactions) {
if(bankTransaction.getDate().getMonth() == month) {

bankTransactionsInMonth.add(bankTransaction);
}
}
return bankTransactionsInMonth;

)

Анализатор банковских операций

|

31

Главное преимущество данного решения заключается в том, что основ­
ное приложение больше не отвечает за функцию парсинга. Теперь эта
работа передана отдельному классу, а его методы могут редактировать­
ся или изменяться независимо. По мере поступления новых требований
к приложению вы можете изменять функционал, реализуемый в классе
BankStatementCSVParser.
Кроме того, если вам нужно, скажем, изменить алгоритм парсинга (напри­
мер, реализовать более сложную версию, которая кэширует результаты), то
теперь достаточно внести изменения только в одном месте. Более того, вы
ввели класс BankTransaction, благодаря которому ваше приложение может
работать независимо от конкретного формата данных.
Мы считаем хорошей привычкой при реализации методов следовать «прин­
ципу наименьшего удивления», то есть добиваться того, чтобы назначение
методов при просмотре кода было очевидным. Вот что это значит:



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



Не изменяйте свойства параметров, так как другие части кода могут за­
висеть от этого.

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

Связность
Итак, вы познакомились с тремя принципами: KISS, DRY и SRP. Но мы все
еще не рассмотрели параметры, позволяющие оценить качество вашего кода.
В среде разработчиков вы будете часто слышать о связности как о важном
качестве различных частей кода. Звучит немного замысловато, однако это
действительно очень полезная концепция, благодаря которой можно легко
оценить, насколько ваш код удобен в обслуживании.
Связность характеризует то, как взаимодействуют друг с другом различ­
ные детали. Если говорить точнее, связность — это мера того, насколько
сильно взаимосвязаны задачи элементов внутри класса или метода. Дру­
гими словами, насколько узлы класса «пересекаются друг с другом». Таким
способом вы можете оценить сложность своего приложения. Вам следует
стремиться к достижению высокой связности, что подразумевает создание

32

|

Глава 2

легко читаемого и легко понимаемого другими кода. В коде, который мы
переработали выше, класс BankTransactionCSVParser обладает высокой связ­
ностью. По сути, он объединяет в себе два метода, предназначенные для об­
работки CSV-данных.
В целом принцип связности применим к классам (внутриклассовая связ­
ность), но точно также его можно применить и к методам.
Если вы посмотрите на точку входа в нашу программу — класс BankStatementAnalyzer — то обнаружите, что его задача заключается только в том,
чтобы связать между собой различные части программы, такие как парсинг,
расчет суммы и выведение результата на экран. При этом код, который вы­
числяет сумму, по-прежнему находится в классе BankStatementAnalyzer и яв­
ляется его статическим методом. Это пример низкой, плохой связности, так
как обязанности по вычислению суммы находятся в классе, который непо­
средственно для этого не предназначен.
Таким образом, лучше переместить операции вычисления в отдельный класс
и назвать его BankStatementProcessor. Вы видите, что аргумент метода спис­
ка транзакций доступен для всех операций, так что вы можете добавить его
в класс в качестве поля. В результате сигнатуры метода становятся проще,
а класс BankStatementProcessor становится более связным. В примере 2-7 по­
казан конечный результат. Дополнительный плюс всего этого заключается
в том, что методы класса BankStatementProcessor могут быть использованы
другими частями приложения независимо.

Пример 2-7. Группировка операций вычисления в класс BankStatementProcessor
public class BankStatementProcessor {
private final List bankTransactions;

public BankStatementProcessor (final List bankTransactions) {

this.bankTransactions = bankTransactions;

)
public double calculateTotalAmount() (
double total = 0;
for(final BankTransaction bankTransaction: bankTransactions) {

total += bankTransaction.getAmount ();

}
return total;

Анализатор банковских операций

|

33

public double calculateTotallnMonth(final Month month) (
double total = 0;
for(final BankTransaction bankTransaction: bankTransactions) {
if(bankTransaction.getDate().getMonthf) == month) {

total += bankTransaction.getAmount ();

I

)
return total;

)
public double calculateTotalForCategory(final String category) (
double total = 0;
for(final BankTransaction bankTransaction: bankTransactions) {
if(bankTransaction.getDescription().equals(category)) {

total += bankTransaction.getAmount ();
)

}
return total;

)

)

Теперь методы этого класса можно использовать в BankStatementAnalyzer,
как показано в примере 2-8.
Пример 2-8. Обработка списка транзакций при помощи класса
BankstatementProcessor
public class BankStatementAnalyzer {
private static final String RESOURCES = "src/main/resources/";
private static final BankStatementCSVParser bankStatementParser = new

BankStatementCSVParser ();
public static void main(final String... args) throws IOException {
final String fileName = args[0];
final Path path = Paths.get(RESOURCES + fileName);
final List lines = Files.readAHLines (path);

final List bankTransactions = bankStatementParser.
parseLinesFrom(lines);

34

|

Глава 2

final BankStatementProcessor bankStatementProcessor = new BankStatementProc
essor(bankTransactions);

collectSummary(bankStatementProcessor);
)
private static void collectSummary(final BankStatementProcessor
bankStatementProcessor) {
System.out.printin("The total for all transactions is "
+ bankStatementProcessor.calculateTotalAmount());

System.out.printin("The total for transactions in January is "
+ bankStatementProcessor.calculateTotallnMonth(Month.JANUARY));
System.out.printin("The total for transactions in February is "
+ bankStatementProcessor.calculateTotallnMonth(Month.FEBRUARY) );
System.out.printin("The total salary received is "
+ bankStatementProcessor.calculateTotalForCategory("Salary") );

}
)

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

Внутриклассовая связность
На практике вы сталкнетесь как минимум с шестью основными типами
связности:


функциональная;



информационная;



служебная;



логическая;



последовательная;



временная.

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

Анализатор банковских операций

|

35

все перечисленные выше типы, после чего закрепим полученные знания при
помощи тезисов, представленных в таблице 2-1.

Таблица 2-1. Плюсы и минусы различных видов связности
Тип связности

Плюсы

Минусы

Функциональная (высокая
связность)

Легко читается

Может привести к избытку черес­
чур простых классов

Информационная (средняя
связность)

Легко обслуживается

Может привести к ненужным
зависимостям

Последовательная (средняя
связность)

Легко обнаружить связанные
операции

Не соблюдает принцип SRP

Логическая (средняя связность)

Обеспечивает определенный тип
сильной категоризации

Не соблюдает принцип SRP

Служебная (слабая связность)

Легко организовать

Тяжело понять назначение класса

Временная (слабая связность)

Нет

Тяжело различить и использовать
отдельные операции

Функциональная
Подход, примененный нами при разработке BankStatementCSVParser, со­
стоял в функциональной группировке методов. Методы parseFromO
и parseLinesFrom() решают определенную задачу: парсинг данных из CSVфайла. По сути, метод parseLinesFromf) использует метод parseFromf). Это
хороший способ достичь высокой связности, потому что методы работа­
ют вместе. Поэтому мы их объединили, чтобы их было легче найти и по­
нять. Опасность функциональной связности заключается в том, что можно
поддаться искушению и создать чрезмерное количество слишком простых
классов, имеющих по одному методу. Движение по этому пути приводит
к ненужному многословию и усложняет код, так как приходится работать
с необоснованно большим количеством классов.

Информационная
Другая причина для группировки методов: они работают с одними и теми
же данными или доменным объектом. Скажем, вам нужно создать, считать,
обновить или удалить объект BankTransaction. Возможно, вы решите создать
класс для выполнения этих операций. В примере 2-9 показан класс, реали­
зующий информационную связность различных методов. Каждый метод ге­
нерирует исключение UnsupportedOperationException, которое предупрежда­
ет о том, что тело функции не реализовано (просто для примера).

36

|

Глава 2

Пример 2-9. Пример информационной связности
public class BankTransactionDAO {

public BankTransaction create(final LocalDate date, final double amount, final
String description) {
// ...
throw new UnsupportedOperationException ();
)
public BankTransaction read(final long id) {

// ...
throw new UnsupportedOperationException ();

)
public BankTransaction update(final long id) {

// ...
throw new UnsupportedOperationException ();

I
public void delete(final BankTransaction BankTransaction) {

// ...
throw new UnsupportedOperationException();

}

}
Это типовой пример, часто встречающийся при организации работы
с базой данных, обслуживающей таблицу определенного доменного объ­
екта. Часто этот шаблон называют DAO (Data Access Object — объект до­
ступа к данным). Он требует наличия своего рода ID-номера для иденти­
фикации объекта. Обычно, DAO представляет собой абстрактный класс
и инкапсулирует доступ к источнику данных, например, к обычной или
резидентной базе данных.

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

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

|

37

метод. Тогда вы создаете некий служебный класс, который становится эта­
ким «мастером на все руки».

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

Логическая
Допустим, вам нужно реализовать поддержку таких форматов как CSV, JSON
и XML. Возможно, вы захотите объединить методы, предназначенные для
обработки разных форматов, в один класс, как показано в примере 2-10.

Пример 2-10. Пример логической связности
public class BankTransactionParser {

public BankTransaction parseFromCSV(final String line) {

// ...
throw new UnsupportedOperationExceptionO;

)
public BankTransaction parseFromJSON(final String line) (

// ...
throw new UnsupportedOperationExceptionO;

}
public BankTransaction parseFromXML(final String line) (

// ...
throw new UnsupportedOperationExceptionO;

}

}

На самом деле данные методы логически относятся к категории «парсинга».
При этом по своей сути они разные и мало связаны друг с другом. Их груп­
пировка также противоречит принципу SRP, о котором мы говорили ранее,
потому что класс берет на себя ответственность за несколько различных за­
дач. Из этого следует, что данный подход не является желательным.

38

|

Глава 2

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

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

Временная
Класс с временной связностью — это класс, который выполняет несколько
операций, связанных между собой только во времени. Стандартным приме­
ром такого класса является класс, который выполняет какие-либо инициализационные действия (например, подключение и отключение соединения
с базой данных), которые вызываются до или после основных операций.
Инициализация и другие операции не связаны между собой, но должны вы­
зываться в строго определенное время.

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

Анализатор банковских операций

|

39

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

Связанность
Другая важная характеристика кода — это связанность. Если связность го­
ворит о том, насколько связаны элементы в классе, пакете или методе, то
связанность показывает уровень зависимости от других классов. Иначе
говоря, связанность показывает, сколько знаний (то есть специфических
функций) вы заложили в определенный класс. Это важно, поскольку, чем
больше функций возложено на класс, тем сложнее вносить в него измене­
ния. По сути, изменения в классе влекут за собой изменения в связанных
с ним классах.

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

Связанность показывает, насколько части зависят друг от друга. К примеру,
класс BankStatementAnalyzer связан с классом BankStatementCSVParser. Что,
если вам нужно изменить парсер таким образом, чтобы он стал понимать
формат JSON? А если понадобится поддержка XML? Не очень приятные из­
менения, верно? Но не переживайте! Вы можете разделить различные со­
ставляющие при помощи интерфейса, оптимального инструмента, обеспе­
чивающего определенную гибкость в условиях изменения требований.
Во-первых, вам нужно ввести интерфейс, который расскажет, как использо­
вать парсер для банковских операций, но без написания конкретной реали­
зации, как показано в примере 2-11.
Пример 2-11. Добавление интерфейса для парсинга банковских операций
public interface BankStatementParser {

BankTransaction parseFrom(String line);
List parseLinesFrom(List lines);

40

|

Глава 2

Теперь класс BankStatementCSVParser будет реализовывать этот интерфейс:

public class BankStatementCSVParser implements BankStatementParser {

// ...

Все это хорошо, но как отвязать BankStatementAnalyzer от специфиче­
ской реализации BankStatementCSVParser? Нужно использовать интер­
фейс! За счет введения нового метода analyzed, который принимает
BankTransactionParser в качестве аргумента, вы больше не связаны с этой
специфической реализацией (смотрите пример 2-12).
Пример 2-12. Разделение парсера и BankStatementAnalyzer
public class BankStatementAnalyzer (
private static final String RESOURCES = "src/main/resources/";
public void analyze(final String fileName, final BankStatementParser

bankStatementParser)
throws IOException {
final Path path = Paths.get(RESOURCES + fileName);
final List lines = Files.readAllLines(path);
final List bankTransactions = bankStatementParser.
parseLinesFrom(lines);

final BankStatementProcessor bankStatementProcessor = new BankStatementProc
essor (bankTransactions);

collectSummary(bankStatementProcessor);

)
// ...
)

Великолепно! Теперь классу BankStatementAnalyzer больше не нужно знать,
как реализуются те или иные конкретные функции, а это упрощает внесе­
ние изменений в программу.

На рис. 2-1 показана разница в зависимостях до и после разделения классов.
Анализатор банковских операций

|

41

Сильная связанность

«BankStatementParser»

Рис. 2-1. Развязка двух классов
Теперь можно собрать все вместе и создать главный класс (пример 2-13).

Пример 2-13. Главный класс приложения
public class MainApplication {

public static void main(final String... args) throws IOException {

final BankStatementAnalyzer bankStatementAnalyzer
= new BankStatementAnalyzer();
final BankStatementParser bankStatementParser
= new BankStatementCSVParser ();

bankStatementAnalyzer.analyze(args[0], bankStatementParser);
I

)

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

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

42

|

Глава 2

гарантировать клиенту, что программа соответствует всем заявленным тре­
бованиям? В этом разделе вы познакомитесь с тестированием и узнаете, как
создать свой первый автоматический тест, используя самую популярную
адаптированную под Java платформу для тестирования: JUnit.

Автоматизированное тестирование
Автоматизированное тестирование звучит как еще одно занятие, которое
может отнять большой кусок времени от самой веселой части — непосред­
ственно написания кода! Почему это должно волновать вас?

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

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

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

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

Анализатор банковских операций

|

43

показаться, что проблемы от вас не ускользнут. А что, если вы работаете
с базой кода с миллионами строк? Или вносите изменения в код, который
писал ваш коллега? Насколько вы будете уверены в его работоспособности?
Если у вас есть набор автоматизированных тестов — очень удобно исполь­
зовать их, чтобы убедиться, что вы не создали новые баги.

Понимание программы
В-третьих, автоматизированные тесты помогут вам понять, как взаимодей­
ствуют между собой различные участки исходного кода. По сути, тесты мо­
гут раскрыть зависимости различных компонентов программы и их влияние
друг на друга. Согласитесь, в некоторых ситуациях получить краткое общее
представление о работе программы может быть чрезвычайно полезно. До­
пустим, вы присоединились к новому проекту. С чего начать, где получить
краткий обзор компонентов программы? Тесты — отличный способ начать.

Использование JUnit
Надеемся, вы убедились в том, что автоматизированные тесты очень важ­
ны. В этом разделе вы создадите свой первый автоматизированный тест при
помощи популярного Java-фреймворка под названием JUnit. У всего есть
своя цена. Создание тестов, в первую очередь, требует времени. Кроме того,
вам придется задуматься о долгосрочном обслуживании написанных вами
тестов, потому что тесты — это тоже код. Тем не менее положительные сто­
роны тестирования, перечисленные выше, перевешивают отрицательные.
В частности, вы научитесь писать модульные тесты, которые проверяют
на корректность маленькие изолированные кусочки программы, такие как
методы или маленькие классы. На протяжении всей книги вы будете по­
лучать рекомендации по созданию хороших тестов. В данной главе впер­
вые пройдет вводный инструктаж по написанию простого теста для класса
BankTransactionCSVParser.

Объявление метода теста
Первый вопрос, который может у вас возникнуть, — это где писать тест.
Стандартное соглашение, принятое в сборщиках Maven и Gradle, призывает
хранить код в src/main/java, а классы тестов в src/test/java. Вам также пона­
добится добавить зависимость к библиотеке JUnit в ваш проект. О том, как
структурировать проекты в Maven и Gradle, вы узнаете в главе 3.

44

|

Глава 2

В примере 2-14 показан простой тест для класса BankTransactionCSVParser.
Harn тестовый класс BankStatementCSVParserTest имеет в названии при­
ставку Test. Это не острая необходимость, но часто используется для луч-

S

Пример 2-14. Модульный тест для CSV-napcepa
import org.junit.Assert;
import org.junit.Test;
public class BankStatementCSVParserTest {

private final BankStatementParser statementparser = new
BankStatementCSVParser();

@Test
public void shouldParseOneCorrectLine() throws Exception (

Assert.fail("Not yet implemented");
)
}

В данном коде много нового. Давайте разбираться.


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



В классе объявлен один метод: shouldParseOneCorrectLine (). Рекоменду­
ется всегда давать методу название, позволяющее понять, что именно
тестируется.



Метод выделен при помощи аннотации @Test из JUnit. Данный знак
означает, что метод представляет собой модульный тест, который дол­
жен быть выполнен. Вы можете объявить приватные вспомогательные
методы в тестовом классе, но они не будут запущены при запуске теста.



Реализация метода вызывает Assert.fail ("Not yet implemented”), что
приводит к провалу модульного теста и выдаче сообщения Not yet
implemented («Еще не реализовано»). Скоро вы узнаете, как действитель­
но реализовать модульный тест с использованием набора операций кон­
троля, доступных в JUnit.
Анализатор банковских операций | 45

Вы можете выполнить приведенный тест в своем любимом сборщике (на­
пример, Maven или Gradle) или с помощью IDE. Например, после запуска
теста в IntelliJ IDE вы получите окно, показанное на рис. 2-2. Как вы видите,
тест закончился неудачно, появилось сообщение Not yet implemented. Те­
перь давайте посмотрим, как реализовать настоящий тест и убедиться в ра­
ботоспособности класса BankStatementCSVParser.

Рис. 2-2. Скриншот из среды IntelliJ IDE с результатом запуска теста

Операторы контроля
Только что вы познакомились с методом Assert. fail (). Это статический ме­
тод, предоставляемый в JUnit, и относится он к операторам контроля. JUnit
поддерживает много операторов контроля, предназначенных для тестиро­
вания различных состояний. Они позволяют задать ожидаемый результат
и сравнить его с фактическим результатом работы.
Один из статических методов — Assert .assertEquals (). Его можно исполь­
зовать для проверки корректности работы метода parseFromO с определен­
ными входными данными. Рассмотрим пример 2-15.
Пример 2-15. Применение операторов контроля
@Test
public void shouldParseOneCorrectLineO throve Exception {
final String line = "30-01-2017,-50,Tesco";

final BankTransaction result = statementparser.parseFrom(line);
final BankTransaction expected
= new BankTransaction(LocalDate.of(2017, Month.JANUARY, 30), -50, "Tesco");
final double tolerance = O.Od;

Assert.assertEquals(expected.getDate(), result.getDate());
Assert.assertEquals(expected.getAmount(), result.getAmount(), tolerance);
Assert.assertEquals(expected.getDescription(), result.getDescription());

46

|

Глава 2

Что происходит? Здесь можно выделить три составляющих.
1.

Вы задаете содержимое для теста. В данном случае это строка данных.

2.

Вы производите некоторое действие. В данном случае — парсинг вход­
ной строки.

3.

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

Данный трехступенчатый шаблон модульного тестирования часто описы­
вают формулой Given- When-Then [«Дано-Если-Тогда»]. Это довольно хоро­
ший подход, он позволяет разделить тест на этапы и понять принцип его
работы.
Если вы запустите тест снова, то с определенной вероятностью увидите зе­
леную строку, показывающую, что тест прошел удачно (рис. 2-3).
*»!
tit *
BankStatementCSVParserTest (com.ii 63ms
shoukJParseOneCorrectline
63m$

/Library/J«v*/3*v*VirttMlNBChin«s/jdkl.B.0_l5.jdk/Contents/Hoee/bin/jeva ...

Process finished with exit code e



Рис. 2-3. Запуск модульного теста

Есть и другие виды операторов контроля. Они сведены в таблицу 2-2.
Таблица 2-2. Операторы контроля
Оператор контроля

Назначение

Assert.fail(message)

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

Assert.assertEquals
(expected, actual)

Tea, который проверяет эквивалентноаь двух значений

Assert.assertEquals
(expected, actual, delta)

Теп, который проверяет два значения типа float или double на равенаво
с определенным допуском

Assert.assertNotNull(object)

Проверяет, что объект не null

Покрытие кода
Вы создали свой первый тест, поздравляем! Но как вы можете определить,
достаточно ли этого? Покрытие кода — это показатель, описывающий,

Анализатор банковских операций

|

47

какой процент вашего кода (какое количество строк или блоков) подверга­
ется тестированию вашим набором тестов. Вообще, стоит нацеливаться на
наибольшее покрытие, потому что в таком случае значительно снижается
вероятность возникновения багов. Пока нет какого-то конкретного зна­
чения покрытия, которое считалось бы достаточным, но мы рекомендуем
удерживать его в пределах не ниже 70-90%. На практике очень сложно до­
стичь 100% покрытия кода, потому что вам придется, к примеру, тестиро­
вать геттеры и сеттеры, а они дают малый объем.
Стоит отметить, что покрытие кода — это не обязательно хороший изме­
ритель качества тестирования кода. По сути, покрытие кода только пока­
зывает, какие участки вы еще не протестировали, но оно ничего не говорит
о качестве самих тестов. Вы можете предусмотреть тесты для простейших
случаев, упустив более сложные и редкие варианты отказов.

Популярными инструментами для оценки покрытия кода в Java являются
JaCoCo, Emma и Cobertura. На практике вы будете сталкиваться с людьми, рас­
сказывающими о линейном покрытии, которое показывает, какое количество
выражений в коде покрыто тестами. Данный подход дает ложное представ­
ление о хорошем покрытии, поскольку условные выражения (if, while, for)
в таком случае считываются как обычные выражения. При этом условные
операторы могут иметь несколько возможных ветвей. Поэтому вы должны
предусматривать охват всех ветвей, учитывая истинные или ложные состоя­
ния для каждой из них.

Выводы


Огромные классы и дублирование кода ведут к созданию совершенно
нечитаемого и неудобного в обслуживании кода.



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



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



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



Высокая связность и слабая связанность — это признаки удобного в об­
служивании кода.

48

|

Глава 2



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



JUnit — это Java-фреймворк для тестов, который позволяет создавать
модульные тесты для проверки поведения методов и классов.



Given-When-Then — это шаблон создания тестов в три этапа, позволяю­
щий хорошо понимать саму реализацию теста.

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


Напишите еще парочку модульных тестов для тестирования CSV-napсера.



Реализуйте поддержку таких операций, как поиск самых наиболее и наи­
менее затратных транзакций за определенный временной период.



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

В завершение
Марк Эрбергцук очень счастлив и благодарен вам за реализацию Анализа­
тора банковских операций. Он дал ему имя «THE Bank Statements Analyzer».
Марку так понравилось ваше приложение, что он хочет развивать его
и дальше. Он просит расширить возможности чтения, парсинга, обработки
и подсчета. К примеру, он фанат формата JSON. В завершение он отметил,
что тестирование сейчас несколько ограниченно и, кроме того, он даже на­
шел пару багов.

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

ГЛАВА 3

Расширяем анализатор
банковских операций

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

Цель
Из предыдущей главы вы узнали, как создать приложение для анализа
банковских операций, работающее с файлами в формате CSV. В процессе
работы над ним вы познакомились с базовыми принципами разработки
программного обеспечения, позволяющими писать обслуживаемый код
в соответствии с принципом единственной ответственности, научились из­
бегать антишаблонов (таких как «класс-бог» и дублирование кода). По мере
совершенствования кода вы также познакомились с такими понятиями, как
связанность (демонстрирующим зависимость от других классов) и связ­
ность (показывающим взаимосвязь между элементами класса или метода).
Тем не менее функционал приложения все еще довольно ограничен. Что,
если нам реализовать возможность поиска транзакций, поддержку раз­
личных форматов, экспорт отчетов в другие форматы, такие как текст или
HTML?

50

|

Глава 3

В этой главе вы займетесь разработкой программного обеспечения более
углубленно. В первую очередь вы познакомитесь с принципом открытости/закрытости, который поможет вам сделать код более гибким и удобным в обслу­
живании. Вы получите ряд рекомендаций по применению интерфейсов, а так­
же узнаете, как избежать возникновения сильной связанности. Кроме того, мы
поговорим об исключениях в Java, обсудим, когда имеет смысл включать их
в API, а когда нет. И наконец, вы узнаете о процессе сборки Java-приложений
с использованием таких сборщиков как Maven и Gradle.
Если в какой-либо момент вам захочется взглянуть на исходный код про­
граммы для этой главы, вы можете скачать пакет в репозитории: /сот/

iteratrlearning/shu_book/chapter_03.

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

1.

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

2.

Марк также хочет иметь возможность экспортировать статистику по
результатам поиска в другой формат, такой как текст или HTML.

Данная глава будет посвящена работе над реализацией этих требований.

Принцип открытости/закрытости
Давайте начнем с простого и реализуем метод, возвращающий все транзакции
с суммой, превышающей заданную. Первый вопрос: в каком месте объявить
этот метод? Можно создать отдельный класс BankTransactionFinder, в котором

Расширяем анализатор банковских операций

|

51

будет находиться простой метод findTransactions(). Однако напомним, что
в предыдущей главе мы создали класс BankTransactionProcessor. Так что же
делать? В данной ситуации не так много плюсов от создания нового класса,
ведь вам нужен всего один новый метод. Новый класс сделает проект сложнее,
так как поспособствует увеличению количества названий, что, в свою очередь,
затруднит понимание зависимостей между различными объектами. Объяв­
ление метода в классе BankTransactionProcessor улучшит читаемость кода. Вы
сразу будете понимать, что в данном классе собраны все методы, которые вы­
полняют разного рода обработку данных. После того как мы определились, где
поместить метод, можно заняться его реализацией (пример 3-1).
Пример 3-1. Поиск транзакций на сумму больше заданной
public List findTransactionsGreaterThanEqual(final int amount) {
final List result = new ArrayListo();
for(final BankTransaction bankTransaction: bankTransactions) {
if(bankTransaction.getAmount() >= amount) {

result.add(bankTransaction);
}
}
return result;

}

Здесь все достаточно рационально. Но что, если вы хотите сделать поиск
в конкретном месяце? Тогда придется дублировать данный метод, как пока­
зано в примере 3-2.

Пример 3-2. Поиск транзакций в определенном месяце
public List findTransactionsInMonth(final Month month) {
final List result = new ArrayListo();
for(final BankTransaction bankTransaction: bankTransactions) (
if(bankTransaction.getDate(),getMonth() == month) {

result.add(bankTransaction);
)

)
return result;

)

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

|

Глава 3

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

Данный подход не будет работать и с более сложными требованиями. Что
если нам понадобится искать транзакции не только на определенную сумму,
но одновременно и за конкретный месяц? Реализовать это новое требова­
ние можно так, как показано в примере 3-3.
Пример 3-3. Поиск банковских операций на определенную сумму
и за определенный месяц
public List findTransactionsInMonthAndGreater(final Month month,
final int amount) {
final List result = new ArrayListo();
for(final BankTransaction bankTransaction: bankTransactions) {
if(bankTransaction.getDate().getMonth() == month && bankTransaction.

getAmount() >= amount) {
result.add(bankTransaction);

)
}
return result;

/
По правде говоря, такой подход имеет ряд недостатков:


Код становится более сложным, поскольку вам нужно объединить не­
сколько свойств банковских операций.



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



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

Вот где на сцену выходит принцип открытости/закрытости. Он продвигает
идею возможности изменения поведения метода или класса без необходи­
мости изменения самого кода. То есть, если говорить о нашем примере, нуж­
но расширить возможности метода findTransactions () без дублирования
или изменения кода. Разве это реально? Как мы говорили выше, концеп­
ции последовательного перебора и логика выбора подходящих транзакций
связаны между собой. В предыдущей главе вы познакомились с отличным
инструментом для разделения задач, а именно — с интерфейсами. Сей­
час мы создадим интерфейс BankTransactionFilter и возложим на него

Расширяем анализатор банковских операций

|

53

ответственность за выбор подходящих транзакций из списка. В нем будет
располагаться метод test () , возвращающий булево значение, а в качестве
аргумента принимающий объект BankTransaction. Таким образом метод
test () будет иметь доступ ко всем свойствам объекта BankTransaction, что
позволит ему установить любые критерии поиска.
Интерфейс, содержащий только один абстрактный метод, называется
функциональным интерфейсом (начиная с Java версии 8). Вы можете ан­
нотировать данный интерфейс при помощи строки @ Functional Interface,
чтобы сделать его назначение более понятным.

Пример 3-4. Интерфейс BankTransactionFilter
@FunctionalInterface
public interface BankTransactionFilter {
boolean test(BankTransaction bankTransaction);

)
В Java 8 появился базовый интерфейс java.util.function.Predicate,
который мог бы стать отличным решениемнашей проблемы. Однако
в этой главе мы уже ввели собственный интерфейс, чтобы не усложнять
код слишком рано.

Интерфейс BankTransactionFilter представляет собой механизм выбора
критериев для объектов BankTransaction. Теперь метод findTransactions ()
можно отредактировать так, как показано в примере 3-5. Это довольно
важный момент, поскольку только что вы применили на практике новый
способ разделения двух алгоритмов. Ваш метод больше не зависит от одной
конкретной реализации фильтра. Вы можете придумать другие реализации,
просто передавая их в качестве аргументов, без необходимости изменять
сам метод. По сути, теперь метод открыт для расширения и закрыт для из­
менения. Благодаря этому значительно снижается вероятность возникнове­
ния новых багов, так как минимизируется количество изменений, которые
нужно внести в уже отлаженный и протестированный код. Другими слова­
ми, старый код работает как и раньше и остается нетронутым.
Пример 3-5. Гибкая реализация метода findTransactions()> построенная на
принципе открытости/закрытости
public List findTransactions(final BankTransactionFilter bankTran

sactionFilter) {

54

|

Глава 3

final List result = new ArrayListo ();
for(final BankTransaction bankTransaction: bankTransactions) {
if(bankTransactionFilter .test(bankTransaction)) {

result.add(bankTransaction);
}

}
return result;

)

Создание экземпляра функционального интерфейса
Марк Эрбергцук счастлив. Теперь вы можете оперативно подстраивать про­
грамму под любые требования за счет метода findTransactions () из класса
BankTransactionProcessor, который реализует соответствующий интерфейс
BankTransactionFilter. Этого можно достичь, реализовав класс так, как по­
казано в примере 3-6, а затем, передав объект в качестве аргумента методу
findTransactions (), как показано в примере 3-7.
Пример 3-6. Объявляем класс, который реализует интерфейс
BankTransactionFilter
class BankTransactionlsInFebruaryAndExpensive implements BankTransactionFilter (

^Override
public boolean test(final BankTransaction bankTransaction) {
return bankTransaction.getDate().getMonth() == Month.FEBRUARY

&& bankTransaction.getAmount() >= l_000);
)
}

Пример 3-7. Вызов метода findTransactions() с определенной реализацией
BankTransactionFilter
final List transactions

= bankStatementProcessor.findTransactions(new BankTransactionlsInFebruaryAndEx
pensive());

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

|

55

стать причиной некой шаблонности и в скором времени привести к загро­
мождению кода. Начиная с Java версии 8 в вашем распоряжении есть лямб­
да-выражения (пример 3-8). Пока вам не стоит волноваться о синтаксисе
применения этих выражений. Более подробно мы познакомимся с лямб­
да-выражениями и ссылками на методы в главе 7. Сейчас же вы можете
представить себе это примерно так: вместо передачи объекта, который
реализует какой-то интерфейс, мы передаем блок кода (по сути, функцию
без названия). bankTransaction — это имя параметра, а стрелка -> отделяет
параметр от тела лямбда-выражения, которое, по сути, представляет собой
кусочек кода, проверяющий транзакцию на соответствие некоторым усло­
виям.

Пример 3-8. Реализация BankTransactionFilter с использованием лямбдавыражения
final List transactions

= bankStatementProcessor.findTransactions(bankTransaction ->
bankTransaction.getDate().getMonth() == Month.FEBRUARY
&& bankTransaction.getAmount() >= l_000);

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


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



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



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

Подводные камни интерфейсов
Какое-то время назад вы познакомились с гибким методом поиска транзак­
ций, соответствующих заданным критериям. По ходу изменения кода нашей
программы возникает вопрос о том, что же будет с остальными методами,
объявленными в классе BankTransactionProcessor. Стоит ли трансформиро­
вать их в интерфейсы? Или, может быть, переместить в другой класс? После
всего у нас остаются три связанных между собой метода, которые мы созда­
ли в предыдущей главе:

56

|

Глава 3



calculateTotalAmount()



calculateTotallnMonth()



calculateTotalForCategory ()

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

Интерфейс-бог
Кто-то из вас, возможно, согласится с тем, что класс BankTransactionProcessor
работает как API. Данная точка зрения может привести к тому, что вам за­
хочется создать интерфейс, позволяющий избавиться от многочисленных
реализаций обработчика банковских операций (пример 3-9). В этом интер­
фейсе будут содержаться все операции, которые должен выполнять обра­
ботчик банковских операций.

Пример 3-9. Интерфейс-бог
interface BankTransactionProcessor {
double calculateTotalAmount ();
double calculateTotallnMonth(Month month);
double calculateTotallnJanuary();
double calculateAverageAmount();
double calculateAverageAmountForCategory(Category category);

List findTransactions(BankTransactionFilter
bankTransactionFilter);

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



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

Расширяем анализатор банковских операций

|

57

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


Конкретные свойства BankTransaction, такие как месяц и катего­
рия, появляются как имена методов: calculateAverageForCategory ()
и calculateTotallnJanuary(). Это еще одна проблема интерфейсов: те­
перь они зависят от определенных средств доступа доменного объекта.
Если изменяется содержимое этого доменного объекта, значит, измене­
ния грядут и в интерфейсе, и, как следствие, во всех его реализациях
тоже.

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

Слишком мизерный
Только что мы пришли к выводу, что чем меньше интерфейс, тем лучше.
Другая крайность — создавать интерфейсы для каждой отдельной опера­
ции, как показано в примере 3-10. В данном случае все интерфейсы реали­
зует класс BankTransactionProcessor.

Пример 3-10. Очень маленькие интерфейсы
interface CalculateTotalAmount {
double CalculateTotalAmount();

}
interface CalculateAverage {
double calculateAverage ();

}
interface CalculateTotallnMonth {
double CalculateTotallnMonth(Month month);

}
Такой подход тоже не способствует улучшению обслуживаемости кода.
И вообще, он вносит в код «антисвязность». Другими словами, становит­
ся намного сложнее найти интересующую вас операцию, так как они все

58

|

Глава 3

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

Явный API против неявного
И все-таки, каким должен быть прагматичный подход? Мы рекомендуем при­
держиваться принципа открытости/закрытости, чтобы сохранять гибкость
операций, а наиболее популярные операции объявлять в виде элементов
класса. Они могут быть реализованы более общими методами. В таком случае
применение интерфейса не сильно обосновано, поскольку мы не планируем
иметь различные реализации BankTransactionProcessor. У всех этих методов
нет какой-то особой специализации, которая была бы полезна всему прило­
жению. Поэтому нет необходимости что-то мудрить и добавлять лишнюю
абстракцию в программу. BankTransactionProcessor — это простой класс, ко­
торый обеспечивает выполнение статистических операций по транзакциям.
Отсюда возникает вопрос о том, где объявлять такие методы, как f indTrans
actionsGreaterThanEqual (), при учете, что их легко можно реализовать в бо­
лее общих методах вроде findTransactions (). Эту дилемму часто называют
проблемой явного или неявного API.

На самом деле здесь есть две стороны медали. С одной стороны, метод fin
dTransactionsGreaterThanEqualO является довольно очевидным и простым
в использовании. Вам не придется волноваться о добавлении описания
в название метода, чтобы упростить читаемость и понимание вашего API.
При этом данный метод довольно ограничен в своем применении, и у вас
есть все шансы получить переизбыток методов, если нужно будет реали­
зовывать дополнительные возможности. С другой стороны, такой метод,
как findTransactions (), изначально является более сложным в использова­
нии и должен быть хорошо задокументирован. При этом он обеспечивает
унифицированный API и участвует во всех случаях поиска транзакций. Нет
четкого правила о том, что лучше, а что хуже. Все зависит от того, какие за­
просы вы реализуете. Если findTransactionsGreaterThanEqual() будет более
общей (часто используемой) операцией, то имеет смысл извлечь ее в отдель­
ный API, тем самым упростив ее применение.

Последняя реализация BankTransactionProcessor показана в примере 3-11.
Расширяем анализатор банковских операций

|

59

Пример 3-11.Ключевые операции класса BankTransactionProcessor
@FunctionalInterface
public interface BankTransactionSunnnarizer {
double summarize(double accumulator, BankTransaction bankTransaction);

}

@FunctionalInterface
public interface BankTransactionFilter {
boolean test(BankTransaction bankTransaction);

)
public class BankTransactionProcessor {

private final List bankTransactions;
public BankStatementProcessor (final List bankTransactions) I
this.bankTransactions = bankTransactions;

)
public double summarizeTransactions(final BankTransactionSummarizer
bankTransactionSummarizer) {
double result = 0;
for(final BankTransaction bankTransaction: bankTransactions) (
result = bankTransactionSummarizer.summarize(result, bankTransaction);

return result;

public double CalculateTotallnMonth(final Month month) {
return summarizeTransactions((acc, bankTransaction) ->

bankTransaction.getDate ().getMonth() == month ? acc +
bankTransaction.getAmount () : acc
);
}
// ...
public List findTransactions(final BankTransactionFilter
bankTransactionFilter) {
final List result = new ArrayListo();
for(final BankTransaction bankTransaction: bankTransactions) {
if(bankTransactionFilter.test(bankTransaction) ) {
result.add(bankTransaction) ;
I
}
return bankTransactions;

60

|

Глава 3

public List findTransactionsGreaterThanEqual(final int amount)
return findTransactions(bankTransaction -> bankTransaction.getAmount () >=

amount);
)

// ...

)
Большинство шаблонов агрегации, с которыми вы встречались, могли
быть реализованы при помощи Streams API, который появился в Java 8.
Например, поиск транзакций можно легко реализовать вот так:

bankTransactions
.stream()
.filter(bankTransaction -> bankTransaction.getAmount() >= l_000)
.collect(toList()) ;
Тем не менее Streams API построен на тех же принципах, с которыми вы
только что познакомились.

Доменный класс или примитив?
Пока мы сохраняем интерфейс BankTransactionSummarizer простым, предпо­
чтительно не возвращать никаких примитивов вроде double по результатам
работы. Причина в снижении гибкости в случае возврата множественного
результата. Например, метод summari zeTransaction () возвращает double. Если
вы соберетесь изменить сигнатуры результата, чтобы возвращать больше
значений, вам придется менять все реализации BankTransactionProcessor.

Решить данную проблему можно при помощи создания доменного класса
под названием Summary, который «обернет» значение double. Тогда в буду­
щем вы сможете добавлять другие поля в этот класс. Такая техника помога­
ет «развязать» различные операции в домене и способствует минимизации
изменений.
Примитив double имеет ограниченное число бит (разрядность), поэто­
му при хранении десятичных чисел мы имеем ограниченную точность.
В качестве альтернативы можно рассмотреть класс java.math.BigDecimal,
обеспечивающий произвольную точность. Однако стоит учитывать, что
за эту точность придется заплатить ресурсами процессора и памяти.

Расширяем анализатор банковских операций

|

61

Множественный экспорт
В предыдущем разделе вы познакомились с принципом открытости/закры­
тости и углубили свои знания в области интерфейсов Java. Все это вам очень
пригодится, так как у Марка Эрбергцука появилось новое требование! Вам
нужно экспортировать итоговую статистику по выбранному списку транзак­
ций в различные форматы, включая текст, HTML, JSON и т. д. С чего начать?

Знакомство с доменным объектом
Для начала вам необходимо определиться с тем, что именно хочет экспорти­
ровать пользователь. Есть несколько возможных вариантов.
Число

Допустим, пользователю интересен только результат какой-нибудь опера­
ции вроде calculateAveragelnMonth. Значит, в качестве результата должен
выдаваться тип double. Хотя это и самый простой подход (как мы уже от­
метили ранее), он является наименее гибким и тяжелее всего поддается из­
менениям. Представьте, что ваш экспортер на входе принимает тип double.
Если вдруг поменяются требования к экспорту данных, вам придется изме­
нять код везде, где вызывается этот экспортер, что способно привести к воз­
никновению багов.

Коллекция
Возможно, пользователь хочет экспортировать список транзакций. Напри­
мер, результат работы метода findTransaction (). Для обеспечения гибкости
в будущем, вы даже могли бы экспортировать Iterable. Такой подход повы­
шает гибкость, однако в то же время он привязывает вас к необходимости
экспорта именно коллекций. А что, если вам понадобится возвращать мно­
жественный результат вроде списков или другой информации?

Специализированный доменный объект
Вы можете ввести некую новую «сущность» SummaryStatistics, представ­
ляющую итоговую информацию, которую хотел бы видеть пользователь
в результате экспорта. Доменный объект — это просто экземпляр класса,
привязанный к вашему домену. Внедряя доменный объект, вы применяете
одну из форм «развязывания». По сути, если возникают новые требования
62

|

Глава 3

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

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

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

Пример 3-12. Доменный объект, хранящий статистическую информацию
public class SunmaryStatistics {
private final double sum;
private final double max;
private final double min;
private final double average;
public Summarystatistics(final double sum, final double max, final double min,
final double average) {

thie.sum = sum;
this.max = max;
this.min = min;
this.average = average;

)
public double getSumO {
return sum;

I
public double getMaxO {
return max;

}
public double getMin() {
return min;

)

Расширяем анализатор банковских операций

|

63

public double getAverageO {
return average;

}
)

Объявление и реализация соответствующего интерфейса
Итак, вы определились с тем, какие данные нужно экспортировать. Самое
время заняться API. Вам нужно создать интерфейс с именем Exporter. При­
чина, по которой мы вводим интерфейс — он дает возможность избавиться
от связи с множеством реализаций экспортеров. Это как раз соответствует
принципу открытости/закрытости, с которым вы познакомились ранее. На
самом деле если вам понадобится заменить реализацию экспортера в JSON на
экспортер в XML, вы все сделаете достаточно просто, учитывая, что они оба
будут реализованы в одном интерфейсе. Первым шагом в создании такого ин­
терфейса может стать код, приведенный в примере 3-13. Метод export () при­
нимает в качестве параметра объект SummaryStatistics, а возвращает void.

Пример 3-13. Плохой интерфейс экспортера
public interface Exporter {
void export(SummaryStatistics summarystatistics);

}

Такого подхода следует избегать по нескольким причинам:



Методы с возвращаемым значением типа void неудобны и сложны в ис­
пользовании, поскольку вам неизвестно, какое именно значение воз­
вращается. Сигнатура метода export () подразумевает, что где-то про­
исходит изменение состояния чего-либо или что данный метод будет
фиксировать или выводить информацию на экран. Мы не знаем!



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

Учитывая все вышесказанное, вам следует придумать новый API, который
возвращает тип String (пример 3-14). Теперь понятно, что Exporter возвра­
щает текстовые данные, которые затем передаются в другую часть програм­
мы для распечатки, сохранения в файл или даже отправки по электронной
почте. Текстовые строки также достаточно удобны для тестирования при
помощи утверждений.
64

|

Глава 3

Пример 3-14. Хороший интерфейс экспортера
public interface Exporter {

String export(SummaryStatistics summaryStatistics);
}

После объявления API для экспорта информации можно реализовать раз­
личные виды экспортеров, взаимодействующих с интерфейсом Exporter. Ва­
риант реализации простейшего HTML-экспортера показан в примере 3-15.

Пример 3-15. Реализация интерфейса Exporter
public class HtmlExporter implements Exporter {

Override
public String export(final SummaryStatistics summaryStatistics) {

String
result
result
result
result
result
+ "";
result
getAverageO +
result
+ "";
result
+ "”;
result
result
result

result = "";
+= Xhtml lang='en'>";
+= ”Bank Transaction Report";
+= "";
+= "";
+= "The sum is: " + summaryStatistics.getSum()

+= "The average is: " + summaryStatistics.
"";
+= "The max is: " + summaryStatistics.getMax()
+= "The min is: " + summaryStatistics.getMin()

+= "";
+= "";
+= "";
return result;

)
)

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

|

65



Что, если синтаксический анализ данных проходит неправильно?



Что, если не удается прочитать CSV-файл, содержащий информацию
о банковских операциях?



Что, если аппаратному обеспечению, на котором работает ваше прило­
жение, не хватает ресурсов? Например оперативной памяти или места
на диске?

В таких случаях вы должны получить сообщение об ошибке с информацией
о трассировке стека, показывающей источник проблемы. Фрагменты в при­
мере 3-16 показывают, как могут выглядеть такие ошибки.
Пример 3-16. Неожиданные ошибки
Exception in thread ’’main" java.lang.ArraylndexOutOfBoundsException: 0
Exception in thread "main" java.nio.file.NoSuchFileException: src/main/resources/
bank-data-simple.csv

Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

Для чего нужны исключения?
На некоторое время остановимся на BankStatementCSVParser. Как нам обра­
батывать проблемы в процессе парсинга? К примеру, строка CSV-файла мо­
жет быть записана не так, как вы ожидали:


Строка CSV-файла может содержать больше, чем три ожидаемых ко­
лонки.



Строка CSV-файла может содержать меньше, чем три ожидаемых ко­
лонки.



Формат данных в некоторых колонках может быть некорректным (на­
пример формат даты).

Когда-то, в страшные времена программирования на языке С, вам пришлось
бы добавлять очень много проверок при помощи условий if, которые воз­
вращали бы коды критических ошибок. У такого подхода есть недостатки.
Во-первых, он основывался на глобальных изменяемых значениях, чтобы
найти самую последнюю ошибку. Это усложняло понимание отдельных час­
тей кода. Как следствие, код становилось тяжелее обслуживать. Во-вторых,
такой подход провоцировал появление ошибок, поскольку вам нужно было
66

|

Глава 3

различать между собой реальные значения и коды ошибок, закодированные
в виде обычных значений. Система типов в этом случае была слабой и могла
бы быть более полезной для программиста. И наконец, управляющий поток
смешивался с бизнес-логикой, что приводило к сложному в обслуживании
и тестировании коду.
Для решения упомянутых проблем в Java предусмотрен механизм исключе­
ний, имеющий множество преимуществ:
Документация

Язык поддерживает исключения как часть сигнатур методов.
Безопасность типов

Система типов определяет, обрабатываете ли вы поток исключений.
Разделение задач

Бизнес-логика и исключения отделены друг от друга при помощи блока tгу\
catch.

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

Проверяемые исключения
Это ошибки, появления которых вы ожидали и были готовы обработать.
В Java вы должны объявлять метод со списком исключений, которые он мо­
жет генерировать. Если вы этого не делаете, тогда вам нужно обеспечивать
наличие локальных блоков try\catch для конкретных исключений.

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

Классы Java-исключений организованы в четкой иерархии (рис. 3-1). Клас­
сы Error и RuntimeException (являются подклассами Throwable ) — непро­
веряемые исключения. Не стоит ожидать, что вы сможете «выловить» их

Расширяем анализатор банковских операций

|

67

и обработать. Класс Exception обычно представляет те ошибки, которые
программа способна обработать.

Непроверяемые

Проверяемые

Рис. 3-1. Иерархия исключений в Java

Шаблоны и антишаблоны для исключений
Когда и какие типы исключений нужно применять? Вы удивитесь, как мож­
но изменить BankStatementParser API, реализовав поддержку исключений.
Тем не менее, к сожалению, однозначного ответа на заданный выше вопрос
не существует. Потребуется немного прагматизма при принятии решения
о том, какой подход является правильным для вас.

Есть две ключевых задачи при обработке CSV-файла:


Правильно обработать синтаксис (CSV, JSON).



Проверить корректность данных (к примеру, длина описания должна
быть менее 100 символов).

Сначала рассмотрим ошибки синтаксиса, а затем перейдем к корректности
данных.

Выбор между проверяемыми и непроверяемыми исключениями
Могут возникнуть ситуации, когда CSV-файл не будет соответствовать тре­
бованиям по синтаксису. Например, из-за отсутствия разделительных запя­
тых. Игнорирование подобных проблем приводит к ошибкам во время вы­
полнения программы. Одним из плюсов исключений в коде является более
простая диагностика проблем пользователем. Соответственно, вы решаете
добавить простую проверку (пример 3-17), которая генерирует исключение
CSVSyntaxException.
68

|

Глава 3

Пример 3-17, Генерация исключения по синтаксису
final String[1 columns = line.split

;

if(columns.length < EXPECTED_ATTRIBUTES_LENGTH) {
throw new CSVSyntaxException ();

}

Каким должно быть исключение CSVSyntaxException: проверяемым или не­
проверяемым? Чтобы ответить на этот вопрос, нужно знать, будете ли вы
требовать от пользователя вашего API совершения каких-либо действий.
Например, пользователь может сделать вторую попытку в случае, если
ошибка временная, или вывести сообщение на экран, что добавит изящ­
ности приложению. Обычно ошибки, связанные с основным алгоритмом
программы (например, неверный формат данных или ошибка в арифме­
тике), обрабатываются непроверяемыми исключениями, поскольку иначе
они заполнили бы код большим количеством блоков try\catch. Кроме того,
не всегда очевидно, каким должен быть правильный механизм обработки
ошибки. Следовательно, нет никакого смысла навязывать его пользовате­
лю вашего API. В дополнение скажем, что системные ошибки (вроде недо­
статочного места на диске) тоже должны обрабатываться непроверяемы­
ми исключениями, потому что пользователь все равно не сможет ничего
сделать. В двух словах: лучше использовать непроверяемые исключения
и очень умеренно проверяемые, чтобы избежать ненужного беспорядка
в коде.
Теперь, когда мы убедились в целостности CSV-файла, можно приступить
к решению вопроса корректности данных. Вам предстоит познакомиться
с двумя распространенными антишаблонами использования исключений,
после чего мы рассмотрим шаблон уведомления, предоставляющий обслу­
живаемое решение проблемы.

Слишком специфические
Первый вопрос, который может у вас возникнуть, — где разметить код про­
верки корректности данных. Вы могли бы получить его во время создания
объекта Bankstatement. Хотя, по некоторым причинам, мы рекомендуем со­
здать специальный класс Validator. Вот почему:



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

|

69



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



Такой алгоритм легко отдельно протестировать.



Данный подход соответствует принципу SRP, что упрощает обслужива­
ние и снижает сложность программы.

Существуют различные подходы к реализации такого алгоритма при
помощи исключений. Один из них (слишком специфический) проде­
монстрирован в примере 3-18. В данном случае вы продумали все воз­
можные непредвиденные ситуации при проверке входных данных и каж­
дую из них преобразовали в проверяемое исключение. Исключения
DescriptionTooLongException, InvalidDateFormat, DatelnTheFutureException
и InvalidAmountException — это объявленные пользователем проверяемые
исключения (то есть они расширяют класс Exception). Хотя такой подход
дает возможность конкретизировать механизмы обработки для каждо­
го исключения, он абсолютно не продуктивен, поскольку требует тонкой
настройки, объявляет много исключений и вынуждает пользователя ин­
дивидуально работать с каждым исключением. А это противоречит цели
упростить понимание и использование API пользователем. Кроме того, вы
не можете помещать все исключения в коллекцию, чтобы предоставить
пользователю в виде списка.
Пример 3-18. Слишком специфические исключения
public class OverlySpecificBankStatementValidator {

private String description;
private String date;
private String amount;
public OverlySpecificBankStatementValidator(final String description, final
String date, final String amount) {
this.description = Objects.requireNonNull(description) ;
this.date = Objects.requireNonNull(description) ;
this.amount = Objects.requireNonNull(description) ;

}
public boolean validate() throws DescriptionTooLongException,

InvalidDateFormat,
DatelnTheFutureException,
InvalidAmountException {

70

|

Глава 3

if(this.description.length() > 100) {
throw new DescriptionTooLongException();

}
final LocalDate parsedDate;
try {

parsedDate = LocalDate.parse(this.date);
}
catch (DateTimeParseException e) {
throw new InvalidDateFormat();

)
if (parsedDate.isAfter(LocalDate.now())) throw new

DatelnTheFutureException();
try {

Double.parseDouble(this.amount);

}
catch (NumberFormatException e) (
throw new InvalidAmountException();

)
return true;

}
)

Слишком однообразные
Еще один вариант — сделать все исключения непроверяемыми. Например,
использовать IllegalArgumentException. В примере 3-19 показана реализа­
ция метода validate(), отражающая данный подход. Теперь проблема за­
ключается в том, что вы просто не можете реализовать индивидуальную
логику обработки, поскольку все исключения одинаковые. Кроме того, вы
все еще не можете собрать полностью все ошибки.

Пример 3-19. Исключения Illegal Argument повсюду
public boolean validate() (

if(this.description.length() > 100) {
throw new IllegalArgumentException("The description is too long");

}
final LocalDate parsedDate;
try {

Расширяем анализатор банковских операций

|

71

parsedDate = LocalDate.parse(this.date);

}
catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid format for date", e);

)
if (parsedDate.isAfter(LocalDate.now() )) throw new

IllegalArgumentException("date cannot be in the future");
try {

Double.parseDouble(this.amount);
)
catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid format for amount", e);

)
return true;

)

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

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

Первое, что вам нужно, — это класс Notification, отвечающий за сбор оши­
бок. Объявляем его как показано в примере 3-20.
Пример 3-20. Внедрение доменного класса Notification, который отвечает
за сбор ошибок
public class Notification (
private final List errors = new ArrayListo ();

public void addError(final String message) (

errors.add(message);

)

1 Этот шаблон впервые был предложен Мартином Фаулером, — Прим. авт.

72

|

Глава 3

public boolean hasErrorsO {
return !errors.isEmpty();

}
public String errorMessage() {
return errors.toString();

}
public List getErrors() {
return this.errors;

Плюсом создания данного класса является то> что теперь вы можете объ­
явить валидатор (метод для проверки корректности данных), который спо­
собен собирать много ошибок за раз. Это было невозможно в двух предыду­
щих подходах. Теперь вместо генерирования исключений вы можете просто
добавлять сообщения в объект Notification (пример 3-21).
Пример 3-21. Шаблон уведомления
public Notification validate() {
final Notification notification = new Notification();
if(this.description.length() > 100) (

notification.addError("The description is too long");

}
final LocalDate parsedDate;
try {

parsedDate = LocalDate.parse(this.date);
if (parsedDate.isAfter(LocalDate.now() )) (
notification.addError("date cannot be in the future");
}
}
catch (DateTimeParseException e) {

notification.addError("Invalid format for date");
}
final double amount;
try {

amount = Double.parseDouble(this.amount);
)

Расширяем анализатор банковских операций

|

73

catch (NumberFormatException e) {

notification.addError("Invalid format for amount");
}
return notification;

}

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

Не игнорируйте исключение
Игнорировать исключения — плохая идея. Так вы не сможете узнать, в чем
на самом деле кроется проблема. Если нет очевидного механизма обработки,
генерируйте непроверяемое исключение. В таком случае, если вам действи­
тельно понадобится обработать проверяемое исключение, вы будете выну­
ждены вернуться и разобраться с ним после того, как увидите проблему во
время выполнения программы.

Не перехватывайте «общие» исключения
По возможности перехватывайте конкретные исключения, чтобы повысить
читаемость и реализовать обработку специфических исключений. Если вы
перехватываете общие исключения Exception, то они также включают в себя
RuntimeException. Некоторые IDE могут самостоятельно генерировать блоки
catch, которые являются слишком обобщенными. Поэтому вам стоит поду­
мать о том, чтобы их конкретизировать.

Документируйте исключения
Документируйте исключения, включая непроверяемые, на уровне API. Это
поможет при устранении неисправностей. На самом деле непроверяемые
исключения в отчете указывают на источник проблемы, который потом
можно найти. Пример 3-22 демонстрирует документирование исключений
с использованием ключевых слов @throws синтаксиса Javadoc.
Пример 3-22. Документирование исключений
^throws NoSuchFileException if the file does not exist
@throws DirectoryNotEmptyException if the file is a directory and could not
otherwise be deleted because the directory is not empty

74

|

Глава 3

^throws
^throws
manager
invoked

IOException if an I/O error occurs
SecurityException In the case of the default provider, and a security
is installed, the (@link SecurityManagerflcheckDelete(String) } method is
to check delete access to the file

Будьте осторожны с исключениями, связанными с конкретной реализацией
Не создавайте исключения, связанные с конкретной реализацией, потому
что это нарушает принцип инкапсуляции вашего API. Объявление метода
read () в примере 3-23 принуждает любые его будущие реализации гене­
рировать исключение OracleException, при том, что метод read () работает
с источниками данных, абсолютно не связанными с Oracle.

Пример 3-23. Избегайте исключений, зависящих от конкретной реализации
public String read(final Source source) throws OracleException (...)

Исключения против управляющего потока
Не используйте исключения для управления потоком. Пример 3-24 демон­
стрирует ситуацию, когда код полагается на исключение для выхода из цик­
ла чтения.

Пример 3-24. Использование исключений для управления потоком
try (
while (true) {

System.out.printin(source.read ());
)

)
catch(NoDataException e) (

)

Такой ситуации следует избегать по нескольким причинам. Во-первых,
ухудшается восприятие кода, потому что синтаксис конструкции try\catch
создает некий беспорядок. Во-вторых, содержимое самого кода становится
менее понятным. Исключения подразумевают работу с ошибками и непред­
виденными ситуациями. Следовательно, не стоит создавать исключение,
если вы не уверены в том, что оно необходимо. И, наконец, при генерации
исключений могут возникать дополнительные проблемы с трассировками
стека.

Расширяем анализатор банковских операций

|

75

Альтернативы исключениям
Вы узнали, как использовать исключения в Java, чтобы сделать свой анали­
затор банковских операций более устойчивым и понятным для пользова­
телей. Но есть ли какие-то альтернативы исключениям? Сейчас мы кратко
расскажем о четырех альтернативах и об их плюсах и минусах.

Использование null
Вместо того чтобы создавать индивидуальное исключение, почему нельзя
просто вернуть значение null, как показано в примере 3-25?

Пример 3-25. Возврат null вместо исключения
final String[] columns = line.split;
if(columns.length < EXPECTED_ATTRIBUTES_LENGTH) {
return null;

}

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

Шаблон null-объекта
В Java иногда можно встретить такой подход, как шаблон null-объекта.
Если говорить коротко, то вместо возврата нулевой ссылки, которая ука­
зывает на отсутствие объекта, вы возвращаете некий объект, который реа­
лизует ожидаемый интерфейс, но метод внутри него пуст. Преимущество
такого подхода состоит в том, что вам не приходится сталкиваться с ис­
ключениями NullPointerExceptions и с огромным количеством проверок на
null. По факту пустой объект довольно предсказуем, потому что не несет
никакого функционала. Однако такой шаблон тоже может быть проблем­
ным, потому что вы рискуете скрыть потенциальные проблемы с данными
за счет объекта, который просто их игнорирует. Как следствие, значительно
усложняется отладка.
76

|

Глава 3

Optional
В Java 8 появился встроенный тип данных java.util .Optional, который
предназначен для информирования о наличии или отсутствии значения.
Optional поставляется с набором методов для обработки отсутствия зна­
чений, что уменьшает количество багов. Также у вас есть возможность «со­
единять» несколько объектов Optional и использовать их в качестве возвра­
щаемого значения из различных API. Пример такого использования — метод
findAnyO в Streams API. Более подробно об использовании Optional мы
расскажем в главе 7.

Тгу
Есть и другой тип данных — Тгу. Он представляет собой операцию, кото­
рая может быть либо удачной, либо нет. Фактически это аналог Optional,
только в первом случае вы работаете со значениями, а во втором — с опе­
рациями. Другими словами, тип данных Тгу дает похожие преимущества
в компоновке кода, а также снижает количество ошибок. Как ни странно,
тип Тгу не встроен в JDK, но поддерживается сторонними библиотеками,
которые легко найти.

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

Зачем нужны сборщики
Предлагаем решить проблему запуска приложения. Есть несколько момен­
тов, о которых нужно позаботиться. Во-первых, после написания самого
кода его необходимо скомпилировать. Чтобы это сделать, можно восполь­
зоваться Java-компилятором (javac). Вы помните все нужные команды для
компилирования нескольких файлов? Как насчет пакетов? Что вы скаже­
те о зависимостях при импорте сторонних библиотек? А что, если проект

Расширяем анализатор банковских операций

|

77

нужно упаковать в специальный формат вроде WAR или JAR? Согласитесь,
все быстро запутывается, и разработчику становится все сложнее и слож­
нее в этом разобраться.
Чтобы автоматизировать все команды, вам придется написать скрипт.
Так вам не понадобится каждый раз вводить их заново. Создание нового
скрипта требует того, чтобы все ваши настоящие и будущие коллеги хо­
рошо представляли ваш образ мышления и имели возможность обслужи­
вать и модернизировать скрипт по мере необходимости. Кроме того, нуж­
но учитывать жизненный цикл программного обеспечения. И это касается
не только разработки и компиляции, но также тестирования и разверты­
вания.
Решение всех этих проблем — использование сборщиков. Инструмент для
сборки — это ваш помощник, который выполняет повторяющиеся действия
в течение жизненного цикла программного обеспечения, включая разра­
ботку, тестирование и развертывание приложения. У сборщиков есть много
плюсов:



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



Они настраивают вас на типовой и стандартизированный процесс сбор­
ки и запуска приложения.



Вы гратите больше времени на разработку, а не на низкоуровневые на­
стройки.



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



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

Сейчас вы познакомитесь с двумя наиболее популярными в Java-сообще­
стве сборщиками: Maven и Gradle1.

Работа с Maven
Maven наиболее популярен в Java-сообществе. Он позволяет описать про­
цесс сборки вашего программного обеспечения вместе с зависимостями.
Кроме того, есть большой репозиторий сообщества, который Maven может
1 Раньше у Java был другой популярный сборщик под названием Ant, но сейчас он считается
«умершим» и больше не может быть использован. — Прим. авт.

78

|

Глава 3

использовать для автоматической загрузки библиотек и зависимостей. Пер­
воначально Maven был представлен в 2004 году, когда, как вы можете до­
гадаться, был очень популярен XML. Следовательно, объявление процесса
сборки в Maven основано на XML.

Структура проекта
Самое прекрасное в Maven — это то, что с самого начала он поставляется со
структурой, помогающей в обслуживании. Проект Maven начинается с двух
основных папок:

/src/main/java
Здесь вы сможете найти все классы, необходимые для вашего проекта.
src/test/java

Здесь должны располагаться все ваши тесты.
Есть еще две дополнительные папки, удобные, но не обязательные:

src/main/resources
Здесь вы можете располагать дополнительные ресурсы вашего проекта, та­
кие как текстовые файлы.
src/test/resources

Здесь вы можете располагать дополнительные ресурсы для тестов.
Применение такой схемы расположения файлов позволяет любому челове­
ку, знакомому с Maven, сразу же найти нужные файлы. Чтобы специализи­
ровать процесс сборки, вам нужно создать XML-файл, в котором указыва­
ются необходимые объявления, задающие порядок сборки приложения. На
рис. 3-2 показана типовая структура проекта Maven.

Рис. 3-2. Стандартная схема расположения папок Maven

Расширяем анализатор банковских операций

|

79

Пример сборочного файла
Следующий шаг — создание файла pom.xml, управляющего процессом сбор­
ки. Фрагмент кода, приведенный в примере 3-26, демонстрирует базовый
набор, необходимый для сборки проекта анализатора банковских опера­
ций. В этом файле вы найдете несколько элементов.
project

Это элемент верхнего уровня во всех файлах pom.xml.
groupld

Этот элемент показывает уникальный идентификатор организации, создав­
шей проект.
artifactld

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

Этот элемент указывает тип пакета, который используется данным артефак­
том (такой как JAR, WAR, EAR и т. д.). Если ничего не указано, то по умолча­
нию применяется тип JAR.
version
Версия артефакта, сгенерированного из проекта.

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

dependencies
Этот элемент определяет список зависимостей в проекте.
Пример 3-26. Файл сборки pom.xml в Maven


4.0.0

80

|

Глава 3

com.iteratrlearning
bankstatement_analyzer
l.0-SNAPSHOT




org.apache.maven.plugins
maven-compiler-plugin
3.7.0

9< /source>
9






j unit
junit
4.12
test





Команды Maven
Следующий шаг после настройки pom.xml — непосредственно использова­
ние Maven для сборки проекта. Есть несколько доступных команд. Мы рас­
скажем только об основных:
mvn clean

Очищает все предыдущие сгенерированные артефакты в предварительной
сборке.

mvn compile

Компилирует исходный код проекта (по умолчанию в созданной папке
target).
Расширяем анализатор банковских операций

|

81

mvn test
Тестирует скомпилированный исходный код.

mvn package
Запаковывает скомпилированный код в подходящий формат вроде JAR.

Например, выполнение команды mvn package из директории, в которой рас­
положен файл pom.xml, на выходе дает примерно такой результат:
[INFO]
[INFO]
[INFO]
[INFO]
[INFO]
[INFO]
[INFO]
[INFO]
[INFO]
[INFO]
[INFO]
[INFO]

Scanning for projects...

------------------------------Building bankstatement_analyzer l.O-SNAPSHOT
------------------------------------------------------------BUILDSUCCESS
------------------------------Total time: 1.063 s
Finished at: 2018-06-10T12:14:48+01:00
Final Memory: 10M/47M

Вы увидите сгенерированный JAR-фай л bankstatement_analyzer-l.O-SNAPSHOT.jar в папке target.
Если вы хотите запустить главный класс в сгенерированном артефакте
с помощью команды mvn, вам придется познакомиться с плагином ехес.

Использование Gradle
Maven — не единственный инструмент сборки, доступный в Java. Доста­
точно популярен также сборщик Gradle. Вы можете спросить: зачем нам
еще один инструмент для сборки? Разве Maven не самый распространен­
ный? Дело в том, что у Maven есть один серьезный недостаток. Это исполь­
зование XML-файлов, делающих его не самым удобным в работе. Приве­
дем пример. Во время рабочего процесса часто бывает нужно обеспечить
реализацию пользовательских системных команд, таких как копирование
82

|

Глава 3

или перемещение файлов. Определение данных команд в синтаксисе XML
совершенно неестественно. Кроме того, XML зарекомендовал себя как
«многословный» язык, что может сильно ухудшить обслуживаемость. При
этом Maven предлагает множество хороших идей вроде стандартизации
структуры проекта, которыми вдохновляется и Gradle. Одним из преиму­
ществ Gradle является использование им дружелюбного языка DSL (Domain
Specific Language — предметно-ориентированный язык), использующего
языки Groovy или Kotlin для задания параметров процесса сборки. В ре­
зультате параметризация сборки проходит более естественно, она проще
настраивается и легче воспринимается. Кроме того, Gradle поддерживает
такие полезные особенности как кэш и инкрементная компиляция, которые
способствуют сокращению времени сборки1.

Пример сборочного файла
Структура проекта в Gradle довольно проста и напоминает Maven. Одна­
ко вместо файла pom.xml вам нужно создать файл build.grade. Также есть
и файл settings.gradle, в котором находятся конфигурационные переменные
и настройки для мультипроектной сборки. В примере 3-27 показан фраг­
мент кода простого сборочного файла Gradle, эквивалентного файлу Maven
из примера 3-26. Вы должны признать, что все выглядит гораздо более ла­
конично!

Пример 3-27. Файд сборки build.gradle в Gradle
apply plugin: 'java'
apply plugin: 'application'

group = 'com.iteratrlearning'
version = ’l.O-SNAPSHOT’

sourcecompatibility = 9
targetcompatibility = 9
mainClassName = "com.iteratrlearning.MainApplication"
repositories {
mavenCentral()

1 С более подробным сравнением сборщиков Maven и Gradle можно ознакомиться на gradle.
org/maven-vs-gradle. — Прим. авт.

Расширяем анализатор банковских операций

|

83

dependencies {
testlmplementation group: 'junit', name: 'junit', version:'4.12'
}

Команды Gradle
Наконец, можно запустить процесс сборки путем выполнения команд, по­
хожих на те, что были в Maven. Каждая команда в Maven — это задача. Вы
можете определять свои собственные задачи и затем выполнять их, или
пользоваться встроенными задачами, такими как test, build и clean:
gradle clean

Очищает файлы, созданные во время предыдущей сборки.
gradle build

Упаковывает приложение.
gradle test
Запускает тесты.

gradle run
Запускает главный класс, указанный в поле mainClassName, с учетом приме­
ненных плагинов.

Ниже представлен пример результата выполнения команды gradle build:
BUILD SUCCESSFUL in Is

2 actionable tasks: 2 executed

Сгенерированный JAR-файл вы найдете в папке build, созданной Gradle
в процессе сборки.

Выводы


Принцип открытости/закрытости основан на идее возможности изме­
нения поведения метода или класса без необходимости изменения его
кода.



За счет неизменяемости существующего кода принцип открыто­
сти/закрытости снижает его хрупкость. Принцип продвигает идею

84

|

Глава 3

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



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



Слишком маленькие интерфейсы с одним методом могут ухудшить по­
казатель связности.



Вам не стоит волноваться на счет добавления «описательных» имен ме­
тодов, чтобы повысить читаемость и восприятие вашего API.



Операции, возвращающие в результате void, сложно протестировать.



Исключения в Java способствуют документированию, безопасности ти­
пов и разделению задач.



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



Слишком специфические (индивидуальные) исключения могут сделать
программирование неэффективным.



Шаблон уведомления позволяет доменному классу накапливать ошибки.



Не игнорируйте исключения или не перехватывайте общие исключения
(generic-исключения), потому что так вы лишите себя возможности бы­
строго поиска источника проблемы.



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



Maven и Gradle — два наиболее популярных в сообществе Java сбор­
щика.

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


Реализовать поддержку экспорта в различные форматы данных, вклю­
чая JSON и XML.



Разработать базовый графический интерфейс (GUI) для анализатора
банковских операций.

Расширяем анализатор банковских операций

|

85

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

ГЛАВА 4

Система управления документами

Задача
После успешной реализации анализатора банковских операций для Марка
Эрбергцука вы решили заняться своими личными делами. В том числе схо­
дить на прием к стоматологу. Доктор Авадж успешно работает уже много
лет. Зубы ее счастливых пациентов остаются здоровыми до самой старости.
Единственный минус в такой работе — это то, что с каждым годом количе­
ство документации на пациентов только растет. Каждый раз ее помощники
тратят все больше времени на поиски записей о предыдущем посещении
пациента.

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

Цель
В этой главе вы откроете для себя много новых принципов программиро­
вания. Ключ к разработке системы управления документами — это наслед­
ственные связи, подразумевающие наследование классов или реализацию
интерфейсов. Чтобы сделать все правильно, вы познакомитесь с принци­
пом подстановки Дисков (Liskov substitution principle, LSP), названным
в честь известного ученого в области компьютерных технологий Барбары
Дисков.

Система управления документами

|

87

Затем вы дополните свои знания о том, когда стоит применять наследование,
обсуждением принципа «Композиция вместо наследования». И наконец, вы
глубже изучите вопрос создания автоматизированных тестов.
Итак, узнав, что вас ожидает в этой главе, вы можете приступить к рассмо­
трению требований доктора Авадж к системе управления документами.
Если в какой-то момент вам захочется взглянуть на исходный код, вы мо­
жете найти его в репозитории по адресу: /com/iteratrlearning/shu_book/

chapter_04.

Требования к системе управления документами
За чашечкой чая доктор Авадж пояснила, что у нее есть документы, которые
она хотела бы систематизировать в виде файлов на компьютере. Система
управления документами должна иметь возможность импортировать дан­
ные файлы и создавать определенные записи о каждом из них. Доктор хо­
чет, чтобы эти записи можно было индексировать и осуществлять по ним
поиск. Ее волнует три типа документов:
Отчеты

Текст с пояснениями по лечению пациента.
Письма

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

В стоматологической практике часто присутствуют рентгеновские снимки
или фотографии зубов и десен. Они имеют определенный размер.
Кроме того, вся документация должна быть «привязана» (прописан путь)
к файлам пациентов, к которым она относится. Доктор Авадж хочет иметь
возможность поиска документов и запрашивать, содержится ли определен­
ная информация в различных типах документов. Например, найти письма,
в которых встречается фраза «Джо Блоггс».

88

|

Глава 4

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

Воплощение идеи
Для решения поставленной задачи можно воспользоваться большим коли­
чеством различных методов. Все они относительно субъективные. Поэтому
мы предлагаем вам попробовать решить задачу доктора Авадж дважды —
до и после прочтения данной главы. Из раздела «Альтернативные подходы»
вы узнаете, почему мы стараемся избегать некоторых методов, а также по­
знакомитесь с общими принципами, лежащими в их основе. Работу над лю­
бым приложением лучше всего начинать с разработки через тестирование
(TDD — Test-Driven Development), чем мы и руководствовались при написа­
нии примеров. Мы не будем рассматривать TDD до главы 5, поэтому давайте
просто хорошенько подумаем о том, как должна вести себя ваша программа,
и постепенно, шаг за шагом, напишем код, реализующий это поведение.

Система управления документами должна при необходимости импорти­
ровать документы и добавлять их в свое внутреннее хранилище. Для того
чтобы реализовать данное требование, давайте создадим класс DocumentManagementSystem и добавим в него два метода:
void importFile(String path)

Получает путь к файлу, который пользователь хочет импортировать в систе­
му управления документами. Поскольку мы имеем дело с публичным API,
который может принимать пользовательский ввод, в качестве типа данных
для пути к файлу мы воспользуемся типом String, вместо более безопасных
типов вроде java.nio.Path или java.io.File.

List contents!)

Возвращает список документов, которые в настоящее время хранит система
управления документами.
Вы заметили, что метод contents!) возвращает список объектов класса
Document. Пока мы еще не обсуждали назначение этого класса, но будем го­
ворить о нем в течение курса. А пока вы можете считать, что это пустой
класс.

Система управления документами

|

89

Импортеры
Основной характеристикой системы является возможность импорта доку­
ментов различного типа. В рамках нашей программы для определения по­
рядка импорта документов вы можете полагаться на их формат, поскольку
доктор Авадж сохраняет свои файлы в конкретных расширениях. Все ее
письма имеют расширение .letter, отчеты — .report, а для изображений ис­
пользуется только формат .jpg.

Проще всего было бы реализовать весь механизм импорта файлов в одном
методе (пример 4-1).
Пример 4-1. Пример «переключателя расширений»
switch(extension) {
case "letter":

// code for importing letters.
break;
case "report":

// code for importing reports.
break;
case "jog":

// code for importing images.
break;

default:
throw new UnknownFileTypeException("For file: " + path);

)

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

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

90

|

Глава 4

Теперь, когда мы знаем, что для импорта файлов нам нужен интерфейс, воз­
никает вопрос, в каком виде представлять файлы, подлежащие импорту?
У нас есть два варианта: использовать простой тип String для представле­
ния пути к файлу или воспользоваться классом для работы с файлами вроде
java.io.File.

Возможно, вы предпочтете вариант, соответствующий принципу жесткой
типизации: выберете тип, представляющий файлы и снижающий риск воз­
никновения ошибок. Давайте так и поступим и воспользуемся объектом
java.io.File в качестве параметра для нашего интерфейса Importer (при­
мер 4-2).
Пример 4-2. Importer
interface Importer {

Document importFile(File file) throws IOException;

I

Вы можете спросить, почему мы не можем так же использовать File и для
публичного API DocumentManagement System? Потому что в случае с нашим
приложением API, возможно, будет «обернут» в какой-нибудь пользова­
тельский интерфейс, и мы не знаем наверняка, в каком виде тот будет при­
нимать файлы. Поэтому мы, чтобы ничего не усложнять, просто используем
тип String.

Класс Document
Теперь давайте объявим класс Document. Предполагается, что каждый доку­
мент будет иметь несколько атрибутов, по которым должен осуществляться
поиск. У разных документов разный набор атрибутов. В данном случае у нас
опять же есть несколько вариантов, плюсы и минусы которых мы рассмо­
трим при создании класса.

Самым простым способом представления документов было бы использо­
вание интерфейса Map, подразумевающего хранение данных
в виде пар ключ/значение. Так почему же нам не воспользоваться этим спо­
собом и просто не передавать Map по всему приложению?
Что ж, внедрение доменного класса для моделирования документа — это
не просто слепое следование принципам ООП, но также и получение ряда
практических преимуществ в обслуживаемости и читаемости программы.
Система управления документами

|

91

Для начала скажем, что невозможно переоценить значение присвоения
конкретных имен компонентам программы. Коммуникация — наше все!
Хорошие команды разработчиков используют некий единый язык для опи­
сания своего программного обеспечения. Если словарный запас, которым
вы пользуетесь при написании приложения, совпадает со словарным за­
пасом, который вы используете при общении с клиентами вроде доктора
Авадж, — вам становится гораздо проще работать с приложением. В про­
цессе разговора с коллегой или с клиентом вам непременно придется прий­
ти к какой-то общей терминологии, при помощи которой вы будете опи­
сывать приложение. Если вы примените то же правило к коду, вам будет
значительно легче понять, с какой частью кода нужно работать. Это назы­
вается открытостью.

Понятие «единого языка» было предложено Эриком Эвансом и берет свое
начало в предметно-ориентированном программировании. Оно подразу­
мевает использование общего языка, разработанного для общения между
разработчиками и пользователями.

Еще один принцип, который должен побудить вас внедрить дополнитель­
ный класс для создания модели документа — это жесткая типизация. Мно­
гие относятся к данному понятию как к свойству языка программирования,
но здесь мы говорим о более практической стороне использования стати­
ческой типизации в разработке программного обеспечения. Типы позволя­
ют нам ограничить область использования данных. К примеру, наш класс
Documents является неизменяемым. После его создания вы не можете в нем
что-то изменить, или модернизировать какой-то из его атрибутов. Реализа­
ции нашего интерфейса Importer создают документы. Их больше ничего не
изменяет. Если вы вдруг обнаруживаете экземпляр Document с ошибкой в од­
ном из его атрибутов, то можете легко сузить круг подозреваемых реали­
заций Importer. Еще один плюс неизменяемости: вы можете индексировать
или кэшировать любую информацию, связанную с экземплярами Document
и не сомневаться в том, что эта информация всегда будет корректной, так
как документы неизменяемы.
Другой вариант, который могут выбрать разработчики при проектировании
Document, — это сделать его расширением HashMap. Поначалу
идея кажется прекрасной, поскольку в HashMap имеется весь необходимый
функционал для моделирования Document. Однако есть несколько причин
считать эту идею плохой.

92

|

Глава 4

Разработка программного обеспечения часто подразумевает ограничение
функциональности чего-либо с целью достижения желаемого результата.
Мы просто выбросили бы на помойку все вышеперечисленные преимуще­
ства неизменяемости, позволив чему угодно в приложении изменять класс
Document, если бы мы сделали его подклассом HashMap. Использование коллек­
ций также дает нам возможность давать методам «значащие» имена вместо
того, чтобы, к примеру, искать атрибут через вызов метода get () — что, по
сути, ни о чем нам не говорит. Чуть позже мы подробно поговорим о конку­
ренции между наследованием и композицией, потому что сейчас мы имеем
дело с прекрасным примером, подходящим для обсуждения данной темы.
Если говорить коротко, доменные классы позволяют именовать элементы
и ограничивать рамки поведения и изменения значений этих элементов,
что повышает открытость («понятность») кода, снижает вероятность ба­
гов. Итак, в конце концов мы принимаем решение моделировать Document
так, как показано в примере 4-3. Возможно, вы удивлены, что тип класса не
public, мы обсудим это позже в разделе «Выбор области действия и инкап­
суляции».

Пример 4-3. Document
public class Document {
private final Map attributes;

Document (final Map attributes) {
this.attributes = attributes;
)
public String getAttribute (final String attnbuteName) {
return attributes.get (attnbuteName);

)

)

Следует обратить внимание на еще один момент касательно Document: у него
пакетный конструктор. Обычно, классы в Java имеют конструкторы типа
public, однако в данном случае это может оказаться плохим решением, по­
скольку позволит коду в любом месте программы создавать объекты такого
типа. Только код в системе управления документами должен иметь право
создавать объекты Document, поэтому мы делаем конструктор пакетным
и ограничиваем доступ, предоставляя его только тому пакету, в котором на­
ходится наша система управления документами.

Система управления документами

|

93

Атрибуты и иерархия Documents
В классе Document мы используем атрибуты типа String. Разве это не проти­
воречит принципам строгой типизации? И да и нет. Мы храним атрибуты
в виде текста, так что по ним можно делать поиск. Кроме того, мы должны
быть уверены, что все атрибуты созданы в правильной изначально заданной
форме, не зависящей от создавшего их импортера. Применение String — не
такое уж и плохое решение в данном случае. Стоит помнить, что передавать
String в приложении с целью представления информации — определенно
плохая идея. Это уже не строгая, а очень жесткая типизация.
В частности, если имеет место более сложное использование атрибутов, в та­
ком случае можно подумать об использовании атрибутов различного типа.
К примеру, если бы мы захотели найти адреса, расположенные на опреде­
ленном расстоянии, или изображения, ширина и высота которых меньше
заданной, тогда наличие строго типизированных атрибутов было бы кстати.
Гораздо проще сравнивать значение ширины, если оно представлено как це­
лое число. В случае с нашей системой управления документами нам просто
не нужен такой функционал.
Можно спроектировать систему управления документами с классовой
иерархией Documents, которая моделировала бы иерархию Importer. Напри­
мер, Reportimporter импортирует объекты класса Report, которые являются
расширением класса Document. Такой подход проходит нашу стандартную
проверку на разумность введения подклассов. Другими словами, данная
схема позволяет утверждать, что Report — это Document, и это утверждение
имеет смысл. Однако мы решили не идти этим путем, поскольку правиль­
ным подходом при конструировании классов в ООП будет учитывать и его
поведение, и данные.

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

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

94

|

Глава 4

говорит: «Нам может понадобиться X» или «было бы хорошо сделать Y» —
просто скажите: «Нет». Раздутые и сложные конструкции вымощены благи­
ми намерениями, направленными на расширяемость и создание кода, кото­
рый «приятно иметь», а не «обязательно иметь».

Реализация и регистрация импортеров
Вы можете реализовать интерфейс Importer с целью поддержки различ­
ных типов файлов. В примере 4-4 показан способ импорта изображений.
Одним из больших преимуществ стандартной библиотеки Java является то,
что она предоставляет очень много функциональных возможностей прямо
«из коробки». Здесь мы считываем файл изображения при помощи метода
ImagelO.read, а затем извлекаем ширину и высоту этого изображения из ре­
зультирующего объекта Buf feredlmage.

Пример 4-4. Imagelmporter
import static com.iteratrlearning.shu_book.chapter_04.Attributes.*;
class Imagelmporter implements Importer {

^Override
public Document importFile(final File file) throws IOException {
final Map attributes = new HashMap();

attributes.put(PATH, file.getPath());
final Bufferedlmage image = ImagelO.read(file);

attributes.put(WIDTH, String.valueOf(image.getWidth()));
attributes.put(HEIGHT, String.valueOf(image.getHeight()));
attributes.put (TYPE, "IMAGE");
return new Document(attributes);

)
}

Имена атрибутов заданы в виде констант в классе Attributes. Это исключает
баги в случае, если разные импортеры будут использовать различные стро­
ки для одного и того же имени атрибута. Например, «Path» вместо «path».
В самом языке Java нет непосредственно понятия константы как таковой,
пример 4-5 показывает наиболее часто используемый прием. В данном слу­
чае константа имеет тип public, потому что мы хотим использовать ее из

Система управления документами

|

95

разных импортеров, хотя вы могли бы использовать private или даже па­
кетную константу. Ключевое слово final дает уверенность в том, что значе­
ние константы не будет переопределено, a static — в том, что у нее может
быть только один экземпляр на класс.
Пример 4-5. Как объявить константу в Java
public static final String PATH = "path";

Существуют импортеры для всех трех типов файлов, но оставшиеся два
вы увидите в разделе «Расширение и повторное использование кода». Не
волнуйтесь, мы ничего не прячем в рукавах. Вообще, для использования
классов Importer в процессе импорта файлов нам нужно зарегистрировать
импортеры, чтобы увидеть их. Мы используем расширение файла, который
хотим импортировать, в качестве ключа для объекта Мар (пример 4-6).
Пример 4-6. Регистрация импортеров
private final Map extensionToImporter = new HashMap();
public DocumentManagementSystemO {

extensionToImporter.put("letter", new Letterimporter());
extensionToImporter.put("report", new Reportimporter());
extensionToImporter.put("jpg", new Imageimporter());

Теперь, когда вы знаете, как импортировать документы, можно заняться по­
иском. Мы не задаемся целью создать самый эффективный поиск хотя бы
потому, что не создаем Google. Нам нужно просто получать информацию,
которую запрашивает доктор Авадж. Из разговора с ней вы поняли, что она
хочет иметь возможность просматривать информацию о разных атрибутах
Document.
Требования доктора Авадж можно выполнить, просто учитывая последо­
вательности атрибутов. Допустим, ей понадобится найти документы, отно­
сящиеся к пациенту по имени Джо и содержащие упоминание диетической
колы в тексте. Для этого мы разработали очень простой язык запросов. За­
просы состоят из последовательности имен атрибутов и значений, разделен­
ных запятыми. Такой запрос выглядит примерно так: "patient: Joe, body: Diet
Coke".

96

|

Глава 4

Поскольку наш алгоритм поиска должен оставаться простым, а не макси­
мально эффективным, то он просто последовательно сканирует все записи
в системе и проверяет их на соответствие запросу.
Строка запроса передается методу search, преобразовывается в объект
Query, который потом можно тестировать на соответствие каждому экзем­
пляру Document.

Принцип подстановки Дисков
Мы обсудили некоторые определенные решения в разработке, касающиеся
классов. Например, мы рассмотрели вопрос моделирования импортеров
с помощью классов, поговорили о том, почему не стоит вводить иерархию
классов для класса Document или почему лучше не делать Document просто
расширением HashMap. На самом деле все это подводит нас к более важному
принципу. К принципу, позволяющему обобщить указанные выше приме­
ры и объединить их в один подход, который вы можете применять в любом
программном продукте. Он называется принципом подстановки Дисков
(Liskov Substitution Principle — LSP) и помогает нам понять, как правиль­
но организовывать связь и реализовывать интерфейсы. LSP — третий из
принципов SOLID, к которым мы будем обращаться на протяжении всей
книги.
Принцип подстановки Дисков часто формулируется в довольно сложных
формальных терминах, но на самом деле он очень прост. Давайте немного
проясним терминологию. В данном контексте встречаясь со словом тип,
думайте о классе или об интерфейсе. Термин подтип подразумевает уста­
новленные наследственные отношения (родитель — ребенок) между типа­
ми. Другими словами, является расширением класса или реализацией ин­
терфейса. Проще говоря, вы можете считать, что дочерние классы должны
реализовывать поведение, которое они наследуют от своих родителей. Зна­
ем, знаем — звучит весьма очевидно, но сейчас мы конкретизируем кое-что
и разобьем принцип LSP на четыре части:

LSP
Пусть q(x) — свойство, доказуемое для объектов х типа Т. Тогда q(y) должно
быть истинно для объектов у типа S, где S — подтип Т.

Система управления документами

|

97

Предварительные условия не могут быть усилены в подтипе

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

Пример 4-7. Определение importFile
public void importFile(final String path) throws IOException {
final File file = new File (path);
if (!file.exists ()) {
throw new FileNotFoundException(path) ;

}
final int separatorindex = path.lastlndexOf('.');
if (separatorindex != -1) {
if (separatorindex == path.length()) {
throw new UnknownFileTypeException("No extension found For file: "

+ path);

}
final String extension = path.substring(separatorindex + 1);
final Importer importer = extensionToImporter.get(extension);
if (importer == null) {
throw new UnknownFileTypeException("For file: " + path);

)
final Document document = importer.importFile(file);

documents.add(document);
) else {
throw new UnknownFileTypeException("No extension found For file: " +

path);

I

}
Принцип LSP подразумевает, что вы не можете требовать больше ограни­
чивающих предусловий, чем в родительском элементе. Так, например, вы
не можете требовать в дочернем объекте, чтобы размер файла был меньше
100 КБ, если родительский класс может импортировать документы любого
размера.

98

|

Глава 4

Постусловия не могут быть ослаблены в подтипе
Это утверждение может ввести вас в замешательство, потому что звучит
почти так же, как и первое. Постусловия — это условия, которые должны
быть истинны после выполнения определенного кода. Например, после
выполнения importFile (), если запрашиваемый файл не был поврежден,
он должен находиться в списке документов, возвращаемом contents ().
Таким образом, если родительский элемент производит какие-то дей­
ствия или возвращает какие-то значения, дочерний элемент должен де­
лать так же.

Инварианты сверхтипа должны быть сохранены в подтипе

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

Правило истории

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

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

Система управления документами

|

99

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

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

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

Область действия и инкапсуляция
Если вы уделили достаточно времени изучению кода, то наверняка обратили
внимание, что интерфейс Importer, его реализации и класс Query находятся
в области действия пакета. Область действия пакета — это область действия
по умолчанию. Поэтому, если вы видите файл класса с class Query в начале,
вы знаете, что он в области действия пакета, если же вы видите public class
Query, вы понимаете, что класс имеет публичный доступ. Пакетная область
действия подразумевает, что только другие классы в том же пакете могут
«видеть» или иметь доступ к данному классу, больше никто не может. Похо­
же на маскировку.

Странная особенность экосистемы Java: несмотря на то, что областью дей­
ствия по умолчанию установлена пакетная область действия, каким бы
проектом мы ни занимались, мы всегда встречаем больше public-классов,
чем классов с пакетной видимостью. По идее, по умолчанию должна быть
видимость public. Однако на самом деле пакетная видимость — довольно
удобный инструмент. Он позволяет инкапсулировать некоторые решения
в проектировании. За счет правильного применения пакетной видимости
вы можете пресечь попытки классов вне пакета получить информацию
о внутренней реализации.

100

|

Глава 4

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

Расширение и повторное использование кода
Что касается программного обеспечения, постоянны только изменения. Че­
рез какое-то время вы можете захотеть добавить новые возможности в свой
продукт, следуя своему желанию, требованиям заказчика или даже измене­
ниям в правилах и регламентах. Как уже говорилось ранее, доктор Авадж со
временем планирует расширить перечень документов, загружаемых в систе­
му. На самом деле когда мы впервые продемонстрировали ей разработанное
программное обеспечение, она сразу поняла, что хотела бы иметь возмож­
ность выставлять при помощи нашей системы счета клиентам. Счет — это
документ с некоторым содержимым (телом) и суммой, имеющий расшире­
ние .invoice, В примере 4-8 показан пример счета.

Пример 4-8. Пример счета
Уважаемый Джо Блоггс!
Это счет за предоставленные вам услуги стоматолога.

Сумма: 100$

С наилучшими пожеланиями,

Замечательный стоматолог,
Доктор Авадж.

К счастью для нас, все счета доктора Авадж имеют один формат. Как вы по­
нимаете, нам нужно извлечь сумму из текста. Строка с суммой начинается
с префикса Сумма:. Имя клиента находится в начале письма и начинается со
слова Уважаемый. По сути, наша система должна реализовать общий метод

Система управления документами

|

101

поиска суффикса в строке, начинающейся с заданного префикса, как пока­
зано в примере 4-9. В данном примере поле lines уже было инициализиро­
вано и хранит строки из файла, который мы импортировали. Мы передаем
методу префикс. Например, Сумма:. Он ассоциируется с окончанием стро­
ки — суффиксом, с указанным именем атрибута.
Пример 4-9. Определение addLineSuffix
void addLineSuffix(final String prefix, final String attributeName) {
for(final String line: lines) {
if (line.startsWith(prefix)) (

attributes.put(attributeName, line.substring(prefix.length()));
break;

)
)
}

На самом деле похожую концепцию мы использовали, когда импортирова­
ли письмо. Рассмотрим образец письма в примере 4-10. Здесь нам нужно
извлечь имя пациента из текста путем поиска строки, начинающейся со сло­
ва Уважаемый. В письмах также есть адреса и блоки текста, которые нужно
извлечь из содержимого текстового файла.
Пример 4-10. Пример письма
Уважаемый Джо Блоггс!

Фейк стрит, 123
Вестминстер
Лондон
Великобритания

Мы написали вам это письмо, чтобы подтвердить перенос посещения доктора Авадж с 29
декабря 2016 на 5 января 20Р.

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

Похожая проблема возникает с импортом отчетов о пациентах. Отчеты док­
тора Авадж перед именем пациента содержат префикс Пациент:. Кроме того,

102

|

Глава 4

в них есть блоки текста, точно как и в письмах. Образец отчета вы можете
увидеть в примере 4-11.
Пример 4-11. Пример отчета
Пациент: Джо Блоггс

5 января 2017 я осматривала зубы Джо.
Мы обсуждали его переход с обычной колы на диетическую.
Новых проблем с зубами не обнаружено.

Таким образом, все три текстовых импортера могли бы реализовывать один
и тот же метод для поиска суффиксов в текстовых строках с заданным пре­
фиксом (смотрите пример 4-9). Если бы доктор Авадж платила нам за коли­
чество строк написанного кода, мы могли бы утроить сумму, проделав одну
и ту же работу три раза! Неплохая стратегия!

К сожалению (а может, и к счастью), заказчики редко платят за количество
строк написанного кода. А вот что действительно имеет значение — так это
требования заказчика к продукту. Конечно, нам хотелось бы иметь возмож­
ность повторно использовать один и тот же код для всех трех импортеров.
И это можно сделать, поместив наш код в класс. И тут мы сталкиваемся с тре­
мя возможными вариантами, каждый из которых имеет свои плюсы и минусы.
Давайте рассмотим их и подумаем, какой выбрать. Итак, вот наши варианты:


Использовать служебный класс.



Применить наследование.



Использовать доменный класс.

Самый простой вариант — создать служебный класс. Его можно назвать
ImportUtil. В таком случае каждый раз, когда вам понадобится метод для
использования в разных импортерах, вы сможете обращаться к данному
служебному классу. В конечном итоге ваш служебный класс превратится
в мешок со статическими методами.
Хотя использование служебного класса — вариант замечательный и про­
стой, его совершенно точно нельзя назвать вершиной объектно-ориентиро­
ванного программирования. ООП включает в себя концепции моделирова­
ния при помощи классов. Если вы хотите создать что-то (thing), то просто
вызываете new Thing () для того, что вам нужно. Атрибуты и поведение, ас­
социированные с этим «чем-то», — это методы класса Thing.

Система управления документами

|

103

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

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

Итак, что же вам делать, если вы хотите, чтобы ваш код соответствовал
упомянутым концепциям? Что ж, следующим наиболее очевидным подхо­
дом может быть использование наследования. В таком случае у вас будет
несколько импортеров, расширяющих класс Textimporter. Весь основной
функционал вы сможете реализовать в этом классе и повторно использо­
вать его в подклассах.
Наследование — это удивительно устойчивый подход в различных услови­
ях разработки. Вы уже познакомились с принципом подстановки Дисков
и ограничениями, которые он накладывает на наследственные отношения.
На практике наследование не всегда применимо, потому что зачастую не
способно моделировать реальные отношения.

В данном случае Text Importer — это Importer, и мы можем быть уверены,
что наши классы соответствуют принципу LSP, и все же такой подход нельзя
назвать лучшим. Проблема наследственных отношений, которые не в пол­
ной мере соответствуют отношениям между объектами в реальной жизни,
заключается в том, что они имеют тенденцию к хрупкости. Поскольку ваше
приложение с течением времени развивается, вам нужны абстракции, спо­
собные развиваться вместе с приложением, а не против него. Как правило,
не стоит вводить наследование только для того, чтобы обеспечить повтор­
ное использование кода.
Последний вариант — смоделировать текстовый файл при помощи до­
менного класса. Чтобы воспользоваться данным подходом, мы могли бы
смоделировать некоторую базовую концепцию и выстроить различные
импортеры, вызывая методы поверх базовой концепции. Итак, поскольку
мы пытаемся манипулировать содержимым текстовых файлов, давайте на­
зовем класс TextFile. Не очень оригинально или креативно, но зато именно

104

|

Глава 4

то, что нам нужно. Теперь вы знаете, где находится функционал для работы
с текстовыми файлами.
В примере 4-12 показано объявление данного класса и его полей. Обрати­
те внимание, что это не подкласс Document, потому что документ не должен
быть связан только с текстовыми файлами — ведь мы можем импортиро­
вать также бинарные файлы в виде картинок. Мы имеем дело просто с клас­
сом, который моделирует базовую концепцию текстового файла и имеет со­
ответствующие методы извлечения данных из текстовых файлов.

Пример 4-12. Объявление класса TextFile
class TextFile {
private final Map attributes;
private final List lines;

// Продолжение кода...

Именно такой подход мы применяем в случае с импортерами. Мы считаем,
что он позволяет нам достаточно гибко моделировать основную проблему.
Он не привязывает нас к хрупкой иерархии наследования, но по-прежнему
позволяет повторно использовать код. В примере 4-13 показано, как импор­
тировать счета. Добавлены суффиксы для имени и суммы, а также добавлен
тип счета.
Пример 4-13. Импортирование счетов
^Override
public Document importFile(final File file) throws lOException {
final TextFile textFile = new TextFile(file);

textFile.addJAneSuffix(NAME_PREFIX, PATIENT);
textFile.addLineSuffix(AMOUNT_PREFIX, AMOUNT);
final Map attributes = textFile.getAttributes();

attributes.put(TYPE, "INVOICE");
return new Document(attributes);

)

Ниже представлен другой пример импортера, который использует класс
TextFile (пример 4-14). Не стоит волноваться о том, как реализован метод
TextFile.addLines. Это объясняется в примере 4-15.

Система управления документами

|

105

Пример 4-14. Импортирование писем
^Override
public Document importFile(final File file) throws IOException {
final TextFile textFile = new TextFile(file);

textFile.addLineSuffix(NAME_PREFIX, PATIENT);
final int lineNumber = textFile.addLines(2, String::isEmpty, ADDRESS);

textFile.addLines(lineNumber + 1, (line) -> line.startsWith("regards,"),

BODY);
final Map attributes = textFile.getAttnbutes();

attributes.put(TYPE, "LETTER");
return new Document(attributes);
)

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

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

Мы не говорим, что копирование кода — это хорошо. Ни в коем случае.
Но иногда при создании новых классов лучшепродублировать небольшой
фрагмент кода. Когда вы реализуете большую часть приложения, правиль­
ная абстракция (такая как TextFile) станет очевидной. Идти по пути устра­
нения дублирования стоит только тогда, когда вы знаете, как это правильно
сделать.
В примере 4-15 показана реализация метода TextFile.addLines. Это общий
код, используемый различными реализациями импортеров. Его первый
аргумент — индекс start — говорит, с какого номера строки начать. Затем
106

|

Глава 4

идет элемент isEnd, который применяется к строке и возвращает true, если
мы достигли конца строки. В конце идет имя атрибута, которое мы собира­
емся ассоциировать с этим значением.

Пример 4-15. Объявление addLines
int addLines(
final int start,
final Predicate isEnd,
final String attributeName) {

final StringBuilder accumulator = new StnngBuilder ();
int lineNumber;
for (lineNumber = start; lineNumber < lines.size (); lineNumber++) {
final String line = lines.get(lineNumber) ;
if (isEnd.test(line)) {
break;

)
accumulator.append(line) ;
accumulator.append("\n") ;

}
attributes.put(attributeName, accumulator.toString().trim());
return lineNumber;

Гигиена тестов
Как вы узнали из главы 2, написание автоматизированных тестов дает много
преимуществ с точки зрения обслуживаемости программного обеспечения.
Тесты позволяют нам уменьшить количество отказов и понять, что явилось
их причиной. Кроме того, они позволяют безопасно модернизировать код.
При этом тесты — не панацея, конечно. Чтобы пользоваться всеми «блага­
ми» тестов, первоначально нужно их создать, а затем и обслуживать. Как
вам известно, написание и обслуживание кода — дело непростое, и многие
разработчики отмечают, что создание автоматизированных тестов может
занимать немало времени.
Строго говоря, для того чтобы решить проблему поддержки тестов, вам
нужно придерживаться определенных правил тестовой гигиены. Тестовая
гигиена подразумевает необходимость сохранения кода тестов «чистым»,

Система управления документами

|

107

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

Именование тестов
Первое, о чем стоит задуматься, когда вы приступаете к работе над теста­
ми — их имена. Порой разработчики становятся слишком самоуверенными
в вопросах имен. Это тема, о которой можно говорить вечно, поскольку по­
чти каждый из нас сталкивается с данной проблемой и, конечно, имеет соб­
ственное мнение на этот счет. На наш взгляд, важно всегда помнить о том,
что где-то есть действительно подходящее имя, и ни в коем случае не забы­
вать о существовании большого количества неудачных вариантов.

Первым тестом, который мы написали для системы управления документа­
ми, стал тест, проверяющий, что мы корректно импортируем файл и создаем
экземпляр Document. Он был написан до того, как мы внедрили концепцию
Importer, поэтому он не тестирует атрибуты, ориентированные на Document.
Его код приведен в примере 4-16.
Пример 4-16. Тест импорта файлов
@Test
public void shouldlmportFile() throws Exception

(
system.importFile(LETTER) ;
final Document document = onlyDocument();

assertAttributeEquals(document, Attributes.PATH, LETTER);

}

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

108

|

Глава 4

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

Стоит отметить, что в именовании существует много антишаблонов. Наи­
худший из них — назвать тест каким-нибудь совершенно бессмысленным
словом, например testl. Что тестирует этот testl? Терпение читателей? От­
носитесь к людям, читающим ваш код, так, как вы хотели бы, чтобы они
относились к вам.
Другой распространенный антишаблон — называть тесты именами какихнибудь ключевых элементов вроде file или document. Имя теста должно
описывать тестируемое поведение, а не объект. Еще один антишаблон — на­
звать тест именем метода, который вызывается в процессе тестирования.
Например, importFile.

Вы можете спросить: разве, называя тест shouldlmportFile, мы не грешим
тем же самым? В этом обвинении есть доля истины, однако в данном слу­
чае мы все-таки описываем поведение, которое тестируется. По сути, метод
importFile тестируется несколькими тестами. Например, shouldlmportLetterAttributes, shouldlmportReportAttributes и shouldlmportlmageAttributes.
Ни один из них не называется importFile — все они описывают какое-то
более конкретное поведение.
Хорошо. Теперь вы знаете, как выглядят плохие имена. А что насчет хоро­
ших? При присваивании имен тестам необходимо следовать трем базовым
правилам:

Используйте терминологию домена

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

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

|

109

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

Придумывая имена тестам, вы можете пользоваться принципом, примененным
в DocumentManagementSystemTest — использовать слово should в качестве пре­
фикса. А можете и не пользоваться. Это вопрос ваших личных предпочтений.

Поведение, а не реализация
При написании теста для класса, компонента или даже целой системы, необхо­
димо затрагивать только общедоступное {публичное) поведение тестируемого
элемента. В случае с системой управления документами мы тестируем толь­
ко публичный API при помощи теста DocumentManagementSystemTest. В дан­
ном случае мы тестируем публичный API класса DocumentManagementSystem и,
таким образом, систему в целом. API представлен в примере 4-17.
Пример 4-17. Публичный API класса DocumentManagementSystem
public class DocumentManagementSystem

{
public void importFile(final String path) {

)
public List contents () {

}
public List search(final String query) {

)
}

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

110

|

Глава 4

ошибок разработчиков, которая приводит к возникновению сложных в об­
служивании тестов. Завязывание теста на мелкие детали делает его хруп­
ким, ведь если в будущем вы измените реализацию этих самых деталей, тест
может начать выдавать отрицательные результаты даже при корректном по­
ведении объекта. Давайте рассмотрим тест, приведенный в примере 4-18.

Пример 4-18. Тест импортера писем
@Test
public void shouldlmportLetterAttributes () throws Exception

{
system.importFile(LETTER) ;
final Document document = onlyDocument();

assertAttributeEquals(document, PATIENT, JOE_BLOGGS);
assertAttributeEquals(document, ADDRESS,
"123 Fake Street\n" +
"Westminster\n" +
"London\n" +
"United Kingdom");
assertAttributeEquals(document, BODY,
"We are writing to you to confirm the re-scheduling of your appointment
\n" +
"with Dr. Avaj from 29th December 2016 to 5th January 2017.");
assertTypels("LETTER", document);
)

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

Система управления документами

|

111

документов, полученных из метода contents (). Это не свойство, которое
ограничивается публичным API класса DocumentManagementSystem, но то,
с чем нужно быть осторожным.
Общим антишаблоном в этом отношении является раскрытие приватного
состояния какого-либо объекта за счет геттеров или сеттеров просто для
того, чтобы упростить тестирование. Вам стоит пытаться избегать подоб­
ного подхода насколько возможно, поскольку он делает ваши тесты хруп­
кими. Если вы прибегнете к нему, чтобы совсем чуть-чуть упростить тест,
в будущем это может привести к большим сложностям при обслуживании.
Так происходит потому, что любое изменение кода, предусматривающее из­
менение отображения внутреннего состояния объекта, требует от вас и из­
менения теста.

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

Не повторяйтесь
В разделе «Расширение и повторное использование кода» обсуждается во­
прос, как избавиться от дублирования кода в вашем приложении и куда при
этом поместить полученный код. Та же самая логика применима и к тестам.
К сожалению, разработчики часто не уделяют должного внимания вопросам
дублирования в тестах так, как они делают это в основном коде программы.
Если вы посмотрите на пример 4-19, то увидите тест, который повторно де­
лает утверждения для различных атрибутов, возвращаемых Document.
Пример 4-19. Тест для импорта изображений
@Test
public void shouldlmportlmageAttnbutes () throws Exception

(

system.importFile(XRAY) ;
final Document document = onlyDocument();

assertAttributeEquals(document, WIDTH, "320");
assertAttributeEquals(document, HEIGHT, "179");
assertTypels("IMAGE", document);

112

Глава 4

В обычной ситуации вам нужно было бы искать имя каждого атрибута и де­
лать утверждение о соответствии его ожидаемому значению. В случае с при­
веденными тестами мы имеем дело с достаточно распространенной опера­
цией, когда данную логику реализует общий метод assertAttributeEquals.
Реализация метода показана в примере 4-20.

Пример 4-20. Реализация нового утверждения
private void assertAttributeEquals (
final Document document,
final String attributeName,
final String expectedValue)

assertEquals(
"Document has the wrong value for " + attributeName,
expectedValue,
document.getAttribute(attributeName) );

I

Хорошая диагностика
Плох тот тест, который не дает отрицательный результат. На самом деле,
если вы никогда не видели отрицательного результата, как вы вообще мо­
жете знать, что ваш тест работает? При написании тестов лучше всего опти­
мизировать случаи отрицательного результата. Говоря «оптимизировать»,
мы не имеем в виду ускорить работу теста при отказе. Мы имеем в виду —
убедиться, что тест написан таким образом, что вы четко понимаете, как
и в каких случаях он дает отрицательный результат. Секрет этого фокуса —
в хорошей диагностике.

Под диагностикой мы понимаем сообщение или информацию, которая
выводится на экран при провале теста. Чем конкретнее сообщение о том,
что пошло не так, тем проще устранить причину отказа. Вы можете спро­
сить, зачем беспокоиться об этом, ведь большинство современных тестов
в Java выполняются в современных IDE (средах разработки), в которые уже
встроены отладчики? Что ж, иногда тесты могут выполняться в средах не­
прерывной интеграции, и иногда это может происходить при помощи ко­
мандной строки. Даже если вы выполняете тестирование из-под IDE — все
равно очень полезно иметь полную диагностическую информацию. Наде­
емся, что мы убедили вас в необходимости качественной диагностики. Но
как это выглядит на деле?
Система управления документами

|

113

В примере 4-21 показан метод, который делает утверждение, что систе­
ма содержит только один документ. Немного позже мы поясним метод
hasSize ().

Пример 4-21. Тест, проверяющий, что система хранит только один
документ
private Document onlyDocument()

(
final List documents = system.contents ();

assertThat(documents, hasSize (1));
return documents.get (0);

Простейшим видом утверждений, предлагаемым JUnit, является
assertTrue (), который принимает булево значение и ожидает, что оно ис­
тинно. В примере 4-22 показывается, как нужно использовать assertTrue,
чтобы реализовать тест. В данном случае значение проверяется на эквива­
лентность нулю. Это приводит к провалу теста shouldlmportFile и демон­
стрирует диагностику с отрицательным результатом. Проблема в том, что
здесь мы не получаем действительно хорошую диагностическую информа­
цию. Просто сообщение AssertionError без информации (рис. 4-1). Вы не
знаете, что именно пошло не так, не знаете, какие именно значения прове­
рялись. Вы не знаете ничего.

Пример 4-22. Пример с assertTrue
assertTrue(documents.size () == 0);


ft

&

Рис. 4-1. Скриншот с отрицательным тестом assertTrue

Самым распространенным утверждением является assertEquals, которое
берет два значения и проверяет их на равенство. Оно перегружается для
проверки примитивных значений. Таким образом, мы можем проверить,
что размер документа равен нулю (пример 4-23). Это дает нам чуть более

114

|

Глава 4

подробные диагностические результаты (рис. 4-2). Мы знаем, что ожидае­
мое значение было 0, полученное значение — 1. Однако мы по-прежнему не
владеем полным объемом информации.

Пример 4-23. Пример применения assertEquals
assertEquals(О, documents.size());

Рис. 4-2. Скриншот с отрицательным тестом assertEquals

Лучший способ сделать утверждение о размере коллекции — использовать
матчер, он позволяет получить более детальную диагностическую инфор­
мацию. В примере 4-24 показано применение данного способа, а также де­
монстрируется его результат. Как видно по рис. 4-3, уже становится понят­
нее, что именно пошло не так.

Пример 4-24. Пример использования assertThat
assertThat(documents, hasSize (0));

1 tot railed-25&ПЗ

/rlcb«rW/Fr*fгеи/jttl. 1. а/bln/ja
jlvl.l>ng.*Ii«rtloFError;
Exwrted: a callection with alia «*>
but: collection alia w»» {});
businessRuleEngine.addAction (() -> {});
assertEquals (2, businessRuleEngine.count ());

При запуске тестов вы увидите, что они завершаются неудачно, выбрасывая
исключение UnsupportedOperationException, как показано на рис. 5-3.

-117]

r/J

Tad
▼ О BustnettRUeEngineTaat
Mm*
О »huuW»tav«e«r,nu»a»WhenMotAc Um»
О thnuldAddTwoActtan»»
Im.

в. lang.lAuwpnrttMaerat tan£xcc*t lan
at r saeend. Butlr« iI Au leEnglntTt11. thou UHaidMHu 1и*вй»tAddingActinn» IBatina.Ta. разделяет
параметр от тела лямбда-выражения, которое представляет собой участок
кода, выполняемого в момент публикации твута.
Еще одно различие между этим примером и анонимным классом заключа­
ется в объявлении переменной события. Ранее нам нужно было явно указы­
вать его тип: Twoot twoot. В этом примере мы не указывали тип вовсе, при
этом пример компилируется. Что происходит под капотом? Компилятор
javac выводит тип переменной из контекста. В конкретно нашем случае —
из сигнатуры onTwoot. Это значит, что вам не нужно явно указывать тип,
если он очевиден.

Расширение Twootr

|

191

IK

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

Ссылки на методы
Одно из самых распространенных выражений — это лямбда-выражение,
вызывающее метод по его параметру. Если нам нужно лямбда-выражение,
которое получает содержимое объекта Twoot, необходимо написать что-то
вроде того, что показано в примере 7-14.

Пример 7-14. Получение содержимого твута
twoot -> twoot.getContent()

Данное выражение является настолько популярным, что для него даже су­
ществует сокращенный синтаксис, позволяющий повторно использовать
существующий метод, и это называется ссылкой на метод. Если бы мы хоте­
ли переписать предыдущее лямбда-выражение с использованием ссылочно­
го метода, то мы бы получили выражение, показанное в примере 7-15.
Пример 7-15. Ссылка на метод
Twoot::getContent

Стандартная форма выглядит как Classname: :methodName. Помните, что, хотя
это и метод, вам не нужны скобки, поскольку, по сути, вы не вызываете ме­
тод. Вы устанавливаете эквивалент лямбда-выражения, которое можно вы­
зывать для вызова метода. Вы можете использовать ссылки на методы вместо
лямбда-выражений, а также вызывать конструкторы при помощи описанно­
го выше синтаксиса. Если бы вы использовали лямбда-выражение для созда­
ния SenderEndPoint, это выглядело бы так, как показано в примере 7-16.

Пример 7-16. Лямбда для создания нового SenderEndPoint
(user, twootr) -> new SenderEndPoint(user, twootr)

To же самое можно написать с использованием ссылок на методы (при­
мер 7-17).
192

|

Глава 7

Пример 7-17. Ссылка на метод для создания SenderEndPoint
SenderEndPoint::new

Данный код не только короче, но и легче читается. Фрагмент Sen­
derEndPoint:: new незамедлительно говорит вам, что вы создаете новый
объект SenderEndPoint, и освобождает вас от необходимости изучать всю
строку кода. Кроме того, ссылки на методы автоматически поддерживают
множественные параметры, пока у вас есть правильный функциональный
интерфейс.
Когда мы знакомились с изменениями Java 8, наш друг сказал, что ссылки на
методы «выглядят как обман». Он имел в виду, что, посмотрев на то, как мы
можем использовать лямбда-выражения для передачи фрагментов кода так,
как будто это данные, возможность ссылаться на метод напрямую — это
обман.

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

Execute Around
Execute Around — очень распространенный шаблон в функциональном
программировании. Вы можете столкнуться с ситуацией, когда у вас есть
общий код инициализации и очистки, который всегда должен работать, но
вы параметризуете различную логику, которая выполняется в коде ини­
циализации и очистки. Пример типового шаблона показан на рис. 7-1. Есть
несколько ситуаций, в которых вы можете использовать execute around, на­
пример:
Файлы
Откройте файл перед использованием, закройте его после использования.
Также вы можете фиксировать исключения, если что-то идет не так. Пара­
метризованный код способен читать из файла или записывать в него.

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

Расширение Twootr

|

193

Соединения базы данных

Откройте соединение с базой данных для инициализации, закройте соеди­
нение по завершении. Часто бывает полезнее объединить соединения с ба­
зой данных в пул, поскольку это позволяет вашей логике также извлекать
соединение из пула.
Код инициализации/подготовки

Код инициализации/подготовки

Задача А
Код очистки/завершения

Код очистки/завершения

Рис. 7-1. Шаблон Execute Around
Поскольку код инициализации и очистки используется во многих местах,
можно попасть в ситуацию, когда он будет дублироваться. В таком случае
при необходимости модификации кода инициализации или очистки вам
придется изменять разные части приложения. Соответственно, возрастет
риск того, что все эти разные фрагменты кода могут стать несовместимыми,
то есть увеличивается вероятность возникновения багов.

Шаблон Execute Around решает эту проблему за счет общего метода, опре­
деляющего и код инициализации, и код очистки. Этот метод принимает
параметр, содержащий исполняемый код (поведение), который отличается
в зависимости от ситуаций использования. Параметр будет использовать
интерфейс, чтобы позволить ему быть реализованным за счет различных
блоков кода, обычно при помощи лямбда-выражений.

В примере 7-18 показан пример применения метода extract. Он исполь­
зуется в Twootr для выполнения выражений SQL базы данных. Здесь со­
здается подготовленный объект для заданного выражения SQL, а затем
запускается extractor. Метод extractor — это обратный вызов, который
извлекает результат, то есть считывает данные из базы данных при помощи
PreparedStatement.

Пример 7-18. Применение шаблона Execute Around в методе extract
R extract(final String sql, final Extractor extractor) (
try (var stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS))
(
stmt.clearParameters();
return extractor.run(stmt);

194

|

Глава 7

} catch (SQLException e) {
throw new IllegalStateException (e);

}

I

Потоки
Наиболее важные особенности функционального программирования в Java
сфокусированы на Collections API и потоках (Streams). Потоки позволяют
нам писать код для работы с коллекциями с более высоким уровнем аб­
стракции, чем позволяют циклы. Интерфейс Stream содержит набор функ­
ций, которые мы рассмотрим в этой главе. Каждая из них относится к об­
щей операции, которую мы выполняли бы в Collection.

шар О
Если у вас есть функция, которая преобразует значение одного типа в дру­
гой, тар () позволяет вам применить функцию к потоку значений, генерируя
новый поток обработанных значений.
Возможно, вы в течение многих лет вполне успешно делали операции конвер­
тирования при помощи циклов. В DatabaseTwootRepos i tor у мы сделали кортеж,
который используется в строке запроса и содержит все значения id различ­
ных пользователей, на которых подписан наш пользователь. Каждое значе­
ние id представляет собой фрагмент String, а весь кортеж заключен в скоб­
ки. Например, если у наших подписчиков идентификаторы richardwarburto
и raoulOK, то мы получим String-кортеж " (richardwarburto, raoulOK)". Для
того чтобы сгенерировать такой кортеж, нужно использовать шаблон «мапинга» (mapping — сопоставление), преобразовывая каждый id в id и добав­
ляя его в List. Метод String. j oin () можно использовать для объединения их
через запятые. В примере 7-19 показан код, написанный в таком стиле.

Пример 7-19. Построение кортежа пользователей при помощи цикла
private String usersTupleLoop(final Set following) {
List quotedlds = new ArrayListo();
for (String id : following) {

quotedlds.add("'" + id + ””’);

)
return '(’ + String.join(",", quotedlds) + ')';

I

Расширение Twootr |

195

map () — одна из самых широко используемых операций в Stream. В приме­
ре 7-20 показан тот же код для кортежа пользователей, но уже с использова­
нием функции тар ().

Здесь также используются возможности joining(), что позволяет нам
объединять элементы из потока в строку.
Пример 7-20. Построение кортежа пользователей при помощи тар
private String usersTuple(final Set following) {
return following

.stream()
.map(id ->
+ id +
.collect(Collectors.joining(",", "(", ")"));
}

Лямбда-выражение передается в map (), принимая String в качестве аргумен­
та, и возвращает String. Не обязательно, чтобы аргумент и возвращаемое
значение были одного типа, однако, переданное лямбда-выражение должно
быть экземпляром Function. Это исходный функциональный интерфейс
с одним аргументом.

forEachQ
Операция f orEach () удобна, когда вы хотите применить эффект для каждо­
го значения в потоке. Например, вам нужно распечатывать имя пользова­
теля или сохранять каждую транзакцию в потоке в базу данных, forEach ()
принимает один аргумент — обратный вызов Consumer, который вызывает
каждый элемент потока в качестве аргумента.

filter ()
Если вы циклически работаете над некоторыми данными и каждый элемент
проверяете при помощи оператора if, вам стоит задуматься над использо­
ванием метода Stream, filter ().
Например, InMemoryTwootRepository нужно запрашивать различные объек­
ты Twoot, чтобы найти объекты, соответствующие TwootQuery. Конкретно,
если позиция больше, чем позиция последнего онлайн пользователя и если
пользователь подписан. Образец, написанный в цикловом стиле, представ­
лен в примере 7-21.

196

|

Глава 7

Пример 7-21. Обработка твутов в цикле при помощи оператора if
public void queryLoop(final TwootQuery twootQuery, final Consumer callback) {
if (!twootQuery.hasUsers ()) {
return;

I

var lastSeenPos]tion = twootQuery.getLastSeenPosition();
var inUsers = twootQuery.getlnUsers ();
for (Twoot twoot : twoots) (
if (inUsers.contains(twoot.getSenderldO) &&

twoot.isAfter(lastSeenPosition)) {
callback.accept(twoot);

)

)

Это называется шаблон «Фильтр». Основная идея фильтра — удерживать
одни элементы потока, а другие пропускать. В примере 7-22 показано, как
можно реализовать этот же код в функциональном стиле.
Пример 7-22. Функциональный стиль
(^Override
public void query(final TwootQuery twootQuery, final Consumer callback) {
if (’twootQuery.hasUsers()) {
return;

)

var lastSeenPosition = twootQuery.getLastSeenPosition ();
var inUsers = twootQuery.getlnUsers();

twoots
.stream()
.filter(twoot -> inUsers.contains(twoot.getSenderld()))
.filter(twoot -> twoot.isAfter(lastSeenPosition))
.forEach(callback);
)

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

Расширение Twootr |

197

Эта функция делает ту же работу, что и функция if ранее. Здесь функция
возвращает true, если строка начинается с цифры. Если вы работаете с уна­
следованным кодом, наличие оператора if внутри цикла for однозначно го­
ворит о том, что вам нужен фильтр. Так как эта функция делает то же, что
и оператор if, она должна вернуть или true, или false для данного значения.
Поток после фильтра содержит значения из входного потока, который дает
результат true.

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

Пример 7-23. Шаблон reduce
Object accumulator = lnitialValue;
for (Object element : collection) {
accumulator = combine(accumulator, element);
}

Объект accumulator «проталкивается» через тело цикла и выходит с итого­
вым значением, которое мы хотели вычислить, accumulator инициализиру­
ется с начальным значением initValue, а затем объединяется с каждым эле­
ментом списка за счет вызова операции combine.
Обратите внимание на элементы, которые отличаются в зависимости от
реализации шаблона. Это initialvalue и функция объединения. В исходном
примере в качестве первого элемента списка мы использовали initialvalue,
но так не должно быть. Чтобы найти самое маленькое значение в списке,
функция combine вернет кратчайший путь выхода из текущего элемента
и accumulator. А теперь предлагаем посмотреть, как этот общий шаблон
можно реализовать при помощи непосредственно операции в Streams API.

Давайте продемонстрируем операцию reduce путем добавления возможно­
сти комбинирования нескольких твутов в один большой твут. У этой опера­
ции будет список объектов Twoot, отправитель твута и его id, передаваемые

198

|

Глава 7

в виде аргументов. Операции нужно объединить различные по содержанию
значения и вернуть наибольшую позицию объединенных твутов. Весь код
представлен в примере 7-24.
Начнем с нового объекта Twoot, который создаем при помощи id, senderld,
с пустым содержимым и с наименьшей возможной позицией — INITIAL_
POSITION. Затем reduce складывает вместе каждый элемент с accumulator, до­
бавляя по одному элементу на каждом шаге. Когда мы достигаем последнего
элемента в потоке, в accumulator находится сумма всех элементов.
Лямбда-выражение, известное как уменыпитель (reducer), осуществляет
объединение и принимает два аргумента, асе — это accumulator, в котором
хранятся предыдущие твуты, которые были объединены. Он также переда­
ется в текущий Twoot в потоке. Уменыпитель в нашем примере создает но­
вый Twoot с максимальной из двух позиций, конкатенацией их содержимого,
и конкретными id и senderld.

Пример 7-24. Реализация суммы с использованием reduce
private final BinaryOperator maxPosition = maxBy (compannglnt (Positio

n::getValue));
Twoot combineTwootsBy(final List twoots, final String senderld, final
String newld) {
return twoots
.stream()
.reduce(
new Twoot(newld, senderld,
INITIAL_POSITION),
(acc, twoot) -> new Twoot (
newld,
senderld,
twoot.getContent() + acc.getContent(),
maxPosition.apply(acc.getPosition(), twoot.getPosition ())));
)

Естественно, сами по себе эти операции не так интересны. Они становятся
по-настоящему мощным инструментом, когда мы объединяем их в «трубу».
В примере 7-25 показан фрагмент кода из Twootr.onSendTwoot(), представ­
ляющий отправку твутов подписчикам пользователя. Первым делом мы вы­
зываем метод followers (), который возвращает Stream. Затем приме­
няем операцию filter, чтобы найти пользователей, которые авторизованы

Расширение Twootr

|

199

на данный момент и которым мы хотим отправить твут. После этого ис­
пользуем операцию forEach, чтобы получить желаемый эффект: отправить
твут пользователям и записать результат.
Пример 7-25. Использование Stream в методе onSendTwoot
user.followers()
.filter(User::isLoggedOn)
.forEach(follower ->
follower.receiveTwoot(twoot);
userRepository.update(follower) ;

Optional
Optional — это тип данных из основной библиотеки Java, представленный
в Java 8 и разработанный в качестве альтернативы null. К старому значению
null есть много претензий. Даже человек, который изобрел эту концепцию,
Тони Хоар, описывал ее как «свою ошибку на миллиард долларов»1. Быть
одним из самых влиятельных людей в сфере компьютерных технологий,
значит, иметь возможность совершить ошибку на миллиард долларов, даже
не видя самого миллиарда.

null часто используется для представления отсутствия значения. И имен­
но в такой ситуации Optional заменяет null. Проблема использования null
в данном случае заключается в ужасной ошибке NullPointerException. Если вы
ссылаетесь на переменную, которая имеет значение null, то ваш код аварийно
завершает работу. У Optional двойная цель. Во-первых, побуждает програм­
миста делать соответствующие проверки на отсутствующее значение, чтобы
избежать багов. Во-вторых, документирует значения, которые ожидаются
с отсутствующим значением в API класса. Это помогает найти скрытые баги.

Давайте взглянем на API для Optional, чтобы понять, как этим пользоваться.
Если вы хотите создать экземпляр Optional из значения, есть рабочий ме­
тод под названием of (). Теперь Optional будет выступать в роли контейнера
для этого значения, которое можно получить при помощи get, что показано
в примере 7-26.
1 Посмотреть видео выступления Тони Хоара «Null References: The Billion Dollar Mistake»
можно на https://oreil.ly/OaXWj. — Прим. авт.

200

|

Глава 7

Пример 7-26. Создание Optional из значения
Optional а = Optional.of("а");
assertEquals("a", a.getO);

Поскольку Optional может также представлять и отсутствующее значение,
есть рабочий метод под названием empty (), и вы можете конвертировать
значение в Optional при помощи метода ofNullable (). Оба этих метода пока­
заны в примере 7-27, также как и применение метода isPresent (), который
показывает, содержится ли значение в Optional.
Пример 7-27. Создание пустого объекта Optional и проверка на наличие
значения
Optional emptyOptional = Optional.empty();
Optional alsoEmpty = Optional.ofNullable(null);

assertFalse(emptyOptional .isPresent ());
// a is defined above
assertTrue (a.isPresent ());

Один из подходов к использованию Optional предусматривает защиту лю­
бого вызова get () за счет проверки isPresent (). Это необходимо, поскольку
вызов get () может сгенерировать исключение NoSuchElementException. Как
ни странно, такой подход — не лучший пример использования Optional.
Если вы пользуетесь именно им, то можно сказать, что все, чем вы занимае­
тесь — это просто воспроизводите другие шаблоны для использования null
(где вы в целях безопасности можете проверить, что значение не null).
Более изящным подходом является вызов метода orElse (), который выдает
альтернативное значение, если контейнер Optional пуст. Если создание аль­
тернативного значения требует много ресурсов, можно использовать метод
orElseGet (). Это позволяет вам передавать функцию Supplier, которая вы­
зывается только тогда, когда Optional действительно пуст. Оба эти метода
демонстрируются в примере 7-28.

Пример 7-28. Использование orElse () и orElseGet ()
assertEquals("b”, emptyOptional.orElse("b"));
assertEquals("c", emptyOptional.orElseGet(() -> "c"));

Расширение Twootr

|

201

В Optional также есть набор методов, которые могут использоваться как
Stream API. Например, filter (), map (), if Present (). Применение этих методов
в Optional API представляется похожим на Stream API, однако в данном случае
ваш поток может содержать только 1 и 0 элементов. Поэтому Optional. filter ()
оставит элемент в Optional, если он соответствует критериям, и вернет пустой
Optional, если он до этого был пуст или если предикат не соответствует дей­
ствительности. Таким же образом тар () преобразует внутреннее значение
Optional, но если он пуст, то функция не применяется вообще. Поэтому при­
менение этих функций безопаснее, чем null. Они взаимодействуют с Optional
только если внутри него что-то есть, if Present это аналог forEach — он приме­
няет обратный вызов Consumer, если присутствует значение, и никак иначе.
Вы можете увидеть метод Twootr.onLogon() в примере 7-29. Это пример
того, как мы можем объединять различные операции для выполнения бо­
лее сложной операции. Мы начинаем с поиска пользователя по идентифи­
катору путем вызова метода UserRepository.get (), который возвращает
Optional. Затем проверяем пользовательский пароль на соответствие при
помощи filter. Мы используем ifPresent, чтобы уведомить пользователя
о пропущенных твутах. Наконец, мы преобразовываем объект User в новый
SenderEndPoint, который возвращается из метода.
Пример 7-29. Использование Optional в методе onLogon
var authenticatedUser = userRepository
.get(userid)
. filter(userOfSameld ->
(
var hashedPassword = KeyGenerator.hash(password, userOfSameld.getSalt());
return Arrays.equals(hashedPassword, userOfSameld.getPasswordO );
});
authenticatedUser.ifPresent(user ->
(
user.onLogon(receiverEndPoint) ;
twootRepository.query (
new TwootQueryO
.inUsers(user.getFollowing ())
.lastSeenPosition(user.getLastSeenPosition()),
user::receiveTwoot);
userRepository.update(user);
));
return authenticatedUser.map(user -> new SenderEndPoint(user, this));

202

|

Глава 7

В этом разделе мы увидели только верхушку функционального програм­
мирования. Если вам интересно более глубокое изучение функциональ­
ного программирования, мы рекомендуем прочитать книги «Современный
язык Java»1 и «Лямбда-выражения в Java 8»2.

Пользовательский интерфейс
На протяжении всей главы мы старались избегать разговоров о пользо­
вательском интерфейсе, так как были сфокусированы на разработке ядра
приложения. Теперь стоит немного углубиться в то, что образцовый проект
представляет как часть своего пользовательского интерфейса, чтобы по­
нять, как собрать вместе моделирование событий. Итак, наш проект — это
одностраничный веб-сайт, в котором применяется JavaScript для реализа­
ции динамической функциональности. Чтобы сохранять все в достаточно
простом виде и не погружаться в несметное число противоборствующих
платформ, мы используем j query, чтобы обновлять сырую HTML-страницу.
Однако сохраняем простое разделение задач в коде.

Когда вы заходите на веб-страницу Twootr, он подключается к хосту при по­
мощи WebSockets. Это один из тех способов связи, которые мы обсуждали
в разделе «От событий к разработке». Весь код для связи находится в пакете
web_adapter chapter_06. КлассWebSocketEndPoint реализует ReceiverEndPoint,
а также вызывает любые нужные методы в SenderEndPoint. Например, ко­
гда получает и анализирует сообщение-запрос на подписку, он вызывает
SenderEndPoint.onFollowO, передавая ему имя пользователя. Возвращаемое
перечисление enum FollowStatus конвертируется в нужный формат для пе­
редачи и отправляется в соединение WebSocket.
Все взаимодействие между пользовательской частью JavaScript и сервером
происходит при помощи стандарта JSON (JavaScript Object Notation — тек­
стовый формат обмена данными)3. JSON был выбран потому, что в пользо­
вательских интерфейсах JavaScript легко десериализовать и сериализовать.

В WebSocketEndPoint нам нужно преобразовывать в и из JSON при помощи
Java. Есть много библиотек для этой цели. Мы выбрали наиболее популярную

1 «Современный язык Java. Лямбда-выражения, потоки и функциональное программирова­
ние», Рауль-Габриэль Урма, Алан Майкрофт, Марио Фуско. — Прим. ред.
2 «Лямбда-выражения в Java 8. Функциональное программирование — в массы», Ричард Уорбертон. — Прим. ред.
3 Подробнее на https://www.json.org. — Прим. авт.

Расширение Twootr

|

203

библиотеку Jackson, которая хорошо поддерживается. JSON часто использу­
ется в приложениях, в которых применяется подход запрос/ответ вместо со­
бытийно-ориентированного подхода. В нашем случае мы вручную извлекаем
поля из объекта JSON, чтобы не усложнять конструкцию. Однако возможно
использовать более высокоуровневый JSON API вроде Binding API.

Инверсия зависимости и внедрение зависимости
В этой главе мы много говорили о шаблонах развязывания. Наше приложение
использует шаблон портов и адаптеров и шаблон репозитория, чтобы развязать
бизнес-логику и детали реализации. Есть большой унифицирующий принцип,
о котором мы думаем, когда сталкиваемся с этими шаблонами — Принцип ин­
версии зависимости (DIP — Dependency Inversion Principle), последний из пяти
шаблонов SOLID, рассматриваемых в этой книге. Как и все остальные, он был
введен Робертом Мартином. Данный принцип утверждает, что:



Модули высокого уровня не должны зависеть от модулей низкого уров­
ня. Оба уровня должны зависеть от абстракций.



Абстракции не должны зависеть от деталей. Детали должны зависеть от
абстракций.

Принцип называется принципом инверсии потому, что традиционно
в структурном программировании встречаются случаи, когда модули высо­
кого уровня производят модули низкого уровня. Часто это является побоч­
ным эффектом нисходящей технологии разработки, о которой мы говорили
ранее. Большую проблему вы разбиваете на подпроблемы, создаете модули
для их решения, и тогда главная проблема (модуль высокого уровня) зави­
сит от подпроблем (модулей низкого уровня).
При разработке Twootr нам удалось избежать таких проблем за счет аб­
стракций. У нас есть высокоуровневый входной класс Twootr, и он не за­
висит от модулей низкого уровня вроде DataUserRepository. Он зависит
от абстракции — интерфейса UserRepository. Такую же инверсию мы
производим в порте пользовательского интерфейса. Twootr не зависит от
WebSocketEndPoint, он зависит от ReceiverEndPoint. Программа сводится
к интерфейсу, а не к реализации.
Связанное понятие — концепция внедрения зависимости (DI — Dependency
Injection). Чтобы понять, что такое концепция DI и зачем она нужна, да­
вайте мысленно проведем эксперимент. Итак, было принято решение, что

204

|

Глава 7

главный класс Twootr должен зависеть от UserRepository и TwootRepository,
чтобы хранить объекты User и Twoot. Внутри Twootr мы определили поля
для хранения экземпляров этих объектов, как показано в примере 7-30. Во­
прос в том, как нам создать эти экземпляры?
Пример 7-30. Зависимости в классе Twootr
public class Twootr

(
private final TwootRepository twootRepository;
private final UserRepository UserRepository;

Первый способ — вызывать конструкторы с помощью ключевого слова new,
как показано в примере 7-31. Здесь мы жестко запрограммировали исполь­
зование репозиториев. Большая часть кода в классе все еще нацелена на
интерфейс, поэтому нам нужно немного изменить реализацию, но это не
совсем честно. Мы должны всегда использовать репозитории без данных,
что означает, что наши тесты класса Twootr зависят от базы данных и, соот­
ветственно, работают медленнее.

Кроме того, если мы будем поставлять различные версии Twootr различным
клиентам, к примеру, «стационарную» версию Twootr, использующую SQL,
для компаний-клиентов и облачную версию, использующую NoSQL, то нам
придется отделять сборки для двух различных версий кода. Недостаточно
просто определить интерфейсы и разделить реализации: нам также нужен
способ связи с правильной реализацией, который не будет разрушать нашу
абстракцию и нарушать подход развязывания.
Пример 7-31. Жестко запрограммированные экземпляры полей
public Twootr()
this.UserRepository = new DatabaseUserRepository();
this.twootRepository = new DatabaseTwootRepository();

)

// How to start Twootr
Twootr twootr = new Twootr();

Наиболее распространенным шаблоном для осуществления различных за­
висимостей является шаблон Abstract Factory (Абстрактная фабрика). Он
Расширение Twootr

|

205

показан в примере 7-32. Здесь у нас есть рабочий («фабричный») метод, ко­
торый мы можем использовать для создания экземпляра нашего интерфей­
са при помощи метода getlnstance!). Когда мы хотим настроить нужную
реализацию, мы можем вызывать setinstance(). Так, например, мы можем
использовать setinstance () в тестах, чтобы создать реализацию в памяти,
в локальной установке для использования базы данных SQL или в облачной
среде для использования базы данных NoSQL. Мы отвязали реализацию от
интерфейса и можем вызывать этот код откуда угодно.
Пример 7-32. Создание экземпляров при помощи рабочих методов
public Twootr()

(
this.userRepository = UserRepository.getlnstance();
this.twootRepository = TwootRepository.getlnstance();

}

// How to start Twootr
UserRepository.setinstance(new DatabaseUserRepository());
TwootRepository.setinstance(new DatabaseTwootRepository());
Twootr twootr = new Twootr();

Как ни странно, у фабричного подхода есть свои недостатки. Для начала
мы создали «большой комок» общего изменяемого состояния. В любой си­
туации, когда нам нужно запустить одну JVM с различными экземплярами
Twootr с разными зависимостями, это будет невозможно. Кроме того, мы
связали жизненные циклы. Возможно, когда-то нам захочется создать но­
вый экземпляр TwootRepository при запуске Twootr или использовать уже
существующий. Фабричный подход не позволит нам сделать это напрямую.
Помимо этого, было бы гораздо сложнее иметь фабричный метод для каж­
дой зависимости, которую мы хотим реализовать в приложении.

Настал момент для появления внедрения зависимости. Можно представить,
что DI это «агент-посредник» — не звоните нам, мы сами вам позвоним.
С помощью DI вместо явного создания зависимостей или использования
фабрик для их создания мы просто берем параметр, и любой экземпляр на­
шего объекта несет ответственность за передачу требуемых зависимостей.
Им может быть метод настройки тестового класса, который передается
в мок-объект. Или метод main () нашего приложения в реализации базы дан­
ных SQL (пример 7-33). Инверсия зависимости — это стратегия. Внедрение
зависимости и шаблон репозитория — это тактики.
206

|

Глава 7

Пример 7-33. Создание экземпляров с использованием внедрения
зависимости
public Twootr(final [JserRepository userRepository, final TwootRepository twootReposi

tory)

I
this.userRepository = userRepository;

this.twootRepository =twootRepository;

}
// How to start Twootr
Twootr twootr = new Twootr(new DatabaseUserRepository(), new DatabaseTwootReposi
toryO);

Получение объектов таким способом не только упрощает создание тестов
для них, но и имеет преимущество в экстернализации (вынесении) созда­
ния самих объектов. Это позволяет коду нашего приложения контролиро­
вать, в какой момент создается UserRepository и какие зависимости в него
заложены. Многие разработчики считают удобным применение DI-фреймворков, таких как Spring и Guice, предлагающих множество возможностей
по сравнению с базовым DI. Например, они задают жизненные циклы для
bean-объектов, которые стандартизируют хуки, вызываемые после создания
экземпляра объектов или до их уничтожения, если это необходимо. Они
также могут предлагать области для объектов (вроде объектов Singleton),
которые создаются только один раз в течение жизненного цикла процесса
или объектов по запросу. Забегая вперед, эти DI-платформы часто хорошо
интегрируются с платформами веб-разработки, такими как Dropwizard или
Spring Boot, обеспечивая продуктивную работу.

Пакеты и сборочные системы
Java позволяет разделить код на различные пакеты. В этой книге код каждой
главы мы помещали в отдельном пакете. Twootr — первый проект, при рабо­
те над которым мы разделили проект на несколько подпакетов.
Вот пакеты, в которых размещены различные компоненты этого проекта:



com.iteratrlearning.shu_book.chapter_0 6 — пакет верхнего уровня
в проекте.



com.iteratrlearning.shu_book.chapter_06.database — содержит адаптер
для базы данных SQL.

Расширение Twootr

|

207



com.iteratrlearning.shu_book.chapter_06.in_memory — содержит адап­
тер для хранения в памяти.



com. iteratrlearning. shu_book.chapter_06 .web_adapter — содержит адап­
тер для пользовательского интерфейса, основанного на WebSockets.

Разделение больших проектов на отдельные пакеты помогает структуриро­
вать код и упрощает разработчикам поиск. Так же, как классы группируют
методы и состояния, пакеты группируют связанные классы. Пакеты дол­
жны подчиняться тем же правилам связности и связанности, что и классы.
Помещайте классы в один пакет, если они должны изменяться одновремен­
но и если они относятся к одной и той же структуре. Например, в проекте
Twootr, если мы хотим модернизировать код хранилища SQL, то знаем, что
должны обратиться к подпакету database.
Пакеты, кроме всего прочего, позволяют скрывать информацию. При рас­
смотрении примера 4-3 мы обсуждали идею пакетного конструктора, чтобы
исключить возможность создания экземпляров объектов вне пакета.
Мы также можем ввести «пакетность» для классов и методов. Это исключит
возможность доступа внешних объектов к деталям класса и поможет до­
стичь снижения связанности. Например, WebSocketEndPoint — это пакетная
реализация интерфейса ReceiverEndPoint, которая находится в пакете web_
adapter. Никакой другой код в проекте не может обращаться к этому классу
напрямую. Только через интерфейс ReceiverEndPoint, выполняющий роль
порта.

Наша идея о своем пакете для каждого адаптера хорошо перекликается
с шаблоном гексагональной архитектуры, который мы задействовали в про­
екте. Стоит отметить, что не каждое приложение является гексагональным.
Поэтому есть две распространенные структуры пакетов, с которыми вы мо­
жете столкнуться в других проектах.
Один из распространенных способов структурирования пакетов — струк­
турирование по слоям. Например, сгруппировать весь код, ответственный
за HTML-страницу, в пакет views, а весь код, который обрабатывает веб­
запросы, — в пакет controller. Несмотря на свою популярность, такой под­
ход к структурированию не всегда является удачным, потому что приводит
к плохой связности и связанности. Если вы захотите поменять свою веб­
страницу, добавив туда какой-то параметр, и выводить значение, основан­
ное на этом параметре, то вам придется иметь дело и с пакетом controller,
и с пакетом views, а возможно, и с какими-то еще.

208

|

Глава 7

Альтернативный способ группировки пакетов — группировка по свойствам.
Например, если вы создаете сайт для электронной коммерции, у вас будет па­
кет cart для покупательской корзины, пакет product для кода, связанного с пе­
речнем товаров, пакет payment для кода, связанного с платежными картами,
и т. д. Такой вариант обеспечит лучшую связность. Если вы, уже осуществляя
поддержку карт Visa, вдруг захотите реализовать поддержку еще и системы
Mastercard, то вам нужно будет модернизировать только пакет payment.
В разделе «Работа с Maven» мы говорили о том, как настроить базовую струк­
туру сборки при помощи сборщика Maven. Если рассматривать как струк­
туру проекта структуру данной книги, то мы имеем один проект Maven,
а различные главы книги — это различные Java-пакеты одного проекта. Это
хорошая и простая структура, которая подойдет к большинству проектов.
Однако она не единственная. И Maven, и Gradle предлагают структуры про­
ектов, которые создают и выводят множество артефактов (элементов) сбор­
ки из одного проекта верхнего уровня.
Это может быть полезно, если вы хотите развернуть различные сборочные
артефакты. Предположим, у вас есть клиент-серверный проект и вам нужно
получить одну сборку на выходе, в которой будут и клиентская, и серверная
части, в то время, как клиент и сервер — это разные бинарные элементы,
работающие на разных машинах. Однако не стоит слишком усложнять сбо­
рочные скрипты.

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

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

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

Расширение Twootr

|

209

многопоточность для взаимодействия с событиями в Twootr. Таким обра­
зом мы смогли бы использовать возможности современных процессоров
и обслуживать большее количество пользователей одной машиной.
Кроме того, мы полностью проигнорировали вопрос отказоустойчиво­
сти, касающийся работы сервиса в случае отказа сервера. Мы также не
уделили внимания масштабируемости. Например, запрос всех твутов
происходит определенным порядком, который легко реализуется на од­
ном сервере, но может стать весьма узким местом в системе. Точно так
же может привести к «затору» и просмотр всех твутов сразу. Представь­
те, если вы уедете в отпуск на неделю и по возвращении вас будет ждать
20 000 твутов!
Рассмотрение данных проблем в деталях не вписывается в рамки нашей гла­
вы. Однако если вы планируете продолжать изучать Java, эти важные вопро­
сы стоит рассмотреть. И мы планируем заняться ими в наших следующих
книгах.

Выводы


Теперь вы можете отвязать хранилище данных от бизнес-логики прило­
жения при помощи шаблона репозитория.



Вы познакомились с реализациями двух типов репозиториев с таким
подходом.



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



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

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

Предположим, что для Twootr мы выбрали технологию pull. Вместо по­
стоянного «проталкивания» сообщений к браузерному клиенту через
WebSockets мы используем HTTP для запроса последних не просмотренных
сообщений.

210

|

Глава 7



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



При помощи TDD реализуйте альтернативную модель Twootr. Вам не
нужно реализовывать HTTP часть. Только основные классы для этой
модели.

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

ГЛАВА 8

Заключение

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

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

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

212

|

Глава 8

следующий набор элементов. Мы обнаружили, что имеет смысл оценивать
прогресс в навыках таким же образом. Если периодически «оглядываться
назад», это поможет вам сфокусироваться и определить направление для
работы. Интенсивная разработка часто подразумевает еженедельную ретро­
спективу, но лично вам не нужно делать это так часто. Ретроспектива раз
в квартал или раз в два года была бы весьма кстати. Еще один полезный мо­
мент — оценивать, какие навыки пригодятся вам в текущей или будущей ра­
боте. Чтобы убедиться, что данные навыки развиваются, стоит ставить цели
на квартал. Они должны учитывать все, что вы хотите изучить или улучшить.
Не обязательно ставить какую-то большую цель вроде изучения нового языка
программирования. Лучше сосредоточиться на чем-то несложном. Например,
освоить новую тестовую платформу или пару шаблонов проектирования.

Что касается новых навыков, мы часто слышим такой вопрос от разработчи­
ков: «Как я могу постоянно учиться новым технологиям, методикам и прин­
ципам?» Это нелегко, ведь каждый занят своими делами. Не стоит пытаться
изучить все. Это верный путь к сумасшествию! Способность определять
нужные навыки, которые будут служить вам в течение долгого времени, —
вот что делает из вас хорошего разработчика. Ключевой момент — всегда
развиваться и работать над собой.

Сознательная практика
Хотя данная книга и покрывает много ключевых вопросов и навыков, необ­
ходимых для хорошего разработчика, очень важно практиковать их. Чтения
самого по себе недостаточно. Только практика поможет вам усвоить новые
навыки. Стремитесь в своей повседневной работе всегда искать оптималь­
ные решения. Каждый шаблон, описанный в этой книге, где-то можно при­
менять, а где-то не стоит. Мы постарались показать вам, как важно заранее
оценивать, в каких ситуациях рассматриваемая вами методика выигрывает,
а в каких — нет.

Часто мы думаем, что природный талант и интеллект — наиболее опреде­
ляющие факторы успеха, однако большинство исследований показывает,
что практика и работа — истинные ключи к успеху. Такие книги, как «Вы­
дающиеся результаты. Талант ни при чем!» Джеффа Колвина и «Гении
и аутсайдеры. Почему одним все, а другим ничего?» Малкольма Гладуэлла,
оценивают ряд ключевых факторов успеха в жизни, и наиболее эффектив­
ным из них является сознательная практика.

Заключение

|

213

Сознательная практика — это разновидность практики, являющейся систе­
матической и имеющей цель. Сознательная практика помогает улучшить
производительность, что требует концентрации внимания. Часто, когда
люди практикуют свои навыки, чтобы усовершенствовать их, они просто
занимаются повторением. Делать одно и то же снова и снова, ожидая улуч­
шений — это не самый эффективный способ.
Приведем пример. Когда мы изучали библиотеку Eclipse Collections1, мы,
стремясь освоить и понять ее системным путем, прошли через замечатель­
ный набор упражнений, поставляющихся с данной библиотекой. Чтобы
убедиться, что мы действительно хорошо усвоили материал, мы выполни­
ли все упражнения три раза. Каждый раз мы сравнивали свои результаты
с предыдущими, находя более четкие и быстрые способы решения.

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

Следующие шаги и дополнительные ресурсы
Что ж, надеемся, вы понимаете, что эта книга — не конец пути. Но чего ис­
кать дальше?
Отличный способ получить новые знания и расширить горизонты — по­
грузиться в ПО с открытым исходным кодом. Большинство таких Javaпроектов, например, JUnit и Spring, находятся на GitHub2. Какие-то из этих
проектов могут быть более «дружелюбными», чем остальные. Часто разра­
ботчики открытых проектов очень заняты и им нужна помощь. Советуем
ознакомиться со списком ошибок и посмотреть, можете ли вы им чем-то
помочь.

1 https://www.eclipse.org/collectionswww.edipse.org/collections/. — Прим. авт.
2 https://github.com. — Прим. авт.

214

|

Глава 8

Не стоит забывать и про классические курсы и онлайн-обучение — еще один
распространенный способ развития навыков. Популярность онлайн-курсов
постоянно растет. Можем сказать, что на Pluralsight1 и на O’Reilly Learning
Platform2 есть отличные варианты курсов Java.
Другой крутой источник информации для разработчиков — это блоги
и Twitter. И Ричард3, и Рауль4 есть в Twitter и часто публикуют интересные
ссылки. Programming Reddit5 часто выступает в качестве агрегатора ссылок,
как и Hacker News6. Наконец, учебная компания, которой управляют авторы
книги, тоже предлагает серию бесплатных статей7.

Спасибо, что прочитали нашу книгу. Мы ценим ваши мысли и отзывы и же­
лаем вам успехов в Java-разработке.

1
2
3
4
5
6
7

https://www.pluralsight.com . — Прим. авт.
https://www.oreilly.com. — Прим. авт.
https://twitter.com/richardwarburto. — Прим. авт.
https://twitter.com/raouluk. — Прим. авт.
https://www.reddit.eom/r/programming. — Прим. авт.
https://news.ycombinator.com. — Прим. авт.
http://iteratrlearning.com/articles. — Прим. авт.

Об авторах

Доктор Рауль-Габриэль Урма — исполнительный директор и основатель
Cambridge Spark, ведущей обучающей организации в сфере ИТ, искуственного интеллекта, карьерного роста и продвижения. Является автором
нескольких книг по программированию, включая такой бестселлер, как
«Современный язык Java» (Modern Java in Action). Рауль-Габриэль имеет
степень доктора философии по информатике Кембриджского университе­
та, а также степень магистра Лондонского Имперского колледжа. Он окон­
чил учебу с отличием первой степени, получил несколько наград в области
технических инноваций. Его научные интересы связаны с языками про­
граммирования, компиляторами, анализом исходных кодов, машинным
обучением и образованием. Был номинирован в качестве Java-чемпиона
Oracle в 2017 году. Также является опытным международным оратором,
проводит лекции по Java, Python, искусственному интеллекту и бизнесу.
Рауль консультировал и работал с такими компаниями, как Google, Oracle,
eBay и Goldman Sachs.

Доктор Ричард Уорбэртон — основатель Opsian.com, мэнтейнер Artio FIX
Engine. Работал в качестве разработчика в различных сферах, в том числе
занимался разработкой инструментов, HFT (высокочастотный трейдинг)
и сетевых протоколов. Автор успешной книги «Лямбда-выражения в Java 8»
(Java 8 Lambdas). Занимается обучением разработчиков на ресурсах http://
iteratrlearning.com и https://www.pluralsight.com/authors/richard-warburton.
Ричард — опытный оратор, он провел десятки встреч, регулярно выступа­
ет в качестве организатора на крупнейших конференциях Европы и США.
Имеет степень доктора философии в сфере информатики Уорикского уни­
верситета.

216

|

Об авторах

В завершение
Животное на обложке книги — это красноголовый мангабей (Cercocebus
torquatus), обезьяна Старого Света, найденная в горном массиве вдоль за­
падного побережья Африки. Мангабей обитают в лесной среде: как в бо­
лотах, так и на равнинах. Большую часть времени проводят на деревьях,
забираясь на высоту до 100 футов (около 30 метров), спускаясь на землю
для поиска пищи (особенно в сухой период). Питаются фруктами, семена­
ми, орехами, растениями, грибами, насекомыми и птичьими яйцами.

Красноголовый мангабей получил свое название из-за белого воротника,
выделяющегося на фоне более темного тела, а также из-за каштаново-крас­
ной головы. Белые веки подчеркивают и без того выразительную мордоч­
ку. Представители этого вида весят в среднем 20-22 фунта (около 9-10 кг)
и имеют рост 18-24 дюйма (45-60 см). Как и многие древесные приматы,
мангабей обладает длинным гибким хвостом, длиннее своего тела. Латин­
ское название Cercocebus фактически означает «хвост обезьяны».
Мангабей живут большими группами от 10 до 35 особей, состоящими из
альфа-самца и самок с детенышами. Взрослые самцы живут в одиночестве
до тех пор, пока не смогут сформировать или найти отряд (название для
группы мангабеев), чтобы возглавить его. Оснащенные большими усили­
тельными горловыми мешками, эти животные очень «вокальны», распола­
гают большим репертуаром криков, хрюканья, кудахтанья и других звуков,
которые служат для оповещения стада о хищниках или для предупрежде­
ния о вторжении злоумышленника. К сожалению, количество шума, про­
изводимого мангабеями, также делает их легкой мишенью для охотников,
добывающих мясо диких животных. Мангабей занесены в Красную книгу.

Предметный указатель
А
ACID, совместимость, 188
Aeron, сервис, 151
Agile, методика, 155
Amazon Simple Queue Service, сервис,
151
AMPQ, сервис, 151
Ant, сборщик, 78
API-интерфейс, 57
явный против неявного, 59

В
BDUF, подход, 155
Bouncy Castle, библиотека, 161
break, оператор, 134

С
Cobertura, инструмент, 48
CRUD, операции, 182
CSV, формат, 22, 26, 37
С#, язык программирования, 14, 17
C++, язык программирования, 14,17

F
final, ключевое слово, 25,96

G
Gradle, сборщик, 44, 77, 209
использование, 82
команды, 84
сборочный файл, 83
Groovy, язык программирования, 83

Н
Hacker News, ресурс, 215
HashMap, расширение, 97
Hibernate, система, 188
HTML, формат, 50, 62
HTTP-запрос, 149

I
if, оператор, 13
IntelliJ IDE, среда, 46

J
D
DMN, стандарт, 121
double, тип, 24, 62
Drools, движок бизнес-правил, 121
DRY, принцип, 27

Е
EasyMock, библиотека, 170
Eclipse Collections, библиотека, 214
Emma, инструмент, 48
enum, тип, 164

218

|

Предметный указатель

JaCoCo, инструмент, 48
JAR, формат, 78
Javadoc, синтаксис, 74
Java Streams API, интерфейс, 139
Java, язык программирования, 14
версии 8, 56, 77
исключения, 66
компилятор, 77
особенности, 16
JDK, комплект разработчика, 77
jQQQ, интерфейс, 139

JSON, формат, 26, 37, 62, 203
JUnit, библиотека, 44, 214

К
KISS, принцип, 22, 27
Kotlin, язык программирования, 83

м
Maven, сборщик, 44, 77, 209
команды, 81
работа с, 78
сборочный файл, 80
структура, 79
Mockito, библиотека, 168

N
new, ключевое слово, 205
null, значение, 76

О
OAuth, система, 154
Optional, тип данных, 77, 158, 200
OReilly Learning Platform, платформа,
215

т
Try, тип данных, 77
Twootr, приложение, 146
требования к, 147

V
Valhalla, проект, 178
var, ключевое слово, 131
void, тип, 64

W
WAR, формат, 78
WebSockets, протокол, 151

X
XML, формат, 37, 82

Y
YAGNI, принцип, 183

Z
ZeroMQ, сервис, 151

А
Р
Pluralsight, компания, 215
Powermock, библиотека, 169
Programming Reddit, ресурс, 215
Push, технология, 149
Python, язык программирования, 14, 17

R
Ruby, язык программирования, 17

S
SOLID, принципы, 11, 12, 17, 97
Spring Integration, интерфейс, 139
Spring, проект, 214
Streams API, инструмент, 61
String, тип, 64, 89
switch, оператор, 90, 119, 132

Абстрактный класс, 37
Абстракция, 189
Автоматизированное тестирование, 43
преимущества, 43
Авторизация
неудачная, 158
Адаптер, 153
Антисвязность, 58
Антишаблон, 109
Атрибут, 94
Аутентификации, сервис, 154

Б
База данных, 194
Базовые типы, 14
Безопасность, 160
Бриллиантовый оператор, 131

Предметный указатель

|

219

в
Валидатор, 73
Верификация, 167
при помощи моков, 168
Внедрение зависимости, 204

Г
Гексагональная архитектура, 153
Гигиена тестов, 107
Гладуэлл, Малкольм, 213
Графический интерфейс пользователя,
152
Группировка методов, 35
временная, 39
информационная, 36
логическая, 37
последовательная, 38
служебная, 37
функциональная, 36

д
Дайджест, 160
Движок бизнес-правил, 119
Drools, 121
компоненты, 120
преимущество, 120
требования к, 120
условия, 127
Действие, 122
Диагностика, 113
Доменно-ориентированная разработка,
92
Доменный класс, 29, 61
Доменный объект, 62
более сложный, 63
специализированный, 62
Дублирование кода, 26, 52, 112

3
Зависимость
внедрение, 204
инверсия, 204

220

|

Предметный указатель

и
Импортер, 90
помещение в класс, 100
реализация,95
регистрация, 95
Инвариант, 99
Инверсия зависимости, 206
Инициализация, 39
Инкапсуляция, 100
Интегрированная среда разработки, 12,
113
Интерфейс, 40, 53
Action, 122
API, 57
ConditionalAction, 135
Exporter, 64
бог, 57
в Java, 57
графический, 152
объявление, 64
подводные камни, 56
пользовательский, 203
принцип разделения, 135
разработка текучего, 139
реализация,64
репозитория,188
создание экземпляра, 55
Исключения, 70, 158
CSVSyntaxException, 68
null, значение, 76
альтернатива, 76
выбор, 68
документирование, 74
игнорирование, 74
методика применения, 74
назначение, 66
непроверяемые, 67
обработка, 65
перехват, 74
поток, 67
проверяемые, 67
против управляющего потока, 75
связанные с конкретной реализацией,
75
слишком однообразные, 71

слишком специфические, 69
шаблон, 68, 72

К
Класс, 14
Attributes, 95
Document, 91,94
Error, 67
Facts, 128
Inspector, 136
Java-исключений, 67
Path, 23
Query, 100
Report, 94
RuntimeException, 67
SenderEndPoint, 170
Validator, 69
абстрактный, 37
анонимный, 127,191
бог, 26
встроенный, 178
доменный, 29, 61
концепции моделирования при
помощи, 103
модульного теста, 45
помещение импортера в, 100
с временной связностью, 39
служебный, 37,103
тестовый, 45
узлы, 32
Клиент-серверная модель, 148
Код
автоматическое тестирование, 43
более читаемый, 190
дублирование, 26, 52, 112
инициализации, 194
исполняемый, 191
копирование, 106
лямбда-ориентированный, 189
неиспользуемый, 182
обслуживаемость, 25
обслуживание, 107
открытость, 93
параметризованный, 193
повторное использование, 101

покрытие, 47
потокобезопасный, 189
расширение, 101
усовершенствование, 123
хэш-, 177
шаблонный, 191
Кокбурн, Алистер, 153
Колвин, Джефф, 213
Коллекция, 62, 189, 198
Константа, 95,117
Копирование кода, 106

Л
Локальная переменная, 131
Лямбда-выражение, 55, 127, 189, 190
уменьшитель, 199

м
Малкольма Гладуэлла, 213
Мартин, Роберт, 204
Матчер, 115
Множественный экспорт, 62
Моделирование
домена, 139
ошибок, 163
правила, 140
состояния, 127
Модульное приложение, 28
Модульный тест, 126
Мокинг, 119,125,167
библиотеки для, 169
Мок-объект, 167

н
Наследование, 104
Наследственные связи, 87
Неизменяемость, 25
Неиспользуемый код, 182

О
Область действия пакета, 100
Обработка исключений, 65
Обратный вызов, 190,196
Обслуживаемость кода, 25

Предметный указатель

|

221

Обслуживание кода, 107
Объект
bean, 207
Singleton, 207
запрос, 184
значения, 176
мок-, 206
ненулевой, 178
ссылочный, 176
Объектно-ориентированное
программирование, 13
Объектно-реляционного отображения,
система, 188
Очередь сообщений, 151

Принцип
KISS, 94
YAGNI, 183
единственной ответственности, 18, 21,
27,138
инверсии зависимости, 18, 204
наименьшего удивления, 32
открытости/закрытости, 18, 51, 59, 64
подстановки Дисков, 18, 87,97,104
разделения интерфейса, 18,135
строгой типизации, 92
эквивалентного отношения, 176
Присвоение имен, 140

Р

П
Пакетная видимость, 100
Пакеты, 207
область действия, 100
структурирование, 208
Параметризация поведения, 190
Пароль, 160
хранение, 160
хэшированный, 160
Парсер, 40
Парсинг, 31, 38
алгоритм, 32
Переменная
булева, 164
локальная, 131
Повторное использование кода, 101
Подписчики, 162
Подтип, 97
Позиции, 172
Покрытие кода, 47
инструменты, 48
Пользовательский интерфейс, 203
Пользовательский объект, 128
Порт, 153
Постусловие, 99
Потоки, 189, 195
Правило истории, 99
Предметно-ориентированный язык, 83
Примитив, 61

222

|

Предметный указатель

Разработка программного обеспечения
сверху вниз, 163
снизу вверх, 162
Разработка через тестирование, 11, 89,
121
назначение, 122
философия, 122
цикл, 123
Разрядность, 61
Расширение кода, 101
Репозиторий,182
интерфейс, 188
проектирование, 182
шаблон, 181

С
Сборочный файл, 80, 83
Сборщик, 46
Ant, 78
Gradle, 77
Maven, 77
использование, 77
назначение, 77
преимущества, 78
Сверхтип, 99
Связность, 32
анти-, 58
внутриклассовая, 33, 35
методов, 39

плохая, 33
последовательная, 38
Связывание, 40
Связь, 151
Синтаксический анализ данных, 28
Система по работе с клиентами, 133
Система управления документами, 87
импорт, 90
тестирование, 110
требования к, 88
Служебный класс, 37
Событийно-ориентированный метод,
149
События, 153
Соли, 162
Ссылка на метод, 192
Статический метод, 46

ф
Фоулер, Мартин, 72
Функциональное программирование,
11,12, 13, 189
Функциональный стиль, 197

X
Хоара, Тони, 200
Хранение информации, 181
Хэш-код, 177
Хэш-функция, 160

ц
Цикл, 13

ч

Т
Твутинг, 166
Текст, формат, 62
Текучий интерфейс, 139
Тест
модульный, 44, 126
объявление метода, 44
Тестирование, 12, 18,42
JUnit, библиотека, 44
автоматизированное, 43
гарантия работоспособности, 43
действия, 128
ошибочных ситуаций, 116
понимание программы, 44
разработка через, 89,121
устойчивость к изменениям, 43
Тесты
гигиена, 107
именование, 108
Типы, 92
Транзакция, 188

Число, 62

ш
Шаблон
Builder, 141
DAO, 37
Execute Around, 193
null-объекта, 76
SOLID, 204
агрегации, 61
анти-, 109, 159
для исключений, 68
Единица работы, 188
мапинга, 195
проектирования, И, 12,17
Репозиторий,181
тестирования, 47
уведомления, 69, 72
Фильтр, 197

У
Уарбуртон, Ричард, 216
Уменьшитель, 199
Урма, Рауль-Габриэл, 216
Усовершенствование кода, 123
Утверждение, 46

э
Эванс, Эрик, 92

Я
Ядро, 153

Предметный указатель

|

223

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

Научно-популярное издание
МИРОВОЙ КОМПЬЮТЕРНЫЙ БЕСТСЕЛЛЕР

Рауль-Габриэль Урма
Ричард Уорбертон

ГИД JAVA-РАЗРАБОТЧИКА
ПРОЕКТНО-ОРИЕНТИРОВАННЫЙ ПОДХОД
Главный редактор Р Фасхутдинов
Руководитель направления В. Обручев
Ответственный редактор Е Истомина
Литературный редактор А Голанцева
Младший редактор А. Захарова
Художественный редактор А Гусев
Компьютерная верстка Э Брегис
Корректоры Л. Макарова, А Баскакова

Страна происхождения Российская Федерация
Шыгарылган eni: Ресей Федерациясы
ООО «Издательство «Эксмо»
123308, Россия город Москва, улица Зорге, дом 1, строение 1, этаж 20 кэб 2013
Тел 8(495)41168 86
Нолте page www eksmo ru E-mail info@eksmo ru
©Hflipyuii «ЭКСМО» АКБ Баспасы,
123308 Ресей, кала Мэскеу Зорге xeiueci 1 уй, 1 гимарат 20 хабат офис 2013 ж
Тел 8(495)411 68 86
Home page www eksmo ru Е mail info@eksrno ru
Тауарбелпс! «Эксмо»
Интернет-магазин www book24 ru
Интернет-магазин www book24 kz
Интернет-дукен wwwbook24kz
Импортёр в Республику Казахстан ТОО »РДЦ Алматы»
Казахстан Республикасындагы импорттаушы «РДЦ-Алматы» ЖШС
Дистрибьютор и представитель по приему претензий на продукцию
в Республике Казахстан ТОО «РДЦ Алматы»
Казахстан Республикасында дистрибьютор жене ен!м бойынша арыз-талаптарды
хабылдаушыныц exuii «РДЦ-Алматы» ЖШС
Алматы х Домбровский кеш , 3«а» литер Б, офис 1
Тел 8(727)251-59 90/91/92 Email RDC-AJmaty@eksmo kz
©HiMHin жарамдылых MepaiMi шектелмеген
Сертификация туралы ахпарат сайттв www eksmo ru/certiflcation
Сведения о подтверждении соответствия издания согласно законодательству РФ
о технимеском регулировании можно получить на сайте Издательства «Эксмо»
www eksmo ru/certiflcation
6нд|рген мемлекет Ресей Сертификация харастырылмаган

Дата изготовления / Подписано в печать 11.10 2021.
Формат 70х1001/16. Печать офсетная. Усл печ л 18,15
Тираж 2000 экз Заказ № 9738

Отпечатано в АО «Можайский полиграфический комбинат»
143200, Россия, г Можайск, ул Мира, 93
www oaompk ru, тел (495) 748-04-67, (49638) 20-685

ПРИСОЕДИНЯЙТЕСЬ К НАМ!

БОМБОРА
ИЗДАТЕЛЬСТВО

БОМБОРА - лидер на рынке полезных
и вдохновляющих книг. Мы любим книги
и создаем их, чтобы вы могли творить,
открывать мир, пробовать новое, расти
Быть счастливыми Быть на волне
МЫ В СОЦСЕТЯХ:

ООО bomborabooks
bombora.ru

Q bombora