Теперь, после того, как мы немного поработали с объектами, давайте попытаемся ответить на вопрос: "какую такую задачу они решают, которую не решают абстракции на основе обычных функций + ассоциативный массив как структура"?
Изложенные ниже тезисы могут показаться вам совсем чуждыми и непонятными в силу отсутствия опыта. В этом нет ничего страшного, главное — увидеть направления, а отработкой мы займёмся в следующих курсах.
Перед тем как начать уходить глубже в тему объектов, хочу вас предостеречь. ООП в современном мире воспринимается большим числом программистов (особенно начинающими), как серебряная пуля, как средство от всех болезней. Учитывая, что в PHP это основной способ строить абстракции, у вас может сложиться такое же впечатление. Но это не так. Во-первых, под ООП понимают две абсолютно разные концепции. Та, которую мы обсуждаем, является мейнстримом, и встроена во многие языки настолько глубоко, что писать в другом стиле либо невозможно, либо очень тяжело. Но есть и другая, созданная Аланом Кеем (Alan Key). Что интересно, именно Алан является создателем термина ООП, но его ООП не имеет почти ничего общего с тем, что сейчас называется ООП.
ООП для меня это сообщения, локальное удержание и защита, скрытие состояния и позднее связывание всего. Это можно сделать в Smalltalk и в LISP. Алан Кей
Во-вторых, существуют другие способы получить поведение, похожее на то, которое вы будете наблюдать в ООП-коде. Более того, многие из них значительно мощнее в возможностях (и некоторые появились задолго до ООП-языков). Например, мультиметоды в языке Clojure дают большую свободу (мультидиспетчеризацию) и позволяют строить полиморфизм на специализированных функциях.
В общем и целом, чем больше разных по структуре языков и парадигм вы знаете, тем лучше понимаете, что происходит. Рекомендую: Clojure, Haskell, Elixir, Kotlin и JavaScript (последний стандарт).
Преимущества
Пожалуй, основное преимущество связано с полиморфизмом подтипов. Подробно эта тема освещается позже. Сейчас лишь скажу, что если мы вызываем функцию, то это всегда некоторая конкретная функция, импортированная из конкретного пространства имён. А вот если мы вызываем метод, то появляются варианты. Когда интерпретатор доходит до кода с вызовом метода
$obj->methodCall()
, он не может сразу сказать, где определён данный метод, потому что ответ на этот вопрос зависит от того, какой тип у$obj
. Отсюда следует, что, если разные объекты содержат методы с одинаковым именем (и сигнатурой), то их можно прозрачно (для вызывающего кода) подменять. На практике такая возможность местами упрощает код (становится меньше условных конструкций), но главное — делает его расширение проще.Работа с абстракцией, основанной на ассоциативном массиве, таит в себе один сюрприз. Так как любая сущность представляется этим массивом, то можно по ошибке вызывать функцию, предназначенную для одной абстракции (например, точки), на другой абстракции (например, отрезка). Что при этом произойдёт — непонятно, и зависит от того, насколько удачно совпали структуры. И если такое произошло, то функция внезапно может отработать без ошибок и даже что-то вернуть. В итоге программа продолжит работать некорректно, вместо того, чтобы завершиться с ошибкой. Инкапсуляция исключает подобную ситуацию. Вызываемый метод всегда принадлежит тому объекту, на котором он вызывается. Если метода нет, то будет ошибка, если есть — то он отработает так, как и должен отработать. Но это преимущество является преимуществом только при сравнении с абстракциями, построенными на общих структурах данных (и в динамических языках), такими, как ассоциативные массивы. В языках с развитой системой типов (но без ООП), например, в Haskell, подобной проблемы также нет.
<?php $segment = makeSegment(makePoint(1, 3), makePoint(10, 11)); // Функция отработает, хотя в неё передали отрезок, а не точку getX($segment); // getX - функция, предназначенная для работы с точками
Это преимущество немного неожиданно. Возможность вызывать методы у объектов открывает дорогу к автокомплиту в редакторах. Да-да! Именно благодаря такому способу вызова редактор может подсказать список методов, которые есть у данного объекта. Если вы сначала пишете функцию, а затем передаёте туда данные, то вы должны знать про существование функции заранее. Но тут нужно оговориться. Вызов функции после данных не является прерогативой ООП. В некоторых современных языках (Nim, D, Rust) поддерживается Unified Function Call, который позволяет проделывать такой же трюк с обычными функциями. Ниже пример на языке Nim.
# Создаётся тип Vector, представляющий из себя кортеж из двух элементов type Vector = tuple[x, y: int] # Определяется функция, принимающая на вход два вектора и возвращающая новый вектор, # полученный сложением исходных векторов proc add(a, b: Vector): Vector = (a.x + b.x, a.y + b.y) let # Создаётся переменная v1, содержащая вектор v1 = (x: -1, y: 4) # Создаётся переменная v2, содержащая вектор v2 = (x: 5, y: -2) # Обычный вызов функции v3 = add(v1, v2) # Вызов через точку: v1 передаётся в функцию add первым параметром v4 = v1.add(v2) # Цепочка вызовов. Результат предыдущего вычисления всегда передаётся первым параметром в следующий v5 = v1.add(v2).add(v1)
Реализация ООП в PHP содержит конструкции для обеспечения сокрытия данных. Справедливости ради скажу, что, несмотря на это, их всегда можно обойти (например, используя Reflection API). Причём не только в PHP, но и в других языках с похожей моделью, например, в Java. С другой стороны, практика показывает, что отсутствие таких механизмов не создает больших проблем при работе. К таким языкам относится JavaScript.
Недостатки
Инкапсуляция. Как и всегда в инженерной деятельности, за возможности нужно платить. Инкапсуляция, при всех своих плюсах, создает огромную проблему. Расширяемость поведения объекта падает до нуля. Если мы работаем с обычными функциями, то достаточно написать новую функцию, чтобы можно было продолжать работать. Когда речь заходит про инкапсуляцию, то всё не так. Дело в том, что методы описываются в классах. В PHP класс можно описать ровно один раз. И большая часть этих классов приходит в проекты из сторонних библиотек. Как только понадобится расширить поведение любого стороннего класса, мы сразу сталкиваемся с проблемами. Любой код из библиотек поставляется как есть, и мы не можем открыть исходный код любой библиотеки и внести необходимые нам правки. По этой причине расширяемость поведения объектов в ООП-языках — головная боль. Как правило, создатели класса пытаются заботиться об этом сами, давая возможность расширять своё поведение снаружи (если это возможно). Существуют языки, в которых эту проблему пытаются решать, позволяя "дописывать" определение класса по ходу работы программы, — например, в Ruby. В JS то же самое достигается за счёт механизма прототипов. Языки, в которых функции и данные разделены не имеют подобного недостатка, и код на них пишется, как ни странно, легче и проще (хотя местами многословнее). Сюда же можно отнести проблему, называемую антипаттерном (плохой реализацией) Божественный объект или God object.
Если вы уже немного знакомы с ООП, то можете подумать, что наследование спасает от этой проблемы. Так вот, наследование не просто не спасает, но и само по себе является проблемой. Об этом я расскажу в соответствующем курсе, когда мы разберём суть наследования как отношения между типами, и ограничениями, без которых наследование невозможно.
Представление любой маломальской сущности в языке с помощью пользовательского типа сильно раздувает программу. А сущности часто создают только по той причине, что не нашлось подходящего под неё типа (в котором логично было бы описать её). Существует миф о том, что программы, написанные в ООП-стиле (на самом деле имеется в виду та самая модель ООП, которая используется в языках типа PHP или Java), при больших размерах оказываются относительно компактными по объёму кода. В реальности всё с точностью до наоборот. Программы на языке Clojure компактнее аналогов на PHP во много раз. И чем больше кода, тем больше разрыв. Эта тема настолько животрепещущая, что кто-то не поленился и создал проект-шутку FizzBuzzEnterpriseEdition. К тому же появляются проблемы с ответственностями. Собака должна есть еду (
$dog->eat($food)
), или же еда съедается собакой ($food->eatBy($dog)
)? Несмотря на кажущуюся абсурдность, подобная проблема реальна и проявляется очень часто.Думаю, что влияние этого пункта вы уже ощутили на себе, хотя мы только начали. Слишком много языковых сущностей (Бритва Оккама). В PHP постоянно добавляют новые возможности по реализации ООП. Вот лишь некоторые из них: абстрактные классы, анонимные классы, интерфейсы, статические методы, видимость методов, видимость свойств, видимость констант, трейты, магические методы, наследование. И это только ключевые слова. А все эти механизмы могут взаимодействовать друг с другом, порождая неведомые комбинации, у каждой из которых есть своё особенное поведение и ограничения. В итоге одно и то же поведение можно реализовать десятком разных способов. Приходится знать тысячи нюансов и постоянно решать споры о том, какой подход лучше.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.