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

в 20:02, , рубрики: vim, vimscript, ооп

Humane VimScript: минималистичная объектная ориентация - 1 Я считаю 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

Пока все

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

Автор: Delphinum

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js