Почему я против объектов. Часть первая, философская
UPD: вторая часть.
LT;DR: это очень личный взгляд на ООП, который я обдумывал довольно долго, но окончательно сформировал после прочтения книги Егора “Elegant Objects”. Если коротко, я высказываюсь против идеи представления кода в объектной модели. Некоторые аргументы почерпнуты из сторонних публикаций и адаптированы для краткости. В таких случаях я даю ссылку на оригинал.
Изначально я планировал написать два в одном: и про ментальную, и про техническую составляющие ООП, но так как текст получается объемный, опубликую пока что первую часть о том, как ООП ложится на (мой) мозг.
Объекты, как утверждают Википедия и учебники, помогают отобразить картину физического мира в коде. Думаю, всем это объяснял преподаватель в школе или университете на примере класса кота или собаки. Потом примеры с наследованием, переопределением класса “голос” и так далее.
За последние годы мое увлечение ООП плавно сошло на нет. В одном из постов я прямо признавался, что не понимаю его принципов. Главное, что меня смущает: во время работы, если это был не строго объектный язык, а Питон, JS или PHP, то все задачи я решал простыми функциями. Каждый раз мне говорили, что просто проект легкий, что однажды наступит БОЛЬШОЙ ПРОЕКТ, где с функциями ты хлебнешь. Но время шло, БОЛЬШОЙ ПРОЕКТ так и не наступил, и, кажется, в эпоху микро-сервисов его уже не дождешься. А я все пишу на функциях и неизменяемых коллекциях. В чем же дело?
И любой объектный код я переписывал на функциях с сокращением числа строк до двух раз. Как так?
Причина мне видится в том, что ООП очень абстрактно. Одно и то же событие реального мира можно интерпретировать в объектной модели совершенно по-разному. Отсюда и трудности изучения, и лишний код.
Представим, что Вася отправляет письмо Пете. На языке объектов это записывается так (опустим определения классов):
User vasya = new User("Vasya");
User petya = new User("Petya");
Message msg = new Message("Hello!");
vasya.send(msg, petya);
Тут, возможно, я нарушил один из бесчисленных паттернов ООП: отвечать за отправку должен не пользователь, а само сообщение (что весьма спорно). Поэтому следующий после меня разработчик поправит код вот так:
msg.send(from=vasya, to=petya)
Это уже не синтаксис Джавы, в ней нет именованных аргументов, а скорее
Питона. Написал так, чтоб было понятно: метод .send()
принимает отправителя и
получателя и сам отправляет письмо.
Уже на этом этапе видна вся неоднозначность и зыбкость объектного мышления. Согласно паттернам, отправку письма логичней вынести в класс сообщения. С другой стороны, в реальном мире именно пользователь инициирует отправку. Письмо это неодушевленная сущность, набор байт или листок бумаги. Как он может что-то отправлять?
Теперь представим, что на каждый чих мы проектируем класс и создаем объект. Как можно утверждать, что отношения между ними логически верны? Как это доказать?
Зыбкость этого принципа порождает все новые и новые книги и статьи в блогах, где авторы доказывают, что именно их подход обеспечивает прозрачность и поддерживаемость программы, написанной в объектном стиле. Но на более приземленном уровне это выражается, простите, в срачи – бурные обсуждения в чатах, когда эмоции и мнения намного опережают опыт.
Проблему отношений между объектами я называю “кто на ком стоял”. В самом деле,
ручка пишет по бумаге pen.writeOn(paper)
или бумага с помощью ручки
paper.write(pen)
? Каждый, кто поспешит ответом в духе “ну конечно первый
(второй) вариант”, не учитывает, что это совершенно субъективно.
Предполагаю, что именно поэтому в ООП-среде так популярен рефакторинг. Ключевая фича каждой промышленной IDE – облегчение рефакторинга: переименование и перенос методов, автоматическая адаптация кода.
В окружающем нас реальном мире физические объекты играют далеко не решающую роль. Иными словами, не все можно выразить через объекты. Рассмотрим, например, акт рукопожатия. Следуя принципам Егора, отразить в коде это можно так:
class User {
private String name
User(String name) {
this.name = name;
}
}
class Handshake {
User user1;
User user2;
Handshare(User user1, User user2) {
this.user1 = user1;
this.user2 = user2;
}
void shake() {
// ... лог в консоль или что угодно
}
}
User user1 = new User("Ivan");
User user2 = new User("Petr");
Handshake hs = new Handshake(user1, user2);
hs.shake();
Но ведь физически рукопожатия не существует. Нет в нашем мире такого объекта, его нельзя купить, смастерить, поставить на полку. Это событие, акт или, выражаясь точнее, действие! А что в программировании выражает действие? Функция.
Предположим теперь, что друзья пожали руки несколько раз с интервалом в 10
минут. Будет ли правильным вызывать у того же объекта метод .shake()
? Или на
каждый раз создавать новый объект?
Если первое, не противоречит ли это принципам ООП? Ведь все это разные рукопожатия, совершенно независящие друг от друга. Что если они жали руки с перерывом в 10 лет?
Если второе, то чем это отличается от вызова функции? Мы создаем объект, вызываем единственный метод и тут же забываем его? Зачем тогда класс и объект? Если один-два метода это все, что нам нужно от класса, не проще ли завести функцию?
На эту тему уже неписана отличная статья “перестаньте писать классы”, изучите обязательно. Видео со слайдами, перевод на Хабре.
Далее можно рассуждать о более сложных вещах. Рукопожатие все же имеет косвенное отношение к физическим объектам. Но такие абстрактные понятия как ненависть, симпатия, эгоизм, честность, религия выразить объектно невозможно. Я имею в виду, что не составит труда написать класс с нужным именем, который инкапсулирует другие классы. Но спрашивается, в чем здесь смысл?
Добавьте процессы: горение, магнитные волны, свет. Все, что перечислено выше имеет значение только в действии. Важен не сам объект, а как он изменяет другие сущности. Опять же, про написана хорошая статья, где рассматривается река с точки зрения ООП- и ФП-программиста.
В книге “Elegant Objects” Егор справедливо замечает, что сегодняшнее ООП это
просто структура данных с прикрепленными к ней функциями. Однако, не делается
акцент на том, сколь масштабно это явление. С выходом языков Go и Rust на них
перешли тысячи бывших Java и С++ разработчиков, и, похоже, сочли новые языки
вполне себе объектными. А ведь и в Go и в Rust объекты – это банальные сишные
структуры. И если функция принимает первым аргументом такую структуру, то вместо
some_action(data, 1, "test")
можно написать data.some_action(1, "test")
. Вот
и вся разница.
Очевидно, сегодня для большинства вызов функции через точку кажется главным показателем объектности языка. Я не хочу никого этим обидеть. Человеку свойственно упрощать рутину: выражать код всей программы с помощью настоящих, “элегантных” объектов, как советует Егор, мне кажется неподъемным делом. А со структурам и функциями проще, и даже похоже на ООП.
Я утверждаю, что современный ООП-подход, согласно которому мы выражаем все сущности как объекты, так же похож на окружающий мир, как шахматы на реальную войну. То есть в кукольной манере и гротескным упрощением. Тысячи лет назад войска действительно выстраивались напротив противника, впереди пешие войска, по бокам боевые слоны, сзади командование. Сегодня это не так.
Кратко резюмируя. В ООП до сих пор нет четкого понимания, как выстраивать отношения между объектами. Огромное число паттернов сбивает с толку. Объекты в коде не могут точно описать нематериальные понятия: природные процессы, отношения между людьми. Гораздо большее значение имеет не объект, а действие над ним.
В следующий раз поговорим о технической стороне ООП.
Нашли ошибку? Выделите мышкой и нажмите Ctrl/⌘+Enter
Vyacheslav Ermolaev, 10th Jan 2018, link
Вы просто смоделировали неправильно бизнес-процесс пересылки. При этом нарушили один из принципов SOLID , а именно single responsibility. Люди в реальном мире выполняют пересылку почты друг другу. Этим занимает почтовая служба, которой для успешного выполнения нужно лишь письмо. Для того, чтобы письмо было доставлено оно должно содержать не только текст сообщения, но информацию об отправителя и получателе.
Тоже самое с ручкой. Умение писать принадлежит только ручке. Можно, конечно, сказать, что эта бумага была исписана ручкой, но страдательный залог - это грамматический термин, а не термин ООП. Боюсь,что при реальном опросе все те, кто реально занимает программированием вариант paper.write(pen) просто не назовут. Методы в ООП это не аналоги сказуемого в грамматике и модели строятся на реальных бизнес-процессах да к тому же очень часто доменную модель предметной области создают не программисты, а аналитики, которые в этой предметной области собаку съели
Михаил Попов, 22nd Feb 2020, link
“Elegant Objects” уже для понимающих разницу между компьютерным и человеческим мышлениеми. Меня прорубило видео Егора ООП враньё.
Как раз я упутался в частном проекте - погружение в задачу занимало каждый раз по часу.
Удовольствие от программирования ушло. Поиск привёл к Егору. Профит = программирую, хоть и по сермяжному, а с удовольствием.
Видео про враньё основано на книге Давида Веста - там написано примерно так - думать как компьютер было сначало полезно, при усложнении задачи компьютер Вас "обует", поэтому выгоднее в сложных задачах думать по человечески, а человек решает сложные задачи артелью, коллективом, отрядом ...
А решать с помощью ООП, то что решается процедурно ... как стричь поросёнка - визгу много, шерсти мало... как заставлять льва ловить мышь - бесперспективно и опасно :-).
От себя добавлю - слово "объект" русскоязычным не подходит. Нам подходит "исполнитель". И "класс" не подходит, нам нужна "матка", она делает исполнителей, которые получают сообщение, что-то минимально нужное делают, передают сообщение.
И компьютер позволяет нам этих исполнителей использовать снова и снова.
Управление толпами исполнителей - погуглите по "ООП разрушить собор построить базар"
Кто не терял кайф программирования, тот не поймёт нашего Егора-"стрельца" :-)
Ivan Grishaev, 22nd Feb 2020, link , parent
Надо просто переболеть Егором и двигаться дальше.
dikun, 12th Oct 2021, link
Если юзер и сообщение не могут поделить send, то это намекает на то, что send по справедливости не должен принадлежать кому-то одному (чтоб не ссорились), но должен дружить и с юзером, и сообщением, т.е.
send(from=vasya, to=petya, what=msg)
при этом send - должен иметь доступ к приватным методам и полям юзера и сообщения (друзья должны знать сокровенное о своих друзьях). Что-то похожее есть в С++. Так и называется - друзья. Но на сколько я помню, друзья в C++ должны всё равно какому-то классу принадлежать. А это плохо.
dikun, 12th Oct 2021, link , parent
Если это ДжаваСкрипт, то send-функция у нас ещё и объект, который можно нашпиговать, например, полями:
send.amotion = new Amotion('angry')
send(from=vasya, to=petya, what=msg)
Andrei Dzikun, 16th Oct 2023, link
Как вариант, делается сервис (псевдокод):
IMessenger messenger = Messenger.Create(); messenger.Send(Message message, User from, User to);
Мессенджер - вполне интуитивно понятный объект.
И, например, юзеры тоже могут создаваться сервисом. В этом случае - это своего рода объект, связующий среду и пользователя. Он, кстати, в реальности может быть даже каким-то сервисом под виндой, который менеджит пользователей.
userService.CreateUser(Environment environment, Role[] roles, String name, …); // or userService.Create(…)
Т.е. это решение - чтобы думать меньше, у кого искать нужный метод. И мы сразу видим, что у нас за юзеры в среде и с какими параметрами. Потому что юзаем один сервис для создания логической группы юзеров (например юзеров для авто-тестов определённой юзер-стори).