ООП умерло? Да здравствует ООП!
Адепты функционального программирования, кажется, вы рано сбросили ООП со счетов. А не зря ли вы вообще на него ополчились?
william krause / unsplash
Рея Мутафис
(Rhea Moutafis)
об авторе
Стартапер со степенью доктора философии Сорбонны и MBA от CDI. Пишет для TheNextWeb, HP Enterprise и Built In.
На заре программирования, в 1960-х годах, компьютеры были довольно слабыми. Поэтому требовалось как-то делить их вычислительные ресурсы между данными и задачами.
Проблема заключалась в том, что тогда нельзя было обработать большой объём данных, не загрузив компьютер до предела его возможностей. А если нужно было решать много разных задач, то каждая могла работать лишь с данными небольшого объёма, иначе компьютер считал бы вечно.
И вот в середине шестидесятых Алан Кэй предложил идею автономных мини-компьютеров, которые бы обменивались не данными, а сообщениями. Так вычислительные мощности можно было бы делить гораздо эффективнее.
от переводчика
Будучи по образованию математиком и молекулярным биологом, Алан Кэй сравнил компьютеры с живыми клетками. Идея состояла в том, чтобы независимые программы (ячейки) отправляли друг другу сообщения. При этом состояние программ не открывалось бы внешней среде (инкапсуляция).
Вот как Кэй вспоминает об этом: «Я считал объекты чем-то вроде биологических клеток и/или отдельных компьютеров в сети, которые могут общаться только через сообщения».
Несмотря на гениальность этой идеи, в объектно-ориентированное программирование она воплотилась не сразу, оно завоевало популярность лишь к 1981 году. С тех пор в ООП неуклонно погружаются как новички, так и бывалые разработчики. И рынок сегодня заполонён ОО-программистами.
ООП правит бал сорок с лишним лет. Однако в последние годы эту парадигму всё чаще критикуют. Возможно ли, что технологии попросту её переросли?
Так ли глупо объединять данные и методы?
Основная идея объектно-ориентированного программирования проста до безобразия: вы разбиваете программу на части, законченные сами по себе. То есть объединяете все данные и методы, которые нужны для обработки только этих данных.
Заметьте, это относится только к понятию инкапсуляции, то есть данные и методы находятся внутри объекта и невидимы снаружи, защищены. С содержимым объекта можно взаимодействовать только через сообщения, так называемые методы доступа (геттеры и сеттеры).
У современного ООП есть и другие важные принципы, которые не входили в изначальную идею. Это наследование и полиморфизм.
Наследование означает, что разработчики могут определять подклассы, которые обладают всеми свойствами, присущими родительскому классу. Эта концепция возникла в 1976 году, спустя десятилетие после появления объектно-ориентированного программирования.
Ещё через десять лет в ООП пришёл полиморфизм. В общих чертах он означает, что метод или объект могут быть шаблонами для других методов и объектов. По сути, это более универсальное понятие, чем наследование, потому что новой сущности можно передавать не все свойства исходного метода или объекта, переопределять их по необходимости.
Особенность полиморфизма состоит в том, что, даже если в исходном коде две сущности зависят друг от друга, вызываемая сущность является скорее шаблоном для фактически используемых объектов. Это облегчает жизнь разработчикам — им не нужно беспокоиться о зависимостях при выполнении программы.
Вообще говоря, наследование и полиморфизм присущи не только объектно-ориентированному программированию. А уникальность ООП — именно в инкапсуляции данных и методов, которые их обрабатывают. Когда вычислительных ресурсов было намного меньше, чем сегодня, эта идея оказалась прорывной.
Пять главных проблем объектно-ориентированного программирования
Став массовым, объектно-ориентированное программирование перевернуло сам подход к написанию кода. До 1980-х годов преобладало процедурное программирование, которое было машинно-ориентированным. Чтобы писать хороший код, нужно было разбираться во внутреннем устройстве компьютера.
ООП же изменило взгляд на разработку ПО, переключило его с машины на человека. Например, в ООП чисто интуитивно понятно, что метод drive() принадлежит к группе данных car, а не к группе teddybear.
Когда появилось наследование, понятности это не убавило: по-прежнему было очевидно, что Hyundai — это подгруппа car, и у них одинаковые свойства, а вот для Hyundai и PoohTheBear — это не так.
Впечатляет? Ещё бы. Но есть и обратная сторона: разработчики, которые знают лишь объектно-ориентированное программирование, воспринимают любую задачу только с позиций ООП. Это всё равно что взять в руки молоток — и с этого момента везде замечать незабитые гвозди. Но если в ящике с инструментами у вас только молоток — это может стать большой проблемой. О чём далее.
Следующие примеры я частично позаимствовал из вирусной истории Чарльза Скалфани (Charles Scalfani) «Прощай, объектно-ориентированное программирование».
Проблема банана, гориллы и джунглей
Предположим, вы пишете программу. В ней понадобился новый класс. Вы сразу вспоминаете, что как раз такой аккуратный маленький класс вы уже создавали в другом проекте, — и он идеально подошёл бы здесь и сейчас.
Нет вопросов! Можно спокойно использовать класс из старого проекта в новом.
Только вот класс этот может оказаться подклассом — а значит, придётся включить в код и его родителя. Родительский же класс тоже окажется зависимым от других классов — и в итоге вы навключаете в программу кучу лишнего кода.
Создатель языка программирования Erlang Джо Армстронг когда-то сказал об этом так:
Проблема объектно-ориентированных языков в том, что они тащат за собой всё своё окружение. Вы хотели всего лишь банан, а получаете в итоге гориллу с бананом и целые джунгли в придачу.
Этим всё сказано.
Использовать классы повторно — это нормально и вообще едва ли не главное достоинство ООП. Но не стоит доводить принцип DRY до крайностей. Иногда лучше написать новый класс, чем тянуть прежний с клубком его зависимостей.
Проблема хрупкого базового класса
Допустим, вы повторно использовали класс из прежнего проекта. Что произойдёт, если его базовый класс изменится?
Испортиться может весь ваш код, даже тот, который никто не менял. Ещё вчера производные классы работали как часы, но сегодня всё сломалось. А просто кто-то внёс незначительное изменение в базовый класс, и это оказалось критичным для целого проекта. Называется такое проблемой хрупкого («незащищённого») базового класса.
Решение использовать класс повторно кажется быстрым и эффективным, но в перспективе может обойтись слишком дорого. Иными словами, чем чаще пользуешься наследованием, тем тяжелее поддерживать работоспособность кода.
Проблема ромбовидного наследования
Наследование — мягкое и пушистое, когда мы берём свойства одного класса и передаём его другим. А если нужно смешать свойства двух разных классов?
Увы, просто и элегантно это сделать не получится.
Например, у нас есть класс Copier. Он описывает копировальный аппарат, который сканирует документ и печатает его на чистом листе.
Вопрос: подклассом чего Copier должен быть — сканера (Scanner) или принтера (Printer)?
Верного ответа просто не существует. Это называется проблемой ромба. Да, код она не ломает, но обескураживает разработчиков часто.
Проблема иерархии
Вопрос выше заключался в том, чей наследник класс Copier. А с ответом я вас обманул: из той ситуации можно выйти изящно. Делаем Copier родительским классом, а Scanner и Printer — подклассами, которые наследуют только подмножество свойств. Вуаля!
Здорово, конечно. Но что, если Copier у нас чёрно-белый, а Printer умеет печатать в цвете? Разве Printer в этом смысле не более общее понятие для Copier? И что делать, если Printer должен подключаться к Wi-Fi, а Copier нет?
Чем больше свойств добавляется в класс, тем сложнее установить правильную иерархию. В этом примере мы имеем дело с наборами свойств Copier и Printer — среди них есть как общие, так и уникальные. Если попытаться выстроить такие классы в линейную иерархию наследования (особенно в крупном и сложном проекте), то это чревато проблемами. Потому что выбрать родителя из Copier, Printer и Scanner не выходит.
Проблема ссылки
Столкнувшись с проблемой иерархии, вы можете решить: окей, займёмся
ОО-программированием в чистом виде — без всяких иерархий. Попросту будем использовать наборы свойств и наследовать их, расширяя или переопределяя когда нужно. Выйдет немного путано, но решение это вполне жизнеспособно, разве нет?
Увы и ах, но нет. Тут возникает иная сложность. Весь смысл инкапсуляции — в том, чтобы обрабатывать данные более эффективно, защищая их друг от друга. А без строгой иерархии это невозможно.
Допустим, объект A должен взаимодействовать с объектом B. Какие у них отношения — значения не имеет, важно лишь, что A не прямой потомок B.
Объект A включает в себя ссылку на B, иначе они не смогли бы взаимодействовать. Но если A содержит данные, доступ к которым также есть у дочерних элементов B, то получается, что эти данные можно изменить сразу из нескольких мест. Таким образом, данные в объекте B больше не защищены — инкапсуляция нарушена.
И хотя многие ОО-разработчики создают продукты именно с такой архитектурой, это уже не ООП, а попросту бардак.
Опасность одной парадигмы
Все названные проблемы ООП объединяет одно: наследование применяется даже там, где не является лучшим решением. Причём я бы не сказал, что это проблемы именно ОО-подхода — потому что исходная его форма никакого наследования не предполагала. Скорее во всех них виновата боязнь выйти за рамки парадигмы, слепая вера в неё.
Однако перестараться можно не только в ООП. Например, в чисто функциональном программировании чрезвычайно сложно обрабатывать ввод данных или вывод сообщений на экран. Для этих целей лучше применять объектно-ориентированный или процедурный подход. Тем не менее некоторые разработчики реализуют ввод-вывод как чистые функции и раздувают свой код до того, что разобраться в нём уже никто не может. Хотя с другим подходом для этого бы хватило пары понятных строк.
В парадигмах, как в религиях, хороша умеренность. Бесспорно, Иисус, Мухаммед и Будда явили миру крутые идеи, но если следовать их учениям до последней запятой, можно изрядно испортить жизнь себе и близким. Во всём важна мера.
Очевидно, что функциональное программирование набирает обороты, тогда как ООП в последние годы сдаёт позиции, его резко критикуют.
Несомненно и то, что изучать новые парадигмы нужно, только вот применять их следует по необходимости. В конце концов, даже если ООП — тот самый молоток, из-за которого разработчики везде видят лишь гвозди, — разве это причина его выкидывать? Не проще ли добавить в рабочий ящик отвёртку, нож и ножницы — и выбирать тот инструмент, который лучше решает конкретную задачу.
Главный вопрос: мы на пороге переворота?
Все жаркие споры о функциональном и объектно-ориентированном программировании привычно сводятся к одному: пришёл ли ООП конец?
Появляется всё больше задач, для которых эффективнее именно функциональный подход: вспомнить хотя бы анализ данных, машинное обучение или параллельное программирование. И чем больше вы этим занимаетесь, тем сильнее цените функциональное программирование.
Но реальность такова, что на десяток вакансий для ОО-разработчиков есть дай бог одно предложение для «функционалов». Хотя это и не значит, что функциональный программист не найдёт работу, — такие специалисты на рынке пока что в дефиците.
Самый вероятный сценарий развития событий, как мне кажется, таков: объектно-ориентированное программирование будет востребовано ещё лет десять. Конечно, за «функционалами» будущее, а если говорить о настоящем, то ОО-разработчики пока что нужнее.
Так что в ближайшие несколько лет не стоит выбрасывать ООП из своего ящика с инструментами. Главное, чтобы оно не лежало там в гордом одиночестве.