[Перевод] Прощай, Объектно-ориентированное программирование.
Уже десятилетия я программирую с на объектно-ориентированных языках программтрования. Первым объектно-ориентированным языком, который я использовал, был C++. После этого был Smalltalk и, в конце, .NET и Java.
Я фанатично использовал преимущество Наследования, Инкапсуляции и Полиморфизма. Три столпа Объектно-ориентированного программирования.
Я жаждал получить обещаное Переиспользование и использовать мудрость полученную теми, кто был до меня в этом новом удивительном мире.
Я не мог сдержать волнения при мысли, что можно сопоставить объекты реального мира с моими классами и ожидал, что всё встанет на свои места.
Ещё никогда я так не ошибался.
Наследование. Первый столп падёт.
На первый взгляд, Наследование кажется самым главным преимуществом ООП. Все упрощённые примеры с иерархией фигур выглядят логичными.

И Переиспользование - это слово дня. Нет… пусть будет словом года и даже больше.
Я повёлся на это и ринулся в мир с новыми знаниями.
Проблема банана, обезьяны, джунглей
С верой в сердце и задачами, требующими решения, я начал выстраивать иерархии классов и писать код. И всё в мире было отлично.
Я никогда не забуду тот день, когда был готов получить преимущества Переиспользования, унаследовавшись от существующего класса. Это был тот самый момент, которого я ждал.
Появился новый проект и я вспомнил про Класс, который я так любил на предыдущем проекте.
Без проблем. Переиспользование - маё спасение. Всё что мне требуется - всего лишь перенести класс из старого проекта в новый.
Ну… На самом деле… не только этот Класс. Нам потребуется ещё родительский класс. Но… И это ещё не всё.
Тьфу… Погодите… Похоже, нам ещё потребуется родительский класс родительского класса… И ещё… Нам потребуются ВСЕ родительские классы. Хорошо… Я справляюсь с этим. Не проблема.
Великолепно. Теперь это не компилируется. Почему?? Ох, вижу… Этот объект содержит другой объект. Таким образом мне требуется и другой класс. Не проблема.
Погодите… Мне нужен не только другой объект. Мне нужен ещё родитель другого класса и родительский класс родительского класса и так далее и так далее для каждого объекта который используется в используемом классе и ВСЕ родительские классы родителей, родители родителей и так далее.
Тьфу.
Вот великолепная цитата создателя Erlang Джо Армстронга:
Проблема объектно-ориентированных языков программирования в том, что у них есть неявные зависимости, котороые они тянут за собой. Вам необходим банан, но вместо этого вы получаете гориллу, держащую банан и все джунгли.
Решение проблемы банана, обезьяны, джунглей
Я решил эту проблему не создавая глубоких иерархий классов. Но если наследование - это ключ к Переиспользованию, то любые ограничения этой возможности ограничат возможности Переиспользования. Так?
Именно.
Так что же делать бедному ООП-программисту?
Включать и Делегировать. Больше об этом далее.
Ромбовидное наследование (Diamond problem)
Рано или поздно эта проблема покажет свою ужасную и, в зависимости от языка программирования, неразрешимую голову.

Многие объекто-ориентированные языки программирования не поддерживают эту возможность, даже не смотря на то, что она выглядит логично. В чём проблема поддержки этой фичи в объектно-ориентированном языке?
Окей, представьте следующий псевдокод:
Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier inherits from Scanner, Printer {
}
Заметьте, что оба класса Scanner и Printer реализуют метод start.
Так какой метод start класс Copier наследует? Scanner? Или Printer? Не может быть, что оба одновременно.
Решение проблемы ромбовидного наследования
Решение простое - не делать подобного.
Да, это так. Многие ОО языки программирования не позволяют вам такого сделать.
Но, но… если я должен сделать подобное? Мне необходимо моё Переиспользование!
Тогда вам надо Включать и Делегировать:
Class PoweredDevice {
}
Class Scanner inherits from PoweredDevice {
  function start() {
  }
}
Class Printer inherits from PoweredDevice {
  function start() {
  }
}
Class Copier {
  Scanner scanner
  Printer printer
  function start() {
    printer.start()
  }
}
Заметьте, что теперь класс Copier содержит ссылки на объекты классов Printer и Scanner. Класс Copier делегирует функцию start реализации из класса Printer. Так же просто можно делегировать её имплементацию классу Scanner.
Эта проблема - ещё одна трещина в столпе Наследования.
Проблема хрупкого базового класса
Итак, моя иерархия классов небольшая и я не допускаю циклических наследований. Бриллианты* не для меня.
И всё было отлично в мире. До тех пор пока…
Однажды, мой работавший ранее код на следующий день перестал работать. Загвоздка в том, что я не менял код.
Возможно, это баг… Но подождите… Что-то изменилось…
И это был не мой код. Ломающее изменение оказалось в базовом классе.
Каким образом изменение базового класса могло поломать мой код?
А вот так…
Представьте, что базовый класс ниже (написан на Java, но дожен быть понятен и тем, кто не знаком с этим языком):
import java.util.ArrayList;
 
