Найти в Дзене
Clean Code

Goodbye, Object Oriented Programming

Оглавление

Прощай, объектно-ориентированное программирование.

Я программировал на объектно-ориентированных языках десятилетиями. Первым OO языком, который я использовал, был C++, затем Smalltalk и, наконец, .NET и Java.

Я был одержим, чтобы использовать преимущества Наследования, Инкапсуляции и Полиморфизма. Три Столпа Парадигмы.

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

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

Я никогда так не ошибался.

Inheritance, the First Pillar to Fall

-2

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

-3

И Повторное использование это слово дня. Нет … сделать это за год и, возможно, навсегда.

Я проглотил всё это и бросился в мир со своим новым пониманием.

Banana Monkey Jungle Problem

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

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

Появился новый проект, и я вспомнил Класс, который мне так понравился в моем последнем проекте.

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

Ну … на самом деле … не только этот класс. Нам понадобится родительский класс. Но … Но это все.

Тьфу … Подожди … Похоже, нам тоже понадобится родитель родителей … А потом … Нам понадобятся ВСЕ родители. Хорошо … Хорошо … Я справлюсь с этим. Нет проблем.

Замечательно. Теперь это не компилируется. Почему?? О, я вижу … Этот объект содержит этот другой объект. Так что мне это тоже нужно. Нет проблем.

Подожди … Мне не нужен этот объект. Мне нужен родитель объекта и родитель его родителя и т.д. И т.д. Для каждого содержащегося объекта и ВСЕХ родителей того, что они содержат вместе с родителями, родителями, родителями…

Тьфу.

Есть замечательная цитата Джо Армстронга, создателя Erlang:

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

Banana Monkey Jungle Solution

Решение проблемы обезьяны. Я могу решить эту проблему, не создавая слишком глубоких иерархий. Но если наследование является ключом к повторному использованию, то любые ограничения, которые я налагаю на этот механизм, наверняка ограничат преимущества повторного использования. Правильно?

Правильно.

Так что же делать бедному объектно-ориентированному программисту, которому помогала Kool?

Содержать и делегировать. Подробнее об этом позже.

The Diamond Problem

Алмазная проблема.

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

-5

Большинство ОО-языков не поддерживают это, кажется, это логично. Что сложного в поддержке этого на ОО-языках?

Хорошо, представьте следующий псевдокод:

-6

Обратите внимание, что и класс Scanner, и класс Printer реализуют функцию, называемую start.

Так какую функцию запуска наследует класс Copier? Только сканера? Только принтера? Это не может быть и то и другое.

The Diamond Solution

Алмазное решение. Решение простое. Не делайте так.

Да это правильно. Большинство ОО-языков не позволят вам сделать подобное.

Но, но … что если мне нужно смоделировать? Я хочу свое Повторное использование!

Тогда вы должны содержать и делегировать.

-7

Обратите внимание, что класс Copier теперь содержит экземпляр Printerи Scanner. Он делегирует функцию запуска реализации класса Printer. Он также может быть легко передан в Scanner.

Эта проблема — ещё одна трещина в колонне Наследования.

The Fragile Base Class Problem

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

И все было правильно. До некоторых пор…

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

Ну, может быть, это ошибка … Но подождите … Что-то изменилось …

Но этого не было в моем коде. Оказывается, изменение было в классе, от которого я унаследовал.

Как изменение в базовом классе может нарушить мой код??

Вот как …

Представьте себе следующий базовый класс (он написан на Java, но его легко понять, если вы не знаете Java):

-8

ВАЖНО: обратите внимание на закомментированную строку кода. Эта линия изменится позже, что и испортит всё.

Этот класс имеет 2 функции на своем интерфейсе, add() и addAll(). Функция add() добавит один элемент, а addAll() добавит несколько элементов, вызвав функцию add.

А вот класс Derived:

-9

Класс ArrayCount является специализацией общего класса Array. Единственное поведенческое различие заключается в том, что ArrayCount ведет подсчет количества элементов.

Давайте рассмотрим оба этих класса подробнее.

Array add() добавляет элемент в локальный ArrayList.
Array addAll() вызывает локальное добавление ArrayList для каждого элемента.

ArrayCount add() вызывает метод add() своего родителя, а затем увеличивает счёт.
ArrayCount addAll() вызывает метод addAll() своего родителя, а затем увеличивает счёт на количество элементов.

И все работает отлично.

Но вот переломный момент. Закомментированная строка кода в базовом классе изменяется на следующее:

-10

Что касается владельца базового класса, он все еще функционирует так, как объявлено. И все автоматизированные тесты все еще проходят.

Но владелец не обращает внимания на класс Derived. А владелец класса Derived ожидает грубого пробуждения.

Теперь ArrayCount addAll() вызывает метод addAll() своего родителя, который внутренне вызывает метод add(), который был ПЕРЕОПРЕДЕЛЕН классом Derived.

Это приводит к тому, что счетчик увеличивается каждый раз, когда вызывается метод add() класса Derived, а затем увеличиваетсяСНОВА на количество элементов, добавленных в addAll() класса Derived.

ОН СЧИТАЕТСЯ ДВАЖДЫ.

