Прототипная объектная ориентация

Предыстория
Приветствую. Я уже не раз говорил, что язык JS я считаю если не самым любимым, то как минимум вторым (после C++). Это связано с тем, что благодаря этому, казалось бы, сложному языку, я постиг один из "дзенов" программирования, а именно великую парадигму прототипной ориентации объектно-ориентированного языка. Многие программисты, не знакомые с этим, не побоюсь этого слова, чистой реализацией, считают ее более сложной и даже не объектной, но на самом деле это не так, скорее наоборот, классическая объектная ориентация оказывается всего лишь надстройкой "сахара" над этой парадигмой.
Не так давно, я начал изучать язык скриптов известного редактора Vim, и сразу же обратил внимание на его объектную парадигму, и занесло...

Объекты в Vim
Забегая вперед скажу, что в этой статье речь пойдет о реализации прототипной ориентации в Vim(!), но все попорядку.
В Vim объекты создаются и используются следующим образом (" в Vim это начало строки комментария):

let obj = {'name': 'Artur'} " Создаем объект
function! obj.getName() dict " Создаем метод объекта
  return self.name
endfunction
echo obj.getName() " Вызываем метод объекта


Что мы видим из стандартного синтаксиса Vim?
* Классы не используются вообще, то есть нет возможности заранее определить структуру будущего объекта, его свойства и методы, объекты создаются сразу перечислением всех свойств и методов, а затем к ним добавляются методы.
* Нет возможности наследовать поведение, то есть мы не можем создать объект основываясь на структуре другого объекта, нужно явно копировать в новый объект все свойства и методы родителя.
* Снижается повторное использование кода, за счет отсутствия наследования, то есть возможности выносить логику в родительские объекты.
Этот список можно продолжать еще долго, но это есть то, с чего начинается любой объектно-ориентированный язык программирования - ассоциативный массив с возможностью передачи функций в качестве его элементов.
Как из всего этого сделать прототипную модель? Достаточно реализовать всего один метод!

Прототипирование
Сначала немного теории. Чтобы существующая в Vim объектная модель могла хоть немного походить на прототипную, достаточно реализовать возможность наследования реализации. В прототипной модели делается это довольно просто, но чтобы понять это, необходимо забыть о классической реализации объектности в языках программирования. Дело в том, что в прототипной модели нет понятия класса вообще, то есть за класс выступает обычный объект, называемый прототипом. Предположим нам нужно реализовать программу для магазина. Имеем два типа объектов: покупатели и сотрудники. И тот и другой тип объектов по сути является типом "Человек", то есть они отличаются только несколькими дополнительными свойствами и методами, а общие можно вынести в отдельный тип объектов. В качестве типа объектов "Человек" выступает объект, в качестве свойств которого задаются значения по умолчанию. В этот же объект выносятся и все общие методы.

let People = {'name': 0} " Прототип Человек
function! People.getName() dict
  return self.name
endFunction

Такой объект называется прототипом, так как на основании его будут создаваться другие типы объектов, а делается это путем создания ссылки на все методы прототипа из дочернего объекта (чтобы не копировать функции), а так же созданием ссылки на сам прототип (чтобы не копировать свойства):

" Функция создает объект на основе прототипа.
" 4 prototype - прототип.
" 4 properties - свойства создаваемого объекта.
function! Expand(prototype, properties)
  let obj = {}

  " Наследование свойств.
  " Наследование реализуется путем формирования ссылки на родительский объект в свойстве parent.
  let obj.parent = a:prototype
  
  " Наследование методов.
  " Наследование реализуется путем формирования ссылок на методы родительского класса в одноименных методах дочернего.
  for k in keys(a:prototype) " Проход по всем методам прототипа
    let t = type(a:prototype[k])
    if t == 2
      let obj[k] = a:prototype[k] " Создание ссылок на методы прототипа из дочернего объекта
    endif
  endfor

  " Формирование свойств.
  " Реализуется путем копирования переданного списка в создаваемый объект.
  for [k, v] in items(a:properties) " Формирование частных свойств объекта
    let t = type(v)
    if t == 3 || t == 4
      let obj[k] = deepcopy(v)
    else
      let obj[k] = v 
    endif
  endfor

  return obj 
endfunction

Вот собственно и вся реализация прототипной модели в Vim. С помощью данной функции можно создавать объекты наследуя структуру других объектов (прототипов). Как это использовать:

let s:Object = {} " Корневой прототип
function s:Object.getType() dict " Метод прототипа
  return 'prototype'
endfunction

" Создание объекта через прототип
let s:o1 = Expand(s:Object, {'name': 'Artur'}) 
function s:o1.getName() dict " Метод объекта
  return self.name
endfunction
echo s:o1.getType() " prototype - вызов метода прототипа
echo s:o1.getName() " Artur - вызов метода объекта

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

let s:o1 = Expand(s:Object, {'name': 'Artur'}) 
function s:o1.getType() dict " Переопределение метода
  return 'object'
endfunction
echo s:o1.getType() " object - вызов переопределенного метода

При переопределении метода всегда можно получить доступ к перегружаемому методы через ссылку parent:

echo s:o1.parent.getType() " prototype - вызов родительского метода


Итоги
Никогда не знаешь, сколько интересного можно найти в "стареньком" редакторе. Обязательно изучайте различные реализации давно известных вам решений, это позволяет взглянуть на одни и те же вещи под совершенно разными углами.

URL: https://visavi.net/articles/528