How Immutability Helps

The following text is a partial translation of the original English article, performed by ChatGPT (gpt-3.5-turbo) and this Jekyll plugin:

В нескольких недавних постах, включая “Getters/Setters. Evil. Period. Objects Should Be Immutable” и “Dependency Injection Containers are Code Polluters”, я всемирно назвал все изменяемые объекты с “сеттерами” (методы объекта, начинающиеся с set) злом. Моя аргументация в основном была основана на метафорах и абстрактных примерах. Однако, очевидно, это не было достаточно убедительно для многих из вас - я получил несколько запросов с просьбой предоставить более конкретные и практические примеры.

Таким образом, чтобы продемонстрировать мое крайне негативное отношение к “изменяемости с помощью сеттеров”, я взял существующую библиотеку на языке Java commons-email от Apache и переработал ее по-своему, без сеттеров и с учетом “объектного мышления”. Я выпустил свою библиотеку в рамках семьи jcabi - jcabi-email. Давайте посмотрим, какие преимущества мы получаем от “чистого” объектно-ориентированного и неизменяемого подхода, без геттеров.

Вот как будет выглядеть ваш код, если вы отправляете электронное письмо с помощью commons-email:

Вот как вы делаете то же самое с jcabi-email:

Я думаю, разница очевидна.

В первом примере вы имеете дело с монструозным классом, который может сделать все за вас, включая отправку вашего MIME-сообщения через SMTP, создание сообщения, настройку его параметров, добавление MIME-частей и т. д. Класс Email из библиотеки commons-email действительно огромный - 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? Я не думаю, что они глупые или непросвещённые. В чем же дело?

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

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

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

Позвольте мне сказать это так - изменяемые классы имеют тенденцию расти в размерах и терять связность.

Если бы авторы commons-email сделали этот класс Email неизменяемым с самого начала, они не смогли бы добавить в него так много методов и инкапсулировать так много свойств. Они не смогли бы превратить его в чудовище. Почему? Потому что неизменяемый объект принимает состояние только через конструктор. Можете представить 33-аргументный конструктор? Конечно, нет.

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

Когда я разрабатывал jcabi-email, я начал с небольшого и простого класса: Postman. Что ж, это интерфейс, так как я никогда не создаю классы без интерфейса. Итак, Postman - это… почтальон. Он доставляет сообщения другим людям. Сначала я создал его версию по умолчанию (я опускаю конструктор для краткости):

Хороший старт, работает. Что теперь? Что ж, Message сложно построить. Это сложный класс из JDK, который требует некоторых манипуляций, прежде чем он может стать красивым HTML-письмом. Поэтому я создал конверт, который будет создавать этот сложный объект для меня (обратите внимание, и Postman, и Envelope являются неизменяемыми и аннотированы @Immutable из jcabi-aspects).

Я также провожу рефакторинг Postman, чтобы он принимал конверт, а не сообщение.

Пока все идет хорошо. Теперь давайте попробуем создать простую реализацию Envelope.

Он работает, но пока не выполняет никаких полезных действий. Он только создает абсолютно пустое MIME-сообщение и возвращает его. Как насчет добавления темы и адресов To: и From: (обратите внимание, класс MIME также является неизменяемым)?

Выглядит правильно и работает. Но всё же слишком примитивно. Как насчёт CC: и BCC:? А что насчёт текста письма? Как насчёт прикреплённых PDF файлов? Что, если я хочу указать кодировку сообщения? Как насчёт Reply-To?

Могу ли я добавить все эти параметры в конструктор? Помните, что класс является неизменяемым, и я не могу добавить метод setReplyTo(). Мне нужно передать аргумент replyTo в его конструктор. Это невозможно, потому что у конструктора будет слишком много аргументов, и никто не сможет его использовать.

Ну, я начал задумываться: как мы можем разбить концепцию “конверта” на более мелкие концепции — и вот что я придумал. Как настоящий конверт в реальной жизни, мой объект MIME будет иметь марки. Марки будут отвечать за настройку объекта Message (снова, Stamp является неизменяемым, так же как и все его реализации):

Теперь я могу упростить мой класс MIME до следующего:

Теперь я создам печати для темы, для Кому:, для От:, для Копия:, для Скрытая копия:, и так далее. Столько печатей, сколько мне нужно. Класс MIME останется таким же - небольшим, связным, читаемым, надежным и т.д.

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

Вот именно суть этой статьи - неизменяемость заставляет вас проектировать маленькие и связные объекты.

Без неизменяемости я пошел бы по тому же пути, что и commons-email. Мой класс MIME стал бы все больше и рано или поздно стал бы таким же большим, как Email из commons-email. Единственное, что остановило меня, - это необходимость провести рефакторинг, потому что я не мог передать все аргументы через конструктор.

Без неизменяемости у меня не было бы такого стимула, и я бы сделал то же самое, что и разработчики Apache с commons-email - раздул класс и превратил его в неподдерживаемое чудовище.

Вот jcabi-email. Надеюсь, этот пример был достаточно иллюстративным и что вы начнете писать более чистый код с помощью неизменяемых объектов.

Translated by ChatGPT gpt-3.5-turbo/42 on 2023-11-28 at 15:37

sixnines availability badge   GitHub stars