О книге

Книга “Элегантные объекты. Java Edition” не сильно распространена на рынке в Казахстане, где я живу. Как только узнал, что появилась возможность приобрести в магазине flip.kz - купил тут же.

В предисловии автор рассказывает историю создания языков через его призму восприятия. Он пишет, что 20 лет назад языки программирования были процедурные, не было классов и ООП. И именно эти программисты, которые мыслили процедурно, и создали первые ООП-языки. Егор нисколько не умаляет их заслуг, но говорит о том, что подход к ООП с тех пор практически не изменился и программисты сейчас пишут на Java/.NET/Ruby так же, как писали процедурные программисты на первых ООП-языках.

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

Честно говоря, я не думаю, что прав во всем, о чем говорю. Я сам многие годы был процедурным программистом.

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

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

Нет именам классов, оканчивающимся на -er

Класс в ООП - это представитель какого-то объекта из реального мира. Основной тезис Егора в том, что в реальном мире нет Хэлперов, Врапперов, Ридеров, Хэндлеров и Контроллеров. Исключения - исторические слова наподобие User или Computer, образованные от слов use и compute соответственно.

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

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

Если мы называем класс именем с -er, то это говорит нам и другим программистам, что класс - набор процедур для манипулирования данными, а не сам объект. Статья Егора расскажет о принципе более подробно.

Один главный конструктор

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

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

В конструкторах нет места коду

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

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

Принцип также находит отклик и в философском подходе к ООП: ООП – это декларативное программирование, а не императивное, но об этом позднее. У Егора есть статья на эту тему.

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

Чем меньше кода, тем легче его поддерживать и сопровождать. Егор рекомендует инкапсулировать не более четырех объектов. Цифра взята из ниоткуда, как признается Егор, он просто вывел ее “по опыту”. Набор инкапсулированных объектов называется состоянием (идентичностью) объекта. Это значит, что класс с одинаковыми значениями в трех его внутренних объектах должны считаться одинаковыми при операциях проверки на идентичность.

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

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

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

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

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

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

Тщательно выбирайте имена методов

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

Метод-строитель - это такой метод, который возвращает какой-то созданный класс. Строитель никогда не должен возвращать void, это противоречит его природе. В имени метода мы также не должны указывать и способ создания объекта, используя слова fetchObject, createObject или getObject.

Егор приводит аналогию с кофейней. Когда мы приходим в кофейню, мы не говорим “Сварите мне кофе”, мы лишь говорим “Я бы хотел чашку кофе”. Во втором случае кофейня сама решает, как мне предоставить зака: сварить кофе, использовать быстрорастворимый или разогреть недопитый кофе предыдущего клиента (прим.ред: шутка про недопитый кофе - моя). Это и есть декларативный подход - класс, у которого мы вызываем метод, сам решает, как его создать и как его построить.

Методы-манипуляторы должны именоваться глаголами для того, чтобы показать, что они что-то делают и преобразовывают. Манипуляторы ничего не возвращают. Клиенты такого класса лишь просят его сделать что-либо, а класс уже сам решает, выполнить просьбу или нет. В аналогию Егор приводит пример с музыкой в кофейне. Нам, к примеру, не нравится громкость музыки в заведении. Если мы попросим убавить звук фразой “Убавьте, пожалуйста, звук, а как убавите, скажите ее громкость”. Такая просьба звучит неуважительно, мы уже вынесли решение об изменении громкости вместо того, кто на самом деле должен это делать. Именно поэтому и методы-манипуляторы возвращают лишь void.

Исключение из правила - методы, возвращающие boolean. Например, success(). Можно было бы назвать этот метод checkSuccess(), но тогда все подобные методы содержали бы префикс check, что ухудшило бы читаемость.

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

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

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

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

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

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

  • Атомарность отказов. Объекты инстанцируются либо полностью, либо происходит отказ операции.
  • Отсутствие временного сцепления. Если нам нужно сформировать класс последовательным вызовом его сеттеров (про сеттеры отдельный пункт есть), то неверный порядок вызовов может привести к проблемам в рантайме.
  • Отсутствие побочных эффектов. Человечкский фактор может привести к ошибкам в коде. Именно сокращение влияния человеческого фактора и является путем к повышению качества кода.
  • Отсутвие NULL-ссылок. О NULL будет написан отдельный пункт, но пока что можно сказать, что если одно из наших приватных полей не инициализировано сразу, значит в остальном коде от нас потребуется проверять его на NULL постоянно.
  • Потокобезопасность. Если наш класс используется в мультипоточном исполнении, то так может произойти, что в одном потоке класс еще полностью не был инстанцирован, а в другом от него уже ожидается некий результат. В итоге - неконсистентные операции.
  • Объекты становятся меньше и проще. Чем проще объект, тем легче его поддерживать.

Комментаторы в блоге указывают Егору, что если инстанцировать каждый раз новые объекты, то куча (heap) может быстро заполниться ненужными объектами, и перформанс системы просядет. Егор же в ответ говорит, что сейчас железо стоит гораздо дешевле, чем время программистов, поэтому чем меньше времени программист тратит на чтение и написание кода, тем дешевле становится проект.

Более подробно в статье Егора.

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

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

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

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

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

Используйте fake-объекты вместо mock-объектов

Моки вынуждают авторов кода относиться к классам как к прозрачным ящикам. Более того, мокинг классов превращает предположения в факты. Например, мы передает в класс Кэша класс Биржи для конвертации валюты. Но вместо того, чтобы использовать оригинальный класс Биржи, мы создаем мок и передаем его как зависимость. В моке мы настраиваем возвращаемый результат рейта конвертации на нужный и ассертим результат работы класса Кэш. Что это значит? Что мы сделали предположение, что Кэш будет использовать метод rate() класса Биржа, хотя об этом мы можем строить лишь предположения. Получается, что если мы изменим внутренний код класса Кэш, то мок об этом ничего не узнает, и юниттесты посыпятся.

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

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

