Заметки по книге "Элегантные объекты" Егора Бугаенко


Заметки по книге Элегантные объекты Егора Бугаенко

В данной статье хочу поделиться своими заметками на книгу Егора Бугаенко "Элегантные объекты. Java Edition". Книга про Объектно-ориентированное программирование (ООП) с практичными советами по улучшению сопровождения и проектированию проектов. Советы нацелены на то, чтобы сделать классы чище, цельнее и понятнее. К большинству советов я указал ссылку на сайт Егора Бугаенко, где можно более детально изучить совет с примерами и комментариями.

Советы и принципы для применения

Не использовать окончание -er в имени класса

Название класса с "-er" говорит о том, что это набор процедур для манипулирования данными.

💡
Класс нужно называть на основе того, чем он является, а не того, что он делает.

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

Примеры плохих названий:

Manager, Controller, Helper, Handler, Writer, Reader, Converter, Validator (-or также вреден), Router, Dispatcher, Observer, Listener, Sorter, Encoder и Decoder.

Исключением являются такие названия как: User, Computer.

Don't Create Objects That End With -ER
Manager. Controller. Helper. Handler. Writer. Reader. Converter. Validator. Router. Dispatcher. Observer. Listener. Sorter. Encoder. Decoder. This is the class names hall of shame. Have you seen them in your code? In open source libraries you're using? In pattern books? They are all wrong. What do they have in common?
https://www.yegor256.com/2015/03/09/objects-end-with-er.html

Сделайте главным один конструктор в классе

Конструктор - точка входа и создания нового объекта.

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

Это улучшает сопровождаемость создания объектов класса.

There Can Be Only One Primary Constructor
I suggest classifying class constructors in OOP as primary and secondary. A primary constructor is the one that constructs an object and encapsulates other objects inside it. A secondary one is simply a preparation step before calling a primary constructor and is not really a constructor but rather an introductory layer in front of a real constructing mechanism.
https://www.yegor256.com/2015/05/28/one-primary-constructor.html

Конструкторы делать без выполняющего кода

Конструкторы должны только создавать объект не делая обработку переданных в них данных.

Обработка данных буде происходить по требованию вызовом методов объекта.

Плюсы этого подхода:

  • Упрощает конструкторы: в них происходит только инициализация полей.
  • Работа по преобразованию данных происходит только тогда, когда она действительно понадобится. Результат преобразований можно закэшировать с помощью декоратора, если это потребуется.
Constructors Must Be Code-Free
How much work should be done within a constructor? It seems reasonable to do some computations inside a constructor and then encapsulate results. That way, when the results are required by object methods, we'll have them ready. Sounds like a good approach? No, it's not.
https://www.yegor256.com/2015/05/07/ctors-must-be-code-free.html

Инкапсулируйте в классах как можно меньше полей

Класс с небольшим количеством полей легче сопровождать и тестировать.

💡
Следует инкапсулировать не более четырех полей.

Это упрощает идентификацию объекта в системе. Идентичность объекта - это как координаты объекта в системе. Чем больше координат, тем усложняется сопровождаемость.

Инкапсулируйте в классе что-нибудь

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

Наличие состояния у объекта класса также помогает идентифицировать объект в системе.

How Much Your Objects Encapsulate?
Which line do you like more, the first or the second: What is the difference? The first class HTTP encapsulates a URL, while the second one expects it as an argument of method read(). Technically, both objects do exactly the same thing: they read the content of the Google home page.
https://www.yegor256.com/2014/12/15/how-much-your-objects-encapsulate.html

Всегда используйте интерфейсы

Плюсы использования интерфейсов:

  • Описывают контракт класса: что класс может делать. Улучшает организованность и сопровождаемость.
  • Позволяют расцепить (decoupling) классы между собой: легче подменять классы и тестировать.
Seven Virtues of a Good Object
Martin Fowler says: A library is essentially a set of functions that you can call, these days usually organized into classes. Functions organized into classes? With all due respect, this is wrong. And it is a very common misconception of a class in object-oriented programming. Classes are not organizers of functions.
https://www.yegor256.com/2014/11/20/seven-virtues-of-good-object.html#2-he-works-by-contracts

Обдуманно именуйте методы

  • Методы "Строители", которые что-то возвращают в ответе, называть существительными.

Не нужно в имени метода добавлять префиксы: get, create, fetch. Метод сам должен знать как ему создать результат. Добавление префикса делает вызов метода приказом.

Примеры:

int pow(int base, int power);
int speed ();
Employee employee(int id);
String parsedCell(int х, int у);
  • Методы "Манипуляторы" называть глаголами. Они ничего не возвращают в ответе.

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

Примеры:

void save(String content);
void put(String key, Float value);
void remove(Employee emp);
void quicklyPrint(int id);
  • Методы, которые возвращают результат булевого типа называть прилагательными.

