Pull to refresh

Улучшенное наследование в CoffeeScript

Reading time3 min
Views5.4K
CoffeeScript принёс в JS неплохую абстракцию классов, основанную на прототипах.
Реализовав известную модель наследования и дополнив её наследованием методов касса,
он позволяет легко строить иерархии классов, даже не зная о цепочках прототипов.
Но и эта модель может быть улучшена.

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


Кроме создания цепочки прототипов конструктров кофе использует следующий код
для наследования свойств класса:

for key of parent
  child[key] = parent[key]  if __hasProp_.call(parent, key)

То есть все свойства просто копируются. При таком наследовании теряется гибкость.

Простейший пример — при изменении метода предка не меняются методы в
наследованных классах. Также не наследуются неперечисляемые свойства.

Было бы гораздо лучше, если свойства класса тоже наследовались по цепочке
прототипов. Всё что нужно — после наследования класса средствами кофе удалить
всё унаследованное :) и установить child.__proto__ = parent.

При таком наследовании у дочернего класса будут доступны все свойства предка,
которые так же можно переопределить. Но появляется возможность реализовывать
интересную функциональность, основанную на том, что свойства принадлежат
прототипу, а не самому объекту.

Один из примеров — переменная экземпляра класса (class instance variable).
UPD: Все таки уточню, что переменная относится к классу, не к его экземпляру.
переменная_экземпляра класса. Доступна в качестве свойства только одного класса.

Object.defineProperty Parent, 'test',
  get: -> @_test if @hasOwnProperty '_test'
  set: (val) -> @_test = val

Parent.test = 1
Parent.test # => 1
Child.test  # => undefined
Child.test = 2
Parent.test # => 1
Child.test  # => 2

Этот подход к наследованию лежит в основе пакета coffee_classkit.
В этом пакете также реализованы методы работы с классами, взятые из Ruby:
include, использующий append_features, extend, использующий extend_object,
хуки inherited, included, extended. Не стану здесь описывать их подробно:
они идиентичны аналогам из руби, только названия в кэмлкейсе.
Кто не не знаком с Ruby, надеюсь, без труда всё поймёт по исходнику,
тем более, что методы не больше шести строк.

Вся функциональность доступна с использованием обычного синтаксиса объявления класса:
classkit = require 'coffee_classkit'

class Child extends Parent
  classkit.extendsWithProto @
  classkit.include @, Mixin

Для удобвства в пакете есть класс, имеющий все эти методы в своем составе.
Унаследовав от него класс, можно использовать их в более явной и привычной форме:

class Example extends classkit.Module
  @extendsWithProto()
  @include Mixin

Также в пакет включен аналог ActiveSupport::Concern:

class Mixin extends classkit.Module
  @extendsWithProto().concern()

  @includedBlock: ->
    # выполняется в контексте базового класса
    @instanceVariable 'test'

  class @ClassMethods
    someClassMethod: ->

  someInstanceMethod: ->

class Base extends classkit.Module
  @include Mixin

  @someClassMethod()

(new Base).someInstanceMethod()

Больше простых примеров можно найти в тестах в репозитории.

С использованием описанных подходов, становится возможным писать модульный
объектно-ориентированный код, не врываясь в глобальное пространство имён.
Развёрнутый пример можно посмотреть в набросках проекта,
написанного с использованием CoffeeClasskit.
Tags:
Hubs:
+6
Comments9

Articles