Если это может произойти, а это происходит, то автор класса Derived должен ЗНАТЬ, как реализован базовый класс. И они должны быть проинформированы о каждом изменении в базовом классе, поскольку это может привести к непредсказуемым последствиям для их производного класса.

Тьфу! Эта огромная трещина навсегда угрожает стабильности драгоценного столба наследства.

The Fragile Base Class Solution

Хрупкое решение для базового класса. Снова сдерживание и делегирование в помощь.

Используя Contain и Delegate, мы переходим от программирования белого ящика к программированию черного ящика. При программировании Белого ящика мы должны смотреть на реализацию базового класса.

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

Эта тенденция тревожит …

Наследование должно было стать огромной победой для Повторного использования.

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

Если вы похожи на меня, вы начинаете задумываться об этом наследовании. Но что еще более важно, это должно поколебать вашу уверенность в силе классификации через иерархии.

The Hierarchy Problem

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

Должен ли я создать папку с именем Documents, а затем создать папку с именем Company в ней?

Или я могу создать папку с именем Company, а затем создать папку с именем Documents в этой папке?

Оба работают. Но что правильно? Какой лучше?

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

Но если родитель и ребенок могут произвольно поменяться местами, то явно что-то не так с этой моделью.

The Hierarchy Solution

Решение иерархии.

Что-то не так …

Категориальные иерархии не работают.

Так для чего нужны иерархии?

Сдерживание.

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

То, что вы не найдете, это категориальные иерархии. Давайте погрузимся на мгновение. Объектно-ориентированная парадигма была основана на реальном мире, наполненном объектами. Но тогда он использует сломанную модель, а именно — Категориальные Иерархии, где нет реальной аналогии.

Но реальный мир наполнен Иерархиями Содержания. Отличный пример Иерархии Защиты — ваши носки. Они находятся в ящике для носков, который содержится в одном ящике в вашем комоде, который содержится в вашей спальне, которая содержится в вашем доме и т.д.

Каталоги на вашем жестком диске являются еще одним примером иерархии содержания. Они содержат файлы.

Так как же тогда классифицировать?

Что ж, если вы думаете о документах компании, это не имеет значения, куда я их положу. Я могу поместить их в папку «Документы» или в папку «Материал».

Я классифицирую это с помощью тегов. Я помечаю файл следующими тегами:

1. Документ
2. Компания
3. Руководство

У тегов нет порядка или иерархии. (Это тоже решает проблему с алмазами.)

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

Но с таким количеством трещин похоже, что столб Наследственности упал.

Прощай, наследственность.

Encapsulation, the Second Pillar to Fall

-11

Инкапсуляция, второй столп падения. На первый взгляд, инкапсуляция является вторым по величине преимуществом объектно-ориентированного программирования.

Переменные состояния объекта защищены от внешнего доступа, т.е. они инкапсулированы в объекте.

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

Инкапсуляция безопасна для ваших переменных.

В этом инкапсуляция НЕВЕРОЯТНА!

Да здравствует инкапсуляция …

Но это пока…

The Reference Problem

Справочная задача. Ради эффективности Объекты передаются в функции НЕ по их значению, а по ссылке.

Это означает, что функции не передадут объект, а передадут ссылкуили указатель на объект.

Если Объект передается по ссылке на Конструктор Объекта, конструктор может поместить ссылку на этот объект в закрытую переменную, которая защищена с помощью инкапсуляции.

Но переданный Объект НЕ безопасен!

Почему бы и нет? Потому что какой-то другой кусок кода имеет указатель на объект, а именно код, который называется конструктор. Он ДОЛЖЕН иметь ссылку на объект, иначе он не может передать его конструктору?

The Reference Solution

Эталонное решение. Конструктор должен будет клонировать переданный объект. И не мелкий клон, а глубокий клон, т.е. Каждый объект, содержащийся в переданном объекте Object, и каждый объект в этих объектах и ​​т.д. И т.д.

Так много для эффективности.

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

И КАЖДЫЙ язык OO имеет эту проблему.

Прощай, Инкапсуляция.

Polymorphism, the Third Pillar to Fall

-12

Полиморфизм, третий столп падения. Полиморфизм был рыжеволосым пасынком Объектно-ориентированной Троицы.

Это своего рода Ларри Файн из группы.

Куда бы они ни пошли, он был там, но он был просто второстепенным персонажем.

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

Интерфейсы дадут вам это. И без всего багажа ОО.

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

Так что без лишних слов, мы прощаемся с ОО Полиморфизмом и приветствуем основанный на интерфейсе Полиморфизм.

Broken Promises

-13

Нарушенные обещания. Ну, ОО, конечно, много обещал в первые дни. И эти обещания все ещё даются наивным программистам, которые сидят в классах, читают блоги и посещают онлайн-курсы.

Мне потребовались годы, чтобы понять, как ОО лгал мне. Я тоже был с широко раскрытыми глазами, неопытным и доверчивым.

И я обжёгся.

Прощай, объектно-ориентированное программирование.

So then what?

И что тогда? Здравствуйте, Функциональное Программирование. Было очень приятно работать с тобой последние несколько лет.

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

Одного раза обжечься хватит.

Translated: medium