Это продолжение предыдущего поста на тему ООП. Напомню, что пишу его под воздействием книги Егора “Elegant Objects”. Прошлая публикация была немного абстрактной и слегка резковатой. Полагаю, так случается со всеми после чтения какого-либо из текстов Егора. Недаром его идеи будоражат интернет: каждый пост становится предметом бесконечных обсуждений.

В этой части поговорим на более технические темы, поэтому тональность будет поспокойней. Напомню, это не полноценная рецензия на книгу; их уже написано достаточно. Скорее, ниже приведены возражения к некоторым тезисам из “Elegant Objects”.

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

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

Напомню, Егор предлагает строить программы при помощи неизменяемых объектов. Каждый такой объект представляет интересы реальной и, возможно, меняющейся сущности реального мира. Объект должен запрашивать минимум входных данных, в противном случае он становится композицией других объектов поменьше. С этими же “элегантными” объектами связан отказ от NULL, getters/settets, наследования и других вещей из промышленного ООП, с чем я полностью согласен.

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

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

Думаю, каждый сталкивался с ситуацией в проекте на Java, когда на каждый квант информации заведен класс, и совершенно не ясно, как построить общую картину. Аргумент выше я позаимствовал из блога Дмитрия Бушенко, который тоже писал отзыв на книгу. Здесь мне нечего добавить к аргументу Дмитрия: помню, разбираясь с Хаскелом, я тоже был поражен взрывному росту типов по мере роста программы. На каждую пару есть функция-конвертор, но пока ее найдешь…

Примеры, которые приводит Егор с классами Cash и пр. действительно смотрятся красиво. То же самое можно сказать про класс Max, который инкапсулирует два числа и сам притворяется числом, производя вычисления только когда к нему обращаются как к числу. Сама элегантность! Но все же у меня остались сомнения, что средних размеров проект, выстроенный на маленьких объектах, не начнет пошатываться из-за множества внутренних связей.

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

new LengthOfInput(
  new TeeInput(
    new BytesAsInput(
      new TextAsBytes(
        new StringAsText(
          "Hello, world!"
        )
      )
    ),
    new FileAsOutput(
      new File("/tmp/hello.txt")
    )
  )
).value(); // happens here

Оставим за скобками вопрос о длине кода (императивная версия займет три строки); нас пока это не интересует.

Главное преимущество подхода Егора выражается в декларативности: мы выстраиваем дерево объектов, которое ничего не делает при создании. Такое дерево можно построить, где-то хранить, передавать куда-то или даже сериализовать. Типичному Lisp-программисту это напомнит принцип “код как данные”, хотя очень условно.

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

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

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

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

Словом, я не уверен в достоинствах дерева объектов.

Следующий момент, на который я бы хотел обратить внимание: приглядитесь к именам классов из примера выше. Мне бросается в глаза одинаковый паттерн именования: ThisAsThat, то есть Одна сущность в Другую. У экземпляров этих классов всего лишь один метод .value() (или .text(), .bytes(), что угодно), который возвращает результат типа второй сущности.

Налицо утилитарность таких объектов: их жизненный цикл состоит исключительно в том, чтобы перевести данные из одного типа в другой. Нужен ли этот класс в дальнейшем? Нет. Это просто преобразование из А в Б. Такой объект даже не заслуживает “уважительного”, как пишет Егор, отношения. Это просто преобразователь, конвертор, читатель, что угодно.

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

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

Взгляните на код: буквально на ровном месте мы возвели множество сущностей: Text, Bytes, Input, TeaInput. А ведь всего-то требовалось скопировать данные. С точки зрения программиста, у которого в проекте десяток библиотек и фреймворков, это должна быть автомарная операция.

Сейчас я попрошу вас, не перематывая страницу вверх, мысленно построить это же дерево. Что было первым? Пишу по-честному: сначала LengthOfInput, под ним TeaInput, а дельше цепочка из Одного в Другое. То ли байты в текст, то ли строка из текста или наоборот. Кто на ком стоял? Теперь подумайте о том, что копирование данных происходит постоянно и во многих местах. Неужели придется держать порядок в голове?

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

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