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

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

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

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

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

class Summator:
    a = None
    b = None

    def set_a(self, a):
        self.a = a

    def set_b(self, b):
        self.b = b

    def get_sum(self):
        return self.a + self.b

sum = Summator()
sum.set_a(2)
sum.set_b(3)
print sum.get_sum()

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

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

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

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

def sum(a, b):
    return a + b

лишена перечисленных недостатков. В ней нет состояния: два значения однозначно преобразуются в одно, поэтому функцию можно выполнять в тредах. К ней можно применить декоратор memoize, чтобы отдавать результат из оперативной памяти.

Нас учат, что краеугольные камни ООП – инкапсуляция, наследование и полиморфизм. Все это есть в современных языках без привязки к ООП. Инкапсуляция – замыкание или словарь, где ключи хранят не только данные, но и функции для работы с ними. Весь Джаваскрипт так работает, еще Луа. В Кложе есть расширение протоколов: разработчик может добавить в существующий протокол свой тип данных. В Гоу разработчики наследуют структуры. Полиморфизм встроен в любой функциональный язык – Хаскел, Эрланг, Кложу.

Говорят, ООП оперирует объектами, поэтому лучше отражает окружающий мир. Это видно в примерах, когда от класса Animal наследуют классы Cat и Dog. Потом заменяют метод голоса (мяу и гав) и т.д. Проблема в том, что мир вокруг нас неописуемо сложен. Природе потребовались миллионы лет, чтобы обуздать хаос и породить первый белковый комочек. Действительно считаете, что ООП может описать сложность окружающего мира?

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

Объекты скрывают явное. Предположим, вызываю метод .get_data() у класса Foo. Знаю, что он унаследован от Bar, а тот от Baz. Как понять, из какого класса будет вызван метод? Был ли он переопределен Foo или Bar? Наоборот, когда вызываю функцию get_data() из модуля foo, ошибки быть не может – это именно та функция, не одноименная из модуля bar.

Любопытно, что в природе нет наследования, какое видим в ООП. Набор генов формируется с учетом генов родителей, но затем особь становится полностью автономной и не зависит от предков. Вы видели, чтобы детеныш терял лапу от того, что папаша-самец попал в капкан? Или если мать сменит пол, то и ребенок тоже? Это же бред. В ООП происходит подобное – потомки прибиты гвоздями к предкам. При изменении цепочки где-то в середине изменения передаются по ниспадающей. В результате все ломается.

Два класса, унаследованные от предка, обречены на страдания сиамских близнецов. Ни один не может претендовать на независимость. Согласно медицине, разделение сиамских близнецов – невероятно трудная задача, во всем мире считаные врачи делают эти операции. Разделение классов с наследниками протекает мучительно. Напротив, объединить функции в класс очень легко. Функция автономна по своей природе. Ей неважно, где находиться – на уровне модуля или класса. В следующий раз перед тем, как объявлять класс, начните с небольшого набора отдельных функций. Возможно, их окажется достаточно.

Глобальный объект, на который ссылаемся через переменные self, this – ни что иное как читерство. Еще в школе рассказывают, что глобальные переменные – плохо и использовать лучше только для чтения. В ООП этот моветон гипертрофирован и возведен культ. Self – жирная глобальная переменная, методы меняют ее поля. Например, на уровне класса объявлено поле data = None. В каком-то методе автор невозбранно итерируется по нему как списку. Подразумевается, что перед этим будет вызван метод set_data, который заполнит поле data. Теперь представьте, что в объекте self двадцать полей. Как это контролировать?

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

– Все современные языки исполнены в ООП, какой ни возьми.

Неправда. Недавно появились Раст, Гоу, Кложа, Ним, где классов либо нет, либо они не являются центральной концепцией языка. Создатели поняли, что грамотные коллекции и структуры важней классов. В Гоу и Ним вызовы методов ни что иное, как синтаксический сахар. В Кложе классов и объектов нет, в Лиспах ООП строится поверх языка. В Тикле (TclTk) ООП-систем вообще пять.

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

Неправда. Вакансии на Кложе, Хаскеле есть. Гуглите на Стековерфлоу. Для Скалы еще больше – проходите собеседование и работайте. К слову, обеспокоенность трудоустройством выдает нужду и играет на руку работодателю. Заказчик не заинтересован в вашем росте. Он хочет принять программиста за 50 тыс. руб. в месяц и держать на одном уровне много лет. Он горячо поддержит вас в том, что ООП – стабильность и уверенность, а ФП – удел академиков НИИ.

– Все пишут на ООП, потому что альтернатив нет.

Программированию всего 50-60 лет. Это меньше одной человеческой жизни, и не идет ни в какое сравнение с медициной или астрономией. В программировании все еще очень мало точек, на которые можно опираться как на непреложные истины подобно аксиомам в геометрии. Поэтому в исторических масштабах то, что видится трендом, окажется зигзагом кривой. Мыслите шире.

– Ты писал на функциональном языке? Или только рассуждаешь?

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

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

Комментарии из старого блога

10/06/15 Корейша Виктор: Очень круто. У меня подобные мысли пару лет крутятся в голове. Но нет такого опыта на разных языках, что бы отвечать на выпады. Спасибо.

10/06/15 Иван Гришаев: Джо Армстронг, создатель Эрганга, высказывается в похожем ключе: http://sotnyk.com/2012/05/19/pochemu-oop-polnyiy-otstoy/

10/08/15 Павел Заикин: Каждому методу свои задачи. В чем-то хорош ООП в чем-то функциональный подход.

Не бывает идеальных методов.

10/08/15 Иван Гришаев: Осталось понять, кому какие =)

10/08/15 Павел Заикин: ООП хорош там где нужно хранить состояния. Там где много однотипных сущностей с немножко разным подходом к выполнению алгоритмов. Там где нужно сделать часть данных недоступным для изменения из других частей программы. Да много где ООП хорош. Но ФП тоже хорош особенно для простых, линейных задач.