Повсеместное торжество объектно-ориентированного подхода - это реальность, данная нам в ощущениях.
На мой взгляд, это очевидно и неоспоримо. Подавляющая часть прикладной разработки ведётся на объектно-ориентированных языках и в объектном стиле. Рекрутеры ищут три заветные буквы в резюме[1]. Студентов программерских факультетов натаскивают на святую троицу "наследование-инкапсуляция-полиморфизм",
Короче, всё объектно-ориентированное по умолчанию считается хорошим, а не-объектно-ориентированное - плохим и устаревшим. Сей консенсус плотно укоренён в головах большинства участников игры: от вяльяжного CEO на презентациях до щуплого кодера где-то на просторах бангалорщины. Слава богу, хотя бы ранняя восторжённая риторика на тему "объектный подход разумен и верен, потому что он отражает устройство материального мира" уже практически сгинула. Всем очевидно, что будь мир устроен аналогично объектно-ориентированным средам, письменность, например, никогда бы не была изобретена: человечество ещё на ранних этапах погрязло бы в бесконечных священных войнах, выясняя, как же всё-таки правильно: "ручка.писать(бумажка, текст)" или "бумажка.написать(текст, ручка)"[2].
Тогда с чего я вообще завёл разговор о каком-то "кризисе"? А вот как раз ещё раз ухвачусь за пример с письменностью и акцентирую внимание на следующем: в объектном лагере согласия нет. Видны тёрки и непонимания между сторонниками различных
Видовое разнообразие
Вопрос механизмов наследования - он вообще довольно больной. Даже между такими флагманами, как C++ и Java, нету согласия по вопросу множественного наследования и интерфейсов. Масла в огонь подлили дженерики, со своими запутанными правилами ковариантной/контравариантной совместимости. Недавно появившийся и успевший нашуметь гугловский Go напугал традиционалистов отказом от наследования вообще. На периферии маячат упомянутая выше прототипная модель, миксины, экстеншн-методы, multiple dispatch и прочая чертовщина. Как соблюсти чистоту канона (и где вообще этот канон) - непонятно совершенно.
Ещё один, недавно возникший раскол - противостояние "жирной" (rich) и "тощей" (anemic) моделей. В последнее время анемичная модель действительно стала набирать обороты. Казалось бы, ну и фиг - какая разница, это ведь всего лишь вариация на ту же самую объектную тему?.. Сейчас я попробую объяснить, почему мне это кажется серьёзной идеологической бомбой под фундаментом "классического" ООП.
Вторичность половых признаков
Начнём по порядку. Наследование, как выяснилось, бывает очень разное и вообще необязательно. Его доволно легко имитировать. Смотрите сами, код:
class MyBase { public MyBase(int param) { // do constructor } public void MyMethod(String param) { // do something; } } class MyChild extends MyBase { public MyChild(param) { // do constructor 2 } // other methods }
по сути оказывается почти эквивалентен[3] коду следующему
interface MyBase { public void MyMethod(String param); } class MyParent implements MyBase { public MyParent(int param) { // do constructor } public void MyMethod(String param) { // do something; } } class MyChild implements MyBase { MyParent _base; public MyChild(param) { MyParent _base = new MyParent(param); // do constructor 2 } public void MyMethod(String param) { return _base.MyMethod(param); } // other methods }
Т.е. наследование класса ("is-a") по сути есть комбинация реализации ("is-a") интерфейса и включения ("has-a") родительского объекта, которому "делегируется" обработка не-переопределённых методов. Как бы синтаксический сахар для такой комбинации.
Собственно, наследование, по факту, примерно так и реализуется C++/Java, если предствавить, что _base является не ссылкой, а типом-значением, а вместо пробрасывающих ("делегирующих") обёрток для MyMethod работает виртуальная таблица методов. (А в Tcl/Snit оно вообще реализуется В ТОЧНОСТИ так.)
Поскольку "интерфейс" суть проявление полиморфизма - мы таки свели наследование к полиморфизму. Он, вообще говоря, тоже иногда существует вполне себе отдельно от ООП (см. полиморфные операторы в раннем Perl), но его пока отложим на потом.
С последним элементом, инкапсуляцией всё гораздо проще. Она попросту необязательна как таковая. Это продемонстрировал Python и другие скриптовые языки: отсутствие ограничений на доступ к полям и методам вовсе не приводит к катастрофам, ибо если разработчик таки добрался до недокументированного метода - скорее всего, он уже разобрался в коде и понимает, что он делает.
Сорвав лишнюю терминологическую шелуху, поглядим на объект и вспомним, что он всё-таки из себя представляет: это всего лишь объединение данных и кода для работы с ними.
На все руки мастер
С одной стороны, казалось бы, это чисто терминологический вопрос. Ну слили структуру и модуль вместе, получили более универсальную единицу организации кода - вроде нормально.
С другой стороны, выяснилось, что даже после такого слияния классы стремятся расползтись в разные стороны, согласно первоначальным предназначениям: возникают "классы-модули", напичканные огромным количеством методов, в т.ч. статических, и склонные к злоупотреблению паттерном Singleton; и "классы-кортежи", содержащие в себе только поля и набор тупых геттеров-сеттеров[5]. Всё это называется "анемичная модель", о которой уже упоминалось выше.
Таким образом, ООП, стремясь заменить собой структурно-процедурное программирование, в конечном итоге вернулось практически к нему же, но только в модных обёртках. Возникает некоторый вопрос: а был ли вообще смысл в манёвре?..
Ещё раз о полиморфизме
Чтобы понять, в чём заключается особая магия полиморфизма, подумаем (как и в случае с наследованием), как бы мы его могли имитировать при помощи подручных средств.
Итак, рассмотрим следующий пример:
interface List { public int getLength(); } class ArrayList { public int getLength() { // blah-blah вернуть хранимую длину } // blah-blah длина массива и ссылка на данные } class LinkedList { public int getLength() { // blah-blah побежать по узлам и сосчитать } // blah-blah ссылка на первый узел }
struct { int typeCode; void* object; } List; const TYPE_ArrayList = 0; const TYPE_LinkedList = 1; int ArrayList_getLength(ArrayList* this) { // blah-blah вернуть хранимую длину } int LinkedList_getLength(LinkedList* this) { // blah-blah побежать по узлам и сосчитать } int List_getLength(List this) { if (this.typeCode==TYPE_ArrayList) return ArrayList_getLength((ArrayList*)this.object); if (this.typeCode==TYPE_LinkedList) return LinkedList_getLength((LinkedList*)this.object); // blah-blah ошибка типа }
int List_getLength(List this) { switch (this.typeCode) { case TYPE_ArrayList: return ArrayList_getLength((ArrayList*)this.object); case TYPE_LinkedList: return LinkedList_getLength((LinkedList*)this.object); } // blah-blah ошибка типа }
(int(void*))[] VTABLE_getLength = {ArrayList_getLength, LinkedList_getLength}; int List_getLength(List this) { VTABLE_getLength[this.typeCode](this.object); }
Если мы выстроим все возможные способы реализации полиморфизма в ряд, по возрастанию их "удобства", получим: if < switch/case < таблица < ООП.
Теперь поразмыслим: какое качество возрастает в этом ряду слева направо? Ответ прост: декларативность. Т.е. отсутствие необходимости делать все проверки и переходы руками. switch/case избавляет от повторения названия проверяемой переменной, таблица избавляет от явного повторения вызова, а поддержка полиморфизма на уровне языка избавляет от необходимости явно указывать таблицу.
Декларативность сокращает и упрощает код. Вместо того, чтобы делать все мелкие операции руками, мы всего лишь задаём набор правил, которые потом обрабатывает некая хитрая подсистема. Меньше кода, меньше повторений, меньше шансов на ошибку, легче поддержка.
Конечно, как показывает пример SQL или чисто-функциональных языков[6], декларативность накладывает свои ограничения: вы не можете контролировать вычисления напрямую и вынуждены прибегать к фокусам для реализации некоторых достаточно типовых, но "непредусмотренных" данным декларативным подходом вещей. Но, как правило, декларативность всё-таки представляет собой благо.
Выводы
Что-то я написал, написал, пора и закругляться. Суммируем поток сознания:
- наследование и инкапсуляция не являются обязательными и определяющими для ООП;
- ООП характеризуется объединением данных и кода и полиморфизмом;
- модульность - это полезно;
- классы заменили собой кортежи и модули, но постепенно превращаются либо в одни, либо в другие;
- таким образом, основное отличие ООП от процедурно-структурного - наличие полиморфизма;
- полиморфизм есть частный случай декларативного программирования;
- декларативное программирование нам строить и жить помогает.
Финальный вывод из всего этого следует такой:
(Следует признать, что, действительно, организационные вопросы обычно влияют на скорость и качество разработки гораздо больше технических - но мы сейчас всё-таки о последних).
1 ^ Личный опыт общения по телефону: "так, а с ООП вы хорошо знакомы?" - "ну вот же, в резюме, два года опыта на C#, это ведь объектный язык, там просто по-другому нельзя!" - "ну вобщем-то верно, но... всё-таки, знаете ли вы ООП?!"
2 ^Было бы ещё радикальное движение, продвигающее подход "текст.написаться(бумажка, ручка)", но оно было бы заклеймлено опасной ересью сразу.
3 ^Интересные эффекты происходят, если MyMethod в базовом классе вызывает какие-то другие виртуальные методы объекта, которые в потомке переопределены; но это, вообще, довольно нездоровый способ организации кода и вместо него лучше прибегать к более явному Dependecy Injection.
4 ^Пакэджи в жабе в конечном итоге сводятся к классам, а неймсмпейсы в пхп появились только совсем недавно.
5 ^Наличие в Java прямого доступа к полям объекта и отсутствие properties - это вообще цырк какой-то. Они вообще высокоуровневый язык делали или где?!
6 ^Функциональное программирование является частным случаем декларативного, я настаиваю!