public class Array
{
  private ArrayList<Object> a = new ArrayList<Object>();
 
  public void add(Object element)
  {
    a.add(element);
  }
 
  public void addAll(Object elements[])
  {
    for (int i = 0; i < elements.length; ++i)
      a.add(elements[i]); // эта строчка изменится.
  }
}
Обратите внимание на строку с комментарием. Она изменится и приведёт к тому, что мой код перестанет работать.
Класс Array содержит два метода add() и addAll(). Метод add() добавляет в коллекцию один элемент, а метод addAll() добавляет несколько элементов вызывая метод add().
И класс-наследник:
public class ArrayCount extends Array
{
  private int count = 0;
 
  @Override
  public void add(Object element)
  {
    super.add(element);
    ++count;
  }
 
  @Override
  public void addAll(Object elements[])
  {
    super.addAll(elements);
    count += elements.length;
  }
}
Класс ArrayCount является специализацией общего класса Array. С одной разницей, что ArrayCount хранит количество элементов массива в поле count.
Давайте внимательно посмотрим на оба класса.
Метод add() класса Array добавляет элемент в локальный ArrayList.
Метод addAll() класса Array вызывает метод add класса ArrayList для каждого элемента.
Метод add() класса ArrayCount вызывает родительский метод add и затем увеличивает счётчик count. Метод addAll() класса ArrayCount вызывает родительский addAll() и затем увеличивает счётчик count на количество элементов в переданном массиве.
Всё работает отлично.
Теперь ломающее изменение. Строка с комментарием в базовом классе изменилась:
public void addAll(Object elements[])
{
    for (int i = 0; i < elements.length; ++i)
        add(elements[i]); // строчка изменилась
}
До тех пор пока создатель базового класса следит за тем, чтобы всё было нормально - всё будет работать как надо. И все автотесты будут проходить.
Но автору безразличен наследуемый класс. И автор класса-наследника получает проблемы.
Теперь ArrayCount.addAll() вызывает базовый addAll() который внутри вызывает переопределённый метод add() из класса-наследника.
Это приводит к тому, что count увеличивается каждый раз когда вызывается метод add() класса-наследника и увеличивается ещё раз, на количество элеметов в массиве, когда вызывается метод addAll() класса-наследника.
ЭЛЕМЕНЫ ПОДСЧИТЫВАЮТСЯ ДВАЖДЫ.
Если это могло произойти, то это произойдёт, автор класса-наследника обязан знать внутреннюю реализацию базового класса. И владельцев классов-наследников необходимо информировать обо всех изменениях в базовом классе так как это может непредсказуемо повлиять на класс-наследник.
Тьфу! Эта огромная трещина будет всегда угрожать стабильности прекрасного столпа Наследования.
Решение проблемы хрупкого базового класса
И снова Включение и Делегирование - путь к спасению.
Используя Включение и Делегирование мы переходим от разработки Белового Ящика (White Box programming) к разработке Чёрного Ящика (Black Box programming).
При разработке Белого Ящика мы должны знать то, как реализован базовый класс.
В случае Чёрного Ящика мы полностью ингорируем реализацию базового класса поскольку не имеем возможности внедрить код в базовый класс переопределяя его функции. Мы должны придерживаемся лишь некоего интерфейса.
Звучит тревожно…
Наследование должно было быть большим вкладом в Переиспользование кода.
Объектно-ориентированные языки программирования не были спроектированы для того, чтобы можно было просто Внедрять и Делегировать. Они были созданы для простого Наследования.
Если вы похожи на меня, то у вас должны появиться вопросы к Наследованию. Но, что более важно, это должно поколебать вашу уверенность в силе создания классов через иерархии классов.
Проблема Иерархий
Каждый раз, когда я создаю компанию, я сталкиваюсь с проблемой когда мне необходимо место, где будут храниться документы компании. Например, Руководство для сотрудников.
Должна ли быть создана папка “Документы” в которой будет папка с названием “Компания”?
Может быть, я должен создать папку “Компания” и в ней создать папку “Документы”?
Оба варианта подходят. Но какой из них - правильный? Который лучше?
Идея категориальных иерархий была в том, что базовый класс (родитель) является более общим, а классы-наследники (потомки) - более специализированными версиями базового класса. И становятся всё более узко-специализированными по мере спускания по иерархии вниз (посмотрите структуру классов выше).
Но если родитель и потомок могут произвольно меняться местами, то, очевидно, с этой моделью что-то не так.
Решение проблемы Иерархий.
Проблема в том, что…
Категориальные Иерерхии не работают
Тогда для чего иерархии подходят?
Включение
Если вы присмотритесь к реальному миру вокруг, то повсюду заметите Включаемые Иерерхии (или Иерархии Эксклюзивного Владения).
Вы не увидете Категориальных Иерархий. Отличный пример Включаемой Иерархии - это ваши носки. Они находятся в ящике для носков, который является одним из ящиков вашего комода, который находится в вашей спальне, которая находится в вашем доме, и так далее.
Директории на вашем жестком диске - другой пример Включаемой Иерархии. Они содержат папки.
Как мы можем их категоризировать?
Итак, если вы подумали о Документах Компании, то вообще не имеет значения куда я их положу. Я могу положить их в папку Документы или в папку Разное.
Для категоризации я решил использовать теги. И присвоил файлу теги:
Document
Company
Handbook
У тегов нет порядка в иерархии. (Это так же решает проблему ромбовидного наследования.)
Теги похожи на интерфейсы, которых может быть несколько для каждого типа.
Но с таким количеством трещин Наследование, похоже, разрушается.
Прощай, Наследование.
Инкапсуляция. Второй столп падёт.
На первый взгляд, Инкапсуляция - вторая большая плюшка объектно-ориентированного программирования.
Состояние объекта скрыто от внешнего доступа, т.е. оно инкапсулировано в объекте.
Больше нет необходимости думать о глобальных переменных, к которым имеют доступ непонятно кто.
Инкапсуляция предоставляет безопасность вашим переменным.
Инкапсуляция ВЕЛИКОЛЕПНА!!!
Долгой жизни инкапсуляции.
До тех пор пока…
Проблема ссылок
Ради эффективности, объекты передаются по ссылке, а не по значению.
Это означает, что в функцию передаётся не значение объекта, а ссылка или указатель на объект.
Если Объект передаётся в конструктор Объекта, то конструктор может сохранить ссылку на Объект в приватную переменную, которая защищена Инкапсуляцией.
Но переданный Объект не в безопасности!
Почему? Потому что указатель на Объект есть и в других местах. Например, в коде, который вызвал конструктор. Этот код объязан иметь ссылку на Объект. Иначе как он мог бы передать его в конструктор?
Решение проблемы ссылок
Конструктор должен создавать копию переданного Объекта. При этом должно осуществляться глубокое копирование, т.е. создаваться копии всех объектов, являющиеся членами переданного Объекта и копии всех объектов этих объектов и так далее.
Столько всего ради эффективности.
И в этом проблема. Не все объекты можно клонировать. Зависимость некоторых из них от ресурсов операционной системы делает клонирование бесполезным в лучшем случае и невозможным - в худшем.
И ВСЕ популярные объектно-ориентированные языки программирования имеют эту проблему.
Прощай, Инкапсуляция.
Полиморфизм. Третий столп падёт
Полиморфизм был рыжим приёмным ребёнком Объектно Ориентированной Троицы.
Он как Ларри Файн в этой группе.
Каждый раз когда они куда-то отправлялись - он всегда был с ними, но только как второстепенный персонаж.
Это не значит, что Полиморфизм плох, это значит лишь то, что вам не нужен Объектно-Ориентированный язык программирования, чтобы добиться его.
Интерфейсы предоставляют вам это. И без всего багажа Объектно-Ориентированности.
И с Интерфейсами у вас нет ограничений на то, как много различных поведений мы можете комбинировать.
Таким образом, без лишнего шума, попрощаемся с Объектно-Ориентированным Полиморфизмом и скажем “Привет” Полиморфизму, основаному на интерфейсах.
Нарушенные обещания
Ну, Объектно-ориентированное программирование пообещало многое. И эти обещания даются наивным программистам в учебных классах, читающих блоги и проходящих онлайн-курсы.
Мне потребовались годы, чтобы понять как ООП меня обманывало. Я тоже был доверчивым.
И меня сожгли.
И что дальше?
Привет, Функциональное программирование. Было очень приятно работать с тобой последние несколько лет.
Просто чтобы ты знал, я НЕ принимаю ни одно из твоих обещаний за чистую монету. Я должен буду проверить их, чтобы поверить.
Однажды обжёгсись на молоке, будешь дуть и на воду.
Ты понимаешь.
Вы можете присоединиться к сообществу веб-разработчиков изучающих и помогающих друг другу разрабатывать веб-приложения с использование функционального программирования на языке Elm, взгляните на группу в Facebook “Learn Elm Programming”
*Отсылка к оригинальному названию ромбовидных наследований
