Humane VimScript: минималистичная объектная ориентация

0a6984bac74c443792f50ac72f2f401f.jpeg Я считаю VimScript крайне недружелюбным, но неожиданно мощным языком. К счастью его возможно одружелюбить, чем я и предлагаю вам заняться в этом цикле статей. Здесь мы рассмотрим некоторые решения типичных задач на VimScript с использованием объектно-ориентированной парадигмы, но не будем касаться основ этого языка. Возможно статья будет интересна так же программистам, интересующимся нестандартной реализацией ставших нам привычными решений.
Возможно некоторые из вас уже читали мои статьи о VimScript и изучали мою библиотеку vim_lib, не правда ли она удобна и проста в использовании? Не правда! Порой «запахи кода» так режут мне глаза, что я не могу его читать. Даже слабый «запашок» вызывает у меня непреодолимое желание сделать «лучше», «правильнее», «проще». К счастью это не сложно, достаточно все еще больше упростить и у меня это получилось, но сейчас не об этом. В этом цикле статей я лишь приведу шаблонные решения (паттерны если вам будет угодно) конкретных задач, а не буду изобретать новую библиотеку.

За более чем год использования моего класса Object в VimScript я убедился, что он содержит «код для галочки», от которого можно безболезненно избавится. Когда появляется «такой запах», это означает что пора все упрощать. В частности от чего можно смело отказаться при реализации объектно-ориентированной модели в VimScript:

  • Классы — их нет как таковых. Класс сводится к набору методов и конструктору, который умеет создавать объекты, расширять их этими методами и инициализировать свойства
  • Инкапсуляция — чем городить костыльный велосепед, проще условится и не использовать свойства объекта напрямую. Раз язык не реализует инкапсуляцию на прямую, не следует мучать его
  • Статичные свойства и методы — это полезная вещь, но не настолько полезная, чтобы заполнять конструктор условиями, выбирающими только не статичные свойства и методы для копирования в объект. Если нужна статика, лучше реализовать ее в виде глобального сервиса


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

Базовый класс
let s:Parent = {}
function! s:Parent.new(a) dict
  return extend({'a': a:a}, s:Parent)
endfunction

function! s:Parent.setA(a) dict
  let l:self.a = a:a
endfunction

function! s:Parent.getA() dict
  return l:self.a
endfunction

let s:pobj = s:Parent.new('foo')
echo s:pobj.getA() " foo


Четыре строчки кода для реализации целого класса. Это решение сводится к инициализации нового словаря и расширению (с помощью функции extend) его методами прототипа.

Далее рассмотрим реализацию наследования с переопределением конструктора и одного из методов родительского класса:

Дочерний класс
let s:Child = {}
function! s:Child.new(a, b) dict
  return extend(extend({'b': a:b}, s:Parent.new(a:a)), s:Child)
endfunction

function! s:Child.setB(b) dict
  let l:self.b = a:b
endfunction

function! s:Child.getB() dict
  return l:self.b
endfunction

function! s:Child.getA() dict
  return call(s:Parent.getA, [], l:self) . l:self.b
endfunction


Всего то конструктор дополняется еще одним вызовом функции extend, что позволяет расширить базовый словарь сначала объектом родительского класса, а затем методами прототипа (дочернего класса). В свою очередь вызов родительского метода из переопределяющего так же довольно просто реализуется с помощью функции call (аналог apply в JavaScript).

Дальнейшее наследование реализуется без добавления новых вызовов extend:

Дальнейшее наследование
let s:SubChild = {}
function! s:SubChild.new(a, b, c) dict
  return extend(extend({'c': a:c}, s:Child.new(a:a, a:b)), s:SubChild)
endfunction


Внимательный читатель уже догадался, что здесь реализовано множественное наследование, что позволяет использовать миксины:

Дальнейшее наследование
let s:Publisher = {}
function! s:Publisher.new() dict
  return extend({'listeners': {}}, s:Publisher)
endfunction

let s:Class = {}
function! s:Class.new() dict
  return extend(extend({}, s:Publisher.new()), s:Class)
endfunction


Полиморфизм очень важная часть объектно-ориентированной парадигмы, и я не мог обойти ее стороной, тем более у меня имеется несколько плагинов, для которых она необходима. Чтобы сделать ее реальностью необходим метод instanceof, позволяющий оценить семантику класса. Все что от него требуется, это проверить, присутствуют ли в объекте методы, объявленные в целевом классе и если да, то можно считать его экземпляром данного класса. Почему именно методы, а не свойства? Потому что мы условились работать с объектами через методы. Это так называемая «Утиная типизация».

instanceof
function! s:instanceof(obj, class)
  for l:assertFun in keys(filter(a:class, 'type(v:val) == 2'))
    if !has_key(a:obj, l:assertFun)
      return 0
    endif
  endfor

  return 1
endfunction

echo s:instanceof(s:childObject, s:Parent) " 1
echo s:instanceof(s:childObject, s:SubChild) " 0


Имея такую прекрасную функцию, не сложно реализовать интерфейсы, определяющие семантику будущих классов:

Пример интерфейса
let s:Iterable = {}
function! s:Iterable.valid() dict
endfunction

function! s:Iterable.next() dict
endfunction

function! s:Iterable.current() dict
endfunction

let s:Iterator = {}
function! s:Iterator.new(array) dict
  return extend(extend({'array': a:array, 'cursor': 0}, s:Iterable), s:Iterator)
endfunction

function! s:Iterator.valid() dict
  return exists('l:self.array[l:self.cursor]')
endfunction

function! s:Iterator.next() dict
  let l:self.cursor += 1
endfunction

function! s:Iterator.current() dict
  return l:self.array[l:self.cursor]
endfunction

let s:iterator = s:Iterator.new([1,2,3])
echo s:instanceof(s:iterator, s:Iterable) " 1


Важно помнить, что объекты расширяются классом интерфейса, а не его экземпляром. Впрочем, как в любом другом языке.
Для кого то это будет открытием, но JSON — это двоюродный брат VimScript! Не верите? Я вам это докажу:

JSON
let s:childObj = s:Child.new(1, 2)
let s:json = string(filter(s:childObj, 'type(v:val) != 2'))
echo s:json " {'a': 1, 'b': 2}
echo eval(s:json) == s:childObj " 1


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

© Habrahabr.ru