Прототипное программирование

Перейти к навигацииПерейти к поиску
Парадигмы программирования

Прототипное программирование — стиль объектно-ориентированного программирования, при котором отсутствует понятие класса, а наследование производится путём клонирования существующего экземпляра объекта — прототипа.

Каноническим примером прототип-ориентированного языка является язык Self. В дальнейшем этот стиль программирования начал обретать популярность и был положен в основу таких языков программирования, как JavaScript, Lua, Io, REBOL и др.

Сравнение с класс-ориентированным подходом

В языках, основанных на понятии «класс», все объекты разделены на два основных типа — классы и экземпляры. Класс определяет структуру и функциональность (поведение), одинаковую для всех экземпляров данного класса. Экземпляр является носителем данных — то есть обладает состоянием, меняющимся в соответствии с поведением, заданным классом.

Сторонники прототипного программирования часто утверждают, что языки, основанные на классах, приводят к излишней концентрации на таксономии классов и на отношениях между ними. В противоположность этому, прототипирование заостряет внимание на поведении некоторого (небольшого) количества «образцов», которые затем классифицируются как «базовые» объекты и используются для создания других объектов. Многие прототип-ориентированные системы поддерживают изменение прототипов во время выполнения программы, тогда как лишь небольшая часть класс-ориентированных систем (например, Smalltalk, Ruby) позволяет динамически изменять классы.

Хотя подавляющее большинство прототип-ориентированных систем основаны на интерпретируемых языках с динамической типизацией, технически возможно добавить прототипирование и в языки со статической проверкой типов. Язык Omega является одним из примеров такой системы.

Конструирование объектов

В класс-ориентированных языках новый экземпляр создаётся через вызов конструктора класса (возможно, с набором параметров). Получившийся экземпляр имеет структуру и поведение, жёстко заданные его классом.

В прототип-ориентированных системах предоставляется два метода создания нового объекта: клонирование существующего объекта, либо создание объекта «с нуля». Для создания объекта с нуля программисту предоставляются синтаксические средства добавления свойств и методов в объект. В дальнейшем, из получившегося объекта может быть получена полная его копия — клон. В процессе клонирования копия наследует все характеристики своего прототипа, но с этого момента она становится самостоятельной и может быть изменена. В некоторых реализациях копии хранят ссылки на объекты-прототипы, делегируя им часть своей функциональности; при этом изменение прототипа может затронуть все его копии. В других реализациях новые объекты полностью независимы от своих прототипов. Ниже рассмотрены оба этих случая.

//Пример наследования в прототипном программировании 
//на примере языка JavaScript

//Создание нового объекта
let foo = {name: "foo", one: 1, two: 2};

//Создание еще одного нового объекта
let bar = {two: "two", three: 3};

bar.__proto__ = foo; // foo теперь является прототипом для bar

//Если теперь мы попробуем получить доступ к полям foo из bar
//то все получится
bar.one // Равно 1

//Свои поля тоже доступны
bar.three // Равно 3

//Собственные поля выше по приоритету полей прототипов
bar.two; // Равняется "two"

Делегирование

В прототип-ориентированных языках, использующих делегирование, среда исполнения способна выполнять диспетчеризацию вызовов методов (или поиск нужных данных) просто следуя по цепочке делегирующих указателей (от объекта к его прототипу), до совпадения. В отличие от отношения «класс — экземпляр», отношение «прототип — потомки» не требует, чтобы объекты-потомки сохраняли структурное подобие со своим прототипом. С течением времени они могут адаптироваться и улучшаться, но при этом нет нужды переделывать прототип. Важно, что добавлять/удалять/модифицировать можно не только данные, но и функции, при этом функции тоже оказываются объектами первого уровня. Вследствие этого большинство прототип-ориентированных языков называют данные и методы объекта «слотами» (ячейками).

Каскадирование

При «чистом» прототипировании — именуемом также каскадным и представленном в Kevo — клонированные объекты не хранят ссылок на свои прототипы. Прототип копируется один-в-один, со всеми методами и атрибутами, и копии присваивается новое имя (ссылка). Это напоминает митоз биологических клеток.

В число преимуществ данного подхода входит тот факт, что создатель копии может менять её, не опасаясь побочных эффектов среди других потомков своего предка. Также радикально снижаются вычислительные затраты на диспетчеризацию, так как нет необходимости обходить всю цепочку возможных делегатов в поисках подходящего слота (метода или атрибута).

В число недостатков можно включить трудности с распространением изменений в системе: модификация прототипа не влечёт за собой немедленное и автоматическое изменение всех его потомков. Тем не менее, Kevo предоставляет дополнительные средства для публикации изменений среди множества объектов, причём на основании их подобия («семейного сходства»), а не по наличию общего предка, что типично для моделей с делегированием.

Другой недостаток в том, что простейшие реализации этой модели приводят к увеличенному (по сравнению с моделью делегирования) расходу памяти, так как каждый клон, пока он не изменён, будет содержать копию данных своего прототипа. Однако эта проблема разрешима оптимальным разделением неизменённых данных и применением «ленивого копирования» — что и было использовано в Kevo.

Критика

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

Что касается первых трёх пунктов, то классы часто рассматриваются как типы (и действительно, в большинстве объектно-ориентированных языков со статической типизацией так оно и есть), и предполагается, что классы предоставляют определённые договорённости и гарантируют, что экземпляры будут вести себя вполне определённым образом.

В части эффективности, объявление классов значительно упрощает компилятору задачу оптимизации, делая более эффективными как методы, так и поиск атрибутов в экземплярах. В случае языка Self немалая часть времени была потрачена на разработку таких техник компиляции и интерпретации, которые позволили бы приблизить производительность прототип-ориентированных систем к их класс-ориентированным конкурентам. Дальнейшая работа в этом направлении, а также прогресс в теории JIT-компиляторов привели к тому, что в настоящее время различие между класс- и прототип-ориентированными подходами мало влияет на эффективность результирующего кода. В частности, прототип-ориентированный Lua является одним из самых быстрых интерпретируемых языков и напрямую соперничает со многими компилируемыми,[1] а транслятор языка Lisaac генерирует код ANSI C, практически не уступающий нативному.[2]

Наконец, возможно наиболее общим местом критики против прототипного программирования является то, что сообщество разработчиков программного обеспечения недостаточно хорошо знакомо с ним, несмотря на популярность и распространённость JavaScript. Вдобавок, так как прототип-ориентированные системы сравнительно новы и всё ещё немногочисленны и редки, приёмы разработки с их использованием до сих пор не получили большого распространения.

Языки

Примечания

  1. Which programming languages are fastest? Архивная копия от 28 июня 2011 на Wayback Machine Computer Language Benchmarks Game.
  2. Lisaac vs. GNU C++. Дата обращения: 4 сентября 2010. Архивировано из оригинала 20 декабря 2008 года.

Литература

  • Иан Грэхем. Объектно-ориентированные методы. Принципы и практика = Object-Oriented Methods: Principles & Practice. — 3-е изд. — М.: «Вильямс», 2004. — С. 880. — ISBN 0-201-61913-X.

Ссылки