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, позволяющий оценить семантику класса. Все что от него требуется, это проверить, присутствуют ли в объекте методы, объявленные в целевом классе и если да, то можно считать его экземпляром данного класса. Почему именно методы, а не свойства? Потому что мы условились работать с объектами через методы. Это так называемая "Утиная типизация".

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 — легко!

Для кого то это будет открытием, но 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

Пока все

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

ссылка на оригинал статьи https://habrahabr.ru/post/278623/