Humane VimScript: минималистичная объектная ориентация
Я считаю 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, позволяющий оценить семантику класса. Все что от него требуется, это проверить, присутствуют ли в объекте методы, объявленные в целевом классе и если да, то можно считать его экземпляром данного класса. Почему именно методы, а не свойства? Потому что мы условились работать с объектами через методы. Это так называемая «Утиная типизация».
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! Не верите? Я вам это докажу:
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
Надеюсь эта статья вас заинтересует и побудит попробовать этот замечательный редактор. Если так, то я постараюсь сделать вашу жизнь проще в следующих статьях.