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

    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.

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 9

      0
      >… так как он использует свойство __proto__, недоступное в некоторых реализациях JS
      А почему не замените его на constructor.prototype?

      > При таком наследовании теряется гибкость. Простейший пример — при изменении метода предка не меняются методы в наследованных классах.
      ---Разве классическое наследование предполагает это? По сути, они устроили классическое наследование, а Вы предлагаете превратить его в прототипное. (Если не имеет значения, то прототипное, конечно, экономичнее, но тогда не имеет смысла этот совет: «Всё что нужно — после наследования класса средствами кофе удалить всё унаследованное...»). Зачем удалять, если нужно не писать? Т.е. пишете свою либу полностью без кофейного наследования.)

      >С использованием описанных подходов, становится возможным писать модульный
      объектно-ориентированный код, не врываясь в глобальное пространство имён.

      ---С использованием каких подходов это было невозможно?
        +1
        1. .prototype — будет применяться к экземплярам класса. Если вы про то, чтобы сделать промежуточный класс для создания класса потомка с нужным прототипом, то тоже загвоздка тут. Чтобы этот прототип применился, нужно вызвать new. При этом JS создает объект с таким прототипом, а нужна функция. Все равно придется использовать __proto__.

        Если же я неправильно понял, приведите, пожалуйста, пример.

        2. Я уже и забывать начал, что где-то нельзя переопределять методы :) Но раз в JS можно переопределять, я этот контекст и использовал. Не уверен, называется ли имеющееся классическим, но в любом случае оно принадлежит подмножеству, доступному с помощью classkit.
        2.1. Можно конечно и свою полностью, но так как-то нагляднее и везде одинаково. А где потребуется — там расширять возможности.
        Но идея отличная — я добавлю такую функциональность.

        3. Тут вы тоже правы — формулировка неправильная. Можно было. Но у каждого свой способ, и это часто мешает совместной разработке. classkit я писал с оглядкой на Ruby, потому что в нём пока лучшая подель модулей и классов, которую я встречал. Изобретать свой велосипед заново и обкатывать его тоже не хотелось.
          0
          1. Нет, я про то, чтобы просто везде заменить .__proto__ на .constructor.prototype, т.к. это его определение за малыми исключениями для null и ещё чего-то. Оправдание __proto__ может быть лишь в этих исключениях (или нежелании их обработать :) ), потому и вопрос, что помешало?
            0
            К сожалению, Function.protoype (ведь это он и есть SomeClass.constructor.prototype) в моей версии node.js доступен только для чтения. Да и в браузере так же. К нему лишь можно добавлять свойства.
            Да и если поменять его сначала для одной функции, а потом для другой, то и в первой он поменяется. А кому нужно наследование, которое наследует во все классы сразу.
            А extendsWithProto как раз таки добавляет предка только в качестве прототипа одного класса.
            Если вы правда знаете, как реализовать для всех движков, поделитесь, пожалуйста. Я буду очень благодарен.

            Я добавлю еще один пример работы classkit, если может остались вопросы по его функциям.
            describe 'coffee_classkit', ->
              describe '#extendsWithProto', ->
                beforeEach ->
                  class @A
                    @x = -> true
                  class @B extends @A
                    classkit.extendsWithProto @
            
                it 'keeps child`s own properties clean', ->
                  assert.deepEqual Object.keys(@A), ['x']
                  assert.deepEqual Object.keys(@B), ['__super__']
                  assert.equal @A.x(), true
                  assert.equal @B.x(), true
            
              0
              Нет, я никогда не менял прототипы, надобности не было, поэтому и спросил. Для изменения, действительно, «constructor.prototype» не проходит в свойство. Как оказалось, a.constructor !==A. Смотрите:
                  A = function(){
                      this.aa = 'aaAProp';
                  };
                  A.prototype = {
                      bb: 'bbAproto'
                  };
                  a = new A();
                  a.constructor.prototype.bb = 'a-changeProto';
                  alert(a.constructor.prototype.bb) //a-changeProto - прототип (неизвестно чего) запоминается
                  alert(a.bb) //bbAproto - но не распространяется
              

              Но если обращаться напрямую к родителям (ссылками или цепочками по superclass при наследовании классов), то свойства предков нормально меняются:
                  A.prototype.bb = 'new-bbAproto';
                  alert(a.bb) //new-bbAproto
              

              Поэтому добраться всегда можно без __proto__, если есть такая задача. Хотя да, будут некрасивые лишние оболочки, делающие аналог __proto__.

              В частности, при наследовании конструкторов мы явно прописываем constructor, и тогда схема работает через «constructor.prototype».
                0
                Я, наверное, непонятно выразился в предыдущем коментарии. Класс — технически это функция. А у любой функции в js .constructor — Function. A Function.prototype (он же .constructor.prototype для класса) перезаписать нельзя, только изменять. Да и то выходит, что он один на все функции.

                Во 2м примере вы привели пример изменения прототипа класса. Это всегда было. Я добивался того, чтобы так же можно было поступать с методами класса (статическими, если так будет понятнее). A.classMethod напримерю. Чтобы в унаследованном В, он был доступен по цепочке прототипов, а не собственным свойством.
        0
        CoffeeScript принёс в JS неплохую абстракцию классов, основанную на прототипах.

        Учитывая, что она всегда была в JS.
          0
          Абстракция? Если точнее, были конструкторы и их прототипы. Кофе поверх этого позволил группировать код относящийся к классу, объявлять методы классов, определять наследование, для которого до этого нужно было было писать функции или подключать библиотеки.
            0
            Хм, виноват. Всё таки изначально я неправильно вас понял. Теперь всё понятно.

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

        Самое читаемое