Делайте интерфейсы краткими, используйте smart-классы

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

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

Предоставляйте менее пяти публичных методов

Чем меньше кода, тем легче его сопровождать. Этот принцип перекликается с принципом о четырех приватных полях. Цифра “пять” тоже взята из головы, по словам автора. Но по сути Егор прав: чем меньше публичных методов в классе, тем легче понять его предназначение.

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

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

Императивное программирование - это когда мы вызываем статический метод с параметрами в процессе исполнения программы и сохраняем в переменной ее результат “тут же”. Как в процедурном языке C до изобретения ООП. Декларативный же стиль предполагает, что мы вместо вызова операций объявляем (декларируем), что в некоторая переменная - это результат выполнения других операций, который будет вычислен позже. Реализация такого подхода достигается за счет классов и их компоновки. Затем, когда нам понадобится результат операции, мы вызываем публичный метод у созданной декларативно переменной и используем его.

В главе также Егор предлагает идти дальше и использовать компонуемые декораторы. Об этом он говорил еще на докладе “Utility-классы нас убивают”. Также более подробно - в статье Егора.

Примечание редактора. Это очень сильно напоминает функциональный подход, где объявляется цепочка вызовов методов с некоторыми параметрами, но сами методы не вызываются до последнего. Также это напоминает и LinqToSQL из .NET, где мы можем навесить кучу экстеншенов на IQueryable, и только по вызову метода ToArrayAsync() мы получим искомую выборку. Иначе говоря, отложенное ленивое исполнение.

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

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

Отношение Егора к NULL весьма однозначно, судя по книге - их просто не должно существовать. Мысль заключается в том, что когда мы задаем значение какой-то переменной NULL, то мы заведомо начинаем не доверять своему же коду и сами решаем проверкой на NULL, нужно ли нам “общаться” с этой переменной. Иначе говоря, не спрашиваем у самого объекта, есть ли у него все нужные нам данные, а просто игнориуем его, если “ему и сказать нечего”.

Проверки на NULL утяжеляют код, делают его менее поддерживаемым.

Примечание редактора. Противоречивый тезис. С одной стороны, проверки на NULL действительно утяжеляют чтение кода, но эта проблема в .NET решается методом-экстеншеном (привет утилитарным методам). С другой стороны, а как можно передать “пустой” объект строки? Вводить некоторую прослойку типа Nullable<T>? В общем, применение этого принципа повлечет утяжеление кода для неподготовленных разработчиков, нужно будет объяснять концепцию остальным сначала.

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

Использование гетерой и сеттеров - плохо по двум основным причинам:

  • Раскрывают детали класса, делая инкапсуляцию бессмысленной.
  • Превращают объекты в некие структуры данных наподобие DTO-классов.

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

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

Внедрение зависимостей - полезная штука, позволяющая разделить классы и уменьшить их связанность. Почему нельзя использовать new вне вторичных констрикторов? Потому что это сразу показывает плохую архитектуру класса.

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

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

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

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

Интроспекция - это “рефлексия”. Приведение типов и проверка на тип вредны для кода, потому что ухудшают понимание кода. Действительно, зачем нам принимать в качестве зависимости базовый класс/интерфейс, чтобы затем проверть его на соответствие более конкретного типа и вызвать у этого самого конкретного типа его методы.

Рефлексия - хороший инструмент для плохих программистов

Приведение типов нарушает ООП путем дискриминации объектов по типу. Мы взаимодействуем с некоторым объектом по-разномув зависимости от его типа. Это довольно странная логика. По идее, если дочерний тип общего ведет себя иначе (раз требуется проверка на тип и приведение), то это проблема проектирования того самого дочернего типа, а не нашего класса.

Егор приводит в пример интересную аналогию. Приведение типов - это как будто мы зовем сантехника починить кран, но затем мы обращаемся к нему “Я полагаю, что вы еще и компьютерщик, так что почините мне принтер”. А если сантехник - никакой не компьютерщик, то у нас будет баг в системе.

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

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

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

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

Возврат NULL в качестве результата - это в некотором роде подход “Безопасного отказа”. Противоположный подход - это “быстрый отказ” (fail fast). Безопасный отказ - это максимальные попытки “сгладить углы” и не выбрасывать исключения, а стараться обрабатывать их как можно безопаснее. Следование этому подходу может аукнуться долгоживущими багами в системе.

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

Примечание редактора. У меня все тот же аргумент против этого тезиса - а что делать с примитивами типа строки? Альтернативы нет, и Егор тоже не дает ее.

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

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

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

Общее мнение

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

Мне также нравится, что автор приводит и противоположное мнение комментаторов к его блог-постам по теме главы. Это показывает, что Егор открыт к дискуссиям, даже если его оппоненты не всегда (полит)корректно выражают свои мысли. Конечно, эти лишь выжимка, которая может быть выгодна автору, однако в книге приведены ссылки на конкретные обсуждения, так что любой читатель может пройти по ним и ознакомиться с оригинальными комментариями.

Ну и в довершение моей статьи-обзора-конспекта пара забавных (на мой взгляд) цитат Егора, которые он использовал в дискуссиях.

Я думаю, что программирование - это образ жизни, религия, искусство, но никак не процесс создания инструмента. Вы проводите 1% жизни на свиданиях, а 80% - за компьютером. Почему мы должны встречаться с красивыми мужчинами/женщинами, но при этом не беспокоимся о красоте собственного кода?

Bruno S.: Это все придирки ради славы и денег. Делайте качественные приложения и называйте методы как угодно.

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