Примеры:

boolean empty ();
boolean readable ();
boolean negative();

Не используйте публичные константы

Константы служат для совместного применения данных.

Негативные последствия использования публичных констант объектами разных классов:

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

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

Public Static Literals ... Are Not a Solution for Data Duplication
I have a new String(array,"UTF-8") in one place and exactly the same code in another place in my app. Actually, I may have it in many places. And every time, I have to use that "UTF-8" constant in order to create a String from a byte array.
https://www.yegor256.com/2015/07/06/public-static-literals.html

Делайте классы неизменяемыми

Используя неизменяемые классы мы улучшаем сопровождаемость. Неизменяемые классы проще понять и поддерживать.

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

Плюсы:

  • Атомарность отказов: у нас либо целостный объект, либо происходит отказ. Нет промежуточного состояния.
  • Отсутствие временного сцепления: наличие возможности изменять состояние объекта с помощью сеттеров может привести к ошибкам во время выполнения приложения. Это может неверный порядок использования сеттеров или отсутствие вызова.
  • Отсутствие побочных эффектов: при вызове методов нет неочевидных изменений состояния объекта.
  • Отсутствие NULL-ссылок: объекты создаются в целостном состоянии. Нет необходимости проверять на NULL при дальнейшей работе объекта.
  • Потокобезопасность: неизменяемые объекты всегда в предсказуемом состоянии и могут быть использованы параллельно в разных потоках.
  • Меньшие и более простые объекты: неизменяемость помогает сделать классы чище и короче. Тяжело сделать неизменяемый класс слишком большим.
Objects Should Be Immutable
In object-oriented programming, an object is immutable if its state can't be modified after it is created. In Java, a good example of an immutable object is . Once created, we can't modify its state. We can request that it creates new strings, but its own state will never change.
https://www.yegor256.com/2014/06/09/objects-should-be-immutable.html

Пишите тесты вместо документации

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

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

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

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

💡
Не документируйте код - делайте его чище.

Лучше писать простые понятные классы, чем писать под них документацию.

💡
Хорошо написанные unit-тесты - полезная документация использования класса.

В unit-тесте можно посмотреть:

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

Используйте fаkе-объекты вместо mосk-объектов

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

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

Built-in Fake Objects
While mock objects are perfect instruments for unit testing, mocking through mock frameworks may turn your unit tests into an unmaintainable mess. Thanks to them we often hear that "mocking is bad" and "mocking is evil." The root cause of this complexity is that our objects are too big.
https://www.yegor256.com/2014/09/23/built-in-fake-objects.html

Делайте максимум 5 публичных методов в классе

Плюсы небольших классов:

  • Меньше вероятность сделать ошибку: легче согласовать небольшое количество методов.
  • Лучше сопровождаемость: меньше кода, меньше методов, легче понимать и поддерживать.
  • Проще достичь цельности класса - связи методов класса со всеми его свойствами.
  • Проще тестировать: меньше точек входа и проще воспроизвести все сценарии использования.

Не используйте статические методы

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

Также не стоит использовать классы-утилиты. Это не полноценный объект, а набор процедур.

