В нескольких последних постах, в том числе «Геттеры / Сеттеры. Злой. Период «. , «Объекты должны быть неизменными» и «Контейнеры внедрения зависимостей являются загрязнителями кода» , я повсеместно обозначил все изменяемые объекты как «сеттеры» (методы объектов, начинающиеся с set
) зло. Моя аргументация основывалась главным образом на метафорах и абстрактных примерах. Очевидно, для многих из вас это было недостаточно убедительно — я получил несколько запросов с просьбой предоставить более конкретные и практические примеры.
Таким образом, чтобы проиллюстрировать мое строго негативное отношение к «изменчивости через сеттеры», я взял существующую Java-библиотеку обыкновенных писем от Apache и перепроектировал ее по-своему, без сеттеров и с «объектным мышлением». Я выпустил свою библиотеку как часть семьи jcabi — jcabi-email . Давайте посмотрим, какие преимущества мы получим от «чистого» объектно-ориентированного и неизменного подхода без получателей.
Вот как будет выглядеть ваш код, если вы отправите электронное письмо по адресу commons-email:
1
2
3
4
5
6
7
8
9
|
Email email = new SimpleEmail(); email.setHostName( "smtp.googlemail.com" ); email.setSmtpPort( 465 ); email.setAuthenticator( new DefaultAuthenticator( "user" , "pwd" )); email.setSubject( "how are you?" ); email.setMsg( "Dude, how are you?" ); email.send(); |
Вот как вы делаете то же самое с jcabi-email :
01
02
03
04
05
06
07
08
09
10
11
12
13
14
|
Postman postman = new Postman.Default( new SMTP( "smtp.googlemail.com" , 465 , "user" , "pwd" ) ); Envelope envelope = new Envelope.MIME( new Array<Stamp>( new StSubject( "how are you?" ) ), new Array<Enclosure>( new EnPlain( "Dude, how are you?" ) ) ); postman.send(envelope); |
Я думаю, что разница очевидна.
В первом примере вы имеете дело с классом монстров, который может сделать для вас все, включая отправку сообщения MIME через SMTP, создание сообщения, настройку его параметров, добавление в него частей MIME и т. Д. Класс Email
от commons- Электронная почта — это действительно огромный класс — 33 частных объекта, более сотни методов, около двух тысяч строк кода. Сначала вы конфигурируете класс с помощью набора установщиков, а затем просите его send()
электронное письмо для вас.
Во втором примере у нас есть семь объектов, созданных с помощью семи new
вызовов. Postman
отвечает за упаковку сообщения MIME; SMTP
отвечает за отправку через SMTP; марки ( StSender
, StRecipient
и StSubject
) отвечают за настройку сообщения MIME перед доставкой; EnPlain
отвечает за создание части MIME для сообщения, которое мы собираемся отправить. Мы создаем эти семь объектов, инкапсулируя один в другой, и затем просим почтальона send()
конверт для нас.
Что не так с изменяемой электронной почтой?
С точки зрения пользователя, в этом нет почти ничего плохого. Email
— это мощный класс с множеством элементов управления — просто нажмите правильный, и работа будет выполнена. Однако, с точки зрения разработчика, класс Email
— это кошмар. Главным образом потому, что класс очень большой и сложный в обслуживании.
Поскольку класс такой большой , каждый раз, когда вы хотите расширить его, вводя новый метод, вы сталкиваетесь с тем фактом, что вы делаете класс еще хуже — длиннее, менее сплоченным, менее читабельным, менее обслуживаемым и т. Д. Вы есть ощущение, что ты копаешься во что-то грязное и нет надежды сделать это чище, никогда. Я уверен, что вы знакомы с этим чувством — большинство устаревших приложений выглядят именно так. У них есть огромные многострочные «классы» (на самом деле, программы на языке COBOL, написанные на Java), которые были унаследованы от нескольких поколений программистов до вас. Когда вы начинаете, вы полны энергии, но после нескольких минут прокрутки такого «класса» вы говорите: «Винт, это почти суббота».
Поскольку класс настолько велик , больше нет скрытия или инкапсуляции данных — 33 переменные доступны более чем 100 методам. Что скрыто? Этот файл Email.java
в действительности представляет собой большой процедурный 2000-строчный скрипт, который по ошибке называют «классом». Ничего не скрыто, когда вы пересекаете границу класса, вызывая один из его методов. После этого вы получите полный доступ ко всем данным, которые могут вам понадобиться. Почему это плохо? Ну, зачем нам нужна инкапсуляция? Чтобы защитить одного программиста от другого, он же защитное программирование . В то время как я занят изменением темы сообщения MIME, я хочу быть уверен, что мне не мешает деятельность какого-либо другого метода, то есть изменение отправителя и касание моей темы по ошибке. Инкапсуляция помогает нам сузить суть проблемы, в то время как этот класс Email
делает с точностью до наоборот.
Поскольку класс такой большой , его модульное тестирование еще сложнее, чем сам класс. Почему? Из-за множественных взаимозависимостей между его методами и свойствами. Чтобы протестировать setCharset()
вы должны подготовить весь объект, вызвав несколько других методов, затем вы должны вызвать send()
чтобы убедиться, что отправляемое сообщение действительно использует указанную вами кодировку. Таким образом, чтобы протестировать однострочный метод setCharset()
вы запускаете весь сценарий интеграционного тестирования, отправляя полное сообщение MIME через SMTP. Очевидно, что если что-то изменить в одном из методов, это повлияет почти на каждый метод тестирования. Другими словами, тесты очень хрупкие, ненадежные и чрезмерно сложные.
Я могу продолжать и продолжать это « потому что класс такой большой », но я думаю, что очевидно, что маленький, сплоченный класс всегда лучше, чем большой. Это очевидно для меня, для вас и любого объектно-ориентированного программиста. Но почему это не так очевидно для разработчиков Apache Commons Email? Я не думаю, что они глупые или необразованные. Что тогда?
Как и почему это случилось?
Так всегда и бывает. Вы начинаете проектировать класс как нечто связное, солидное и маленькое. Ваши намерения очень позитивные. Очень скоро вы понимаете, что есть что-то еще, что этот класс должен сделать. Затем что-то еще. Тогда еще больше.
Лучший способ сделать ваш класс более мощным — это добавить сеттеры, которые вводят параметры конфигурации в класс, чтобы он мог обрабатывать их внутри, не так ли?
Это коренная причина проблемы! Основной причиной является наша способность вставлять данные в изменяемые объекты с помощью методов конфигурации, также известных как «сеттеры». Когда объект изменчив и позволяет нам добавлять сеттеры, когда мы захотим, мы будем делать это без ограничений.
Позвольте мне выразиться так: изменчивые классы имеют тенденцию расти в размерах и теряют связность .
Если бы авторы обыкновенной электронной Email
в начале сделали этот класс Email
неизменным, они бы не смогли добавить в него столько методов и инкапсулировать так много свойств. Они не смогут превратить его в монстра. Почему? Потому что неизменный объект принимает состояние только через конструктор. Можете ли вы представить конструктор с 33 аргументами? Конечно нет.
Когда вы делаете свой класс неизменным в первую очередь, вы вынуждены сохранять его сплоченным, маленьким, прочным и устойчивым. Потому что вы не можете инкапсулировать слишком много, и вы не можете изменить то, что инкапсулировано. Всего два или три аргумента конструктора, и все готово.
Как я разработал неизменное письмо?
Когда я разрабатывал jcabi-email, я начал с небольшого и простого класса: Postman
. Ну, это интерфейс, так как я никогда не делаю классов без интерфейса. Итак, Postman
… Postman
. Он доставляет сообщения другим людям. Сначала я создал его версию по умолчанию (для краткости опускаю ctor):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
import javax.mail.Message; @Immutable class Postman.Default implements Postman { private final String host; private final int port; private final String user; private final String password; @Override void send(Message msg) { // create SMTP session // create transport // transport.connect(this.host, this.port, etc.) // transport.send(msg) // transport.close(); } } |
Хорошее начало, это работает. Что теперь? Ну, Message
сложно построить. Это сложный класс из JDK, который требует некоторых манипуляций, прежде чем он может стать хорошим HTML-письмом. Поэтому я создал конверт, который будет создавать для меня этот сложный объект (обратите внимание, что и Postman
и Envelope
являются неизменяемыми и помечены @Immutable из jcabi-aspect ):
1
2
3
4
|
@Immutable interface Envelope { Message unwrap(); } |
Я также рефакторинг Postman
чтобы принять конверт, а не сообщение:
1
2
3
4
|
@Immutable interface Postman { void send(Envelope env); } |
Все идет нормально. Теперь давайте попробуем создать простую реализацию Envelope
:
1
2
3
4
5
6
7
8
9
|
@Immutable class MIME implements Envelope { @Override public Message unwrap() { return new MimeMessage( Session.getDefaultInstance( new Properties()) ); } } |
Это работает, но ничего полезного пока нет. Он только создает абсолютно пустое сообщение MIME и возвращает его. Как насчет добавления темы и адресов To:
и From:
(обратите внимание, класс MIME
также неизменен):
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@Immutable class Envelope.MIME implements Envelope { private final String subject; private final String from; private final Array<String> to; public MIME(String subj, String sender, Iterable<String> rcpts) { this .subject = subj; this .from = sender; this .to = new Array<String>(rcpts); } @Override public Message unwrap() { Message msg = new MimeMessage( Session.getDefaultInstance( new Properties()) ); msg.setSubject( this .subject); msg.setFrom( new InternetAddress( this .from)); for (String email : this .to) { msg.setRecipient( Message.RecipientType.TO, new InternetAddress(email) ); } return msg; } } |
Выглядит правильно, и это работает. Но это все еще слишком примитивно. Как насчет CC:
и BCC:
А как насчет текста электронной почты? Как насчет вложений PDF? Что если я хочу указать кодировку сообщения? А как насчет Reply-To
?
Могу ли я добавить все эти параметры в конструктор? Помните, что класс является неизменным, и я не могу представить метод setReplyTo()
. Я должен передать аргумент replyTo
в его конструктор. Это невозможно, потому что конструктор будет иметь слишком много аргументов, и никто не сможет его использовать.
Итак, что мне делать?
Ну, я начал думать: как мы можем разбить понятие «конверт» на более мелкие понятия — и это то, что я изобрел. Как и в реальном конверте, мой объект MIME
будет иметь штампы. Stamps будет отвечать за настройку объекта Message
(опять же, Stamp
является неизменным, как и все его разработчики):
1
2
3
4
|
@Immutable interface Stamp { void attach(Message message); } |
Теперь я могу упростить мой класс MIME
следующим образом:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
@Immutable class Envelope.MIME implements Envelope { private final Array<Stamp> stamps; public MIME(Iterable<Stamp> stmps) { this .stamps = new Array<Stamp>(stmps); } @Override public Message unwrap() { Message msg = new MimeMessage( Session.getDefaultInstance( new Properties()) ); for (Stamp stamp : this .stamps) { stamp.attach(msg); } return msg; } } |
Теперь я создам штампы для темы, для To:
для From:
для CC:
для BCC:
и т. Д. Столько марок, сколько мне нравится. Класс MIME
останется прежним — маленький, сплоченный, читаемый, солидный и т. Д.
Здесь важно то, почему я принял решение провести рефакторинг, когда класс был относительно небольшим. Действительно, я начал беспокоиться об этих классах штампов, когда мой класс MIME
был всего 25 строк.
Именно в этом смысл этой статьи — неизменность заставляет вас создавать маленькие и сплоченные объекты .
Без неизменности я бы пошел в том же направлении, что и обычная электронная почта. Мой MIME
класс увеличился бы в размерах и рано или поздно стал бы таким же большим, как Email
от commons-email. Единственное, что остановило меня, это необходимость рефакторинга, потому что я не мог передать все аргументы через конструктор.
Без неизменности у меня не было бы этого мотиватора, и я бы сделал то, что разработчики Apache сделали с обычной электронной почтой — раздул класс и превратил его в неуправляемого монстра.
Это jcabi-электронная почта . Я надеюсь, что этот пример был достаточно иллюстративным, и вы начнете писать более чистый код с неизменяемыми объектами.
Похожие сообщения
Вы также можете найти эти сообщения интересными:
- Парные скобки
- Избегайте конкатенации строк
- Типичные ошибки в коде Java
- Контейнеры DI являются загрязнителями кода
- Геттеры / сеттеры. Злой. Период.
Ссылка: | Как неизменность помогает наш партнер по JCG Егор Бугаенко в блоге About Programming . |