OOP Alternative to Utility Classes
A utility class (aka helper class) is a "structure" that has only static methods and encapsulates no state. StringUtils, IOUtils, FileUtils from Apache Commons; Iterables and Iterators from Guava, and from JDK7 are perfect examples of utility classes. This design idea is very popular in the Java world (as well as C#, Ruby, etc.)
https://www.yegor256.com/2014/05/05/oop-alternative-to-utility-classes.html

Не допускайте аргументов со значением NULL

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

Если пользователь метода все-таки передал NULL, лучше игнорировать такое использование и не делать дополнительную проверку. В конечном итоге это все равно приведет к ошибке NullPointerException и пользователю нужно будет исправить ошибку.

Why NULL is Bad?
A simple example of NULL usage in Java: What is wrong with this method? It may return NULL instead of an object-that's what is wrong. NULL is a terrible practice in an object-oriented paradigm and should be avoided at all costs.
https://www.yegor256.com/2014/05/13/why-null-is-bad.html

Не используйте геттеры и сеттеры

Полноценные объекты в ООП это не простые структуры данных.

Структура данных - это набор данных, прозрачный ящик. Мы напрямую общаемся с ее данными. Структура данных пассивная.

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

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

Getters/Setters. Evil. Period.
There is an old debate, started in 2003 by Allen Holub in this Why getter and setter methods are evil famous article, about whether getters/setters is an anti-pattern and should be avoided or if it is something we inevitably need in object-oriented programming. I'll try to add my two cents to this discussion.
https://www.yegor256.com/2014/09/16/getters-and-setters-are-evil.html

Не используйте оператор new вне вторичных конструкторов

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

Лучше использовать инъекцию (внедрение) зависимостей с использованием интерфейсов.

Во вторичных конструкторах можно использовать new() для создания объектов своего класса.

Operator new() is Toxic
To instantiate objects, in most object-oriented languages, including Java, Ruby, and C++, we use operator new(). Well, unless we use static factory methods, which we don't use because they are evil. Even though it looks so easy to make a new object any time we need it, I would recommend to be more careful with this rather toxic operator.
https://www.yegor256.com/2018/01/02/operator-new-is-toxic.html

Избегайте интроспекции и приведения типов

Речь идет об операторе instanceof и методе Class.cast() в Java или их аналогах в других языках.

Приведение типов и проверки на тип негативно сказываются на понимании и сопровождаемости кода.

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

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

Class Casting Is a Discriminating Anti-Pattern
Type casting is a very useful technique when there is no time or desire to think and design objects properly. Type casting (or class casting) helps us work with provided objects differently, based on the class they belong to or the interface they implement.
https://www.yegor256.com/2015/04/02/class-casting-is-anti-pattern.html

Никогда не возвращайте NULL

Возврат NULL вносит недоверие к классу. Это заставляет пользователей класса перепроверять и обрабатывать результат его работы.

Возврат NULL - это попытка безопасного отказа, что приводит к ухудшению сопровождаемости.

Есть три варианта решения:

  • Выбрасывать исключение для быстрого отказа (fail fast). Это помогает клиенту быстрее увидеть и обработать ошибку.
  • Возвращать коллекцию объектов. Это помогает обработать вариант с пустым результатом поиска объекта.
  • Использовать паттерн "Пустой объект" ("Null object"), если это подходит под ситуацию. Такой объект похож на настоящий, но он ведет себя по другому.
Why NULL is Bad?
A simple example of NULL usage in Java: What is wrong with this method? It may return NULL instead of an object-that's what is wrong. NULL is a terrible practice in an object-oriented paradigm and should be avoided at all costs.
https://www.yegor256.com/2014/05/13/why-null-is-bad.html

Бросайте только проверяемые исключения

Проверяемые исключения в Java добавляет контракт и улучшает документирование кода. Лучше использовать их вместо непроверяемых.

Еще несколько советов:

  • Не ловить исключения без необходимости: обработку исключений лучше производить на более верхних уровнях приложения
  • Стройте цепочки исключений: ловим исключение на нижнем уровне и выбрасываем новое с дополнительной информацией и словленным исключением в цепочке. Так улучшается контекст ошибки.
  • Восстанавливайтесь единожды: восстановление и возврат пользователю ошибки должны быть на самом верхнем уровне приложения.
Checked vs. Unchecked Exceptions: The Debate Is Not Over
Do we need checked exceptions at all? The debate is over, isn't it? Not for me. While most object-oriented languages don't have them, and most programmers think checked exceptions are a Java mistake, I believe in the opposite-unchecked exceptions are the mistake. Moreover, I believe multiple exception types are a bad idea too.
https://www.yegor256.com/2015/07/28/checked-vs-unchecked-exceptions.html

Делайте классы либо константными (final), либо абстрактными

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

Лучше использовать константные (final) классы или абстрактные родительские классы. Это относится как к самому классу, так и к методам.

Такой подход позволяет четко указывать намерения и предотвращает возможность неудачного использования наследования:

  • final - черный ящик для его пользователей. Его можно только инкапсулировать.
  • abstract - незавершенный прозрачный ящик, полуфабрикат. У него есть отсутствующие компоненты, которые нужно сделать.
Seven Virtues of a Good Object
Martin Fowler says: A library is essentially a set of functions that you can call, these days usually organized into classes. Functions organized into classes? With all due respect, this is wrong. And it is a very common misconception of a class in object-oriented programming. Classes are not organizers of functions.
https://www.yegor256.com/2014/11/20/seven-virtues-of-good-object.html#7-his-class-is-either-final-or-abstract

Завершение

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

Сайт книги Elegant Objects:

Elegant Objects
Elegant Objects (EO) is an object-oriented programming paradigm that renounces traditional techniques like null, getters-and-setters, code in constructors, mutable objects, static methods, annotations, type casting, implementation inheritance, data objects, etc. Books: Online: Telegram chat and@painofoop (Russian only). Join our soalition if you are ready to help. Twitter: #elegantobjects.
https://www.elegantobjects.org/

Сайт автора книги Егора Бугаенко:

Elegant Objects
"Elegant Objects" is my first book. It is about object-oriented programming from an idealistic and, maybe, a bit of a futuristic point of view. There are 23 independent recommendations for a practical OOP designer on how to make code cleaner, classes more solid, and architecture more visible.
https://www.yegor256.com/elegant-objects.html

Спасибо, что прочитали. Буду вам очень признателен если поделитесь этой статьей!

Опубликовано