geektimes

Решетчатое наследование

  • четверг, 27 ноября 2014 г. в 02:10:59
http://habrahabr.ru/post/242649/

Наследование, при кажущейся простоте, часто приводит к сложным, сопротивляющимся изменениям структурам. Иерархии классов растут как самый настоящий лес.
Целью наследование является привязка кода (набора методов) к минимальному набору свойств сущности (как правило — объекта), которые он обеспечивает и которые ему требуются. Это упрощает повторное использование, тестирование и анализ кода. Но наборы свойств со временем становятся очень большими, начинают пересекаться нетривиальным образом. И в структуре классов появляются миксины и прочее множественное наследование.
Внести изменения в глубине иерархии становится проблематично, приходится думать заранее о «внедрении зависимостей», разрабатывать и использовать сложные инструменты рефакторинга.

Возможно ли всего этого избежать? Стоит попытаться — пусть методы будут привязаны к множеству характерных свойств объекта (тегов), а иерархия наследования выстраивается автоматически по вложенности этих множеств.

Пусть мы разрабатывает иерархию для игровых персонажей. Часть кода будет общая для всех персонажей — она привязана к пустому набору свойств. Код, отвечающий за их отображение будет представлен в виде вариантов для OpenGL и DirectX разных версий. Что-то будет зависеть от расы персонажа, что-то от наличия и вида магических способностей и тп. Теги персонажа первичны. Они перечисляются явно, а не наследуются. А реализация наследуется в зависимости от набора тегов (по вложенности). Таким образом умение стрелять из ПЗРК не окажется у кенгуру, потому что его унаследовали от пехотинца.

Идея такого подхода была предложена Дмитрием Кимом. Автор не стал ее воплощать в код, я попробую исправить это упущение.
Реализация такого подхода на Clojure, как обычно, на github.

Реализация этого метода наследования сделана поверх системы обобщенных функций (мультиметодов) Clojure.
С каждым мультиметодом, определенным с помощью defmulti, связана иерархия и функция диспечеризации, которая отображает аргументы в элементы (или массив элементов) иерархии. Обычно элементами иерархии являются типы данных, но в своих иерархиях можно использовать так же «ключевые слова» и «символы», которыми будут отмечены данные, отнесенные к нужному типу.
Конкретная реализация метода для элемента иерархии задается с помощью defmetod.
Выглядит это так:
(use 'inheritance.grid)
(def grid (make-grid-hierarchy))

(defmulti canFly "персонаж может летать" (grid-dispatch1) :hierarchy #'grid)
(defmulti canFireball "персонаж может пускать файрболы" (grid-dispatch1) :hierarchy #'grid)
(defmulti canFire "персонаж может поджечь" (grid-dispatch1) :hierarchy #'grid)

(defmethod canFly (get-grid-node {} #'grid) [p] false)
; поумолчанию персонажи не летают
(defmethod canFly (get-grid-node {:magic :air} #'grid) [p] true)
; владеющие магией воздуха - летают
(defmethod canFly (get-grid-node {:limbs :wings} #'grid) [p] true)
; крылатые летают

(defmethod canFireball (get-grid-node {} #'grid) [p] false)
; поумолчанию персонажи не пускают файрболы
(defmethod canFireball (get-grid-node {:magic :fire, :limbs :hands} #'grid) [p] (> (:mana p) 0))
; владеющие магией огня и имеющие рука - пускают, если есть мана.

(defmethod canFire (get-grid-node {} #'grid) [p] false)
; огнем, поумолчанию, ни кто не владее
(defmethod canFire (get-grid-node {:limbs :hands} #'grid) [p] true)
; рукастые могут развести огонь
(defmethod canFire (get-grid-node {:magic :fire} #'grid) [p] (> (:mana p) 0))
; владея магией руки иметь не обязательно
(defmethod canFire (get-grid-node {:magic :fire, :limbs :hands} #'grid) [p] true)
; магия и руки совместимы - Clojure боится перепутать причину, по которой дано это свойства

(def mage ((with-grid-node {:magic :fire, :limbs :hands :race :mage} #'grid) {:mana 100, :power 5}))
(def barbar ((with-grid-node {:magic :none, :limbs :hands :race :human} #'grid) {:power 500}))
(def phoenix ((with-grid-node {:magic :fire, :limbs :wings :race :mage} #'grid) {:mana 200, :power 4}))
(def elf ((with-grid-node {:magic :air, :limbs :hands :race :elf} #'grid) {:mana 300, :power 13}))

(canFire elf)
; true

(canFireball elf)
; false

(canFly elf)
; true

(canFly mage)
; false

(canFire mage)
; true


Как это устроено:
Для начала надо создать иерархию — это будет обычная иерархия Clojure, с таблицей, отображающей набор тегов (в виде ассоциативного массива) в участующий в иерархии символ. Таблица изначально пустая и хранится в метаинформации объекта-иерархии.
(defn make-grid-hierarchy "Создание новой решеточной иерархии" []
   (let [h (make-hierarchy)]
; это стандартная иерархия
    (with-meta h (assoc (or (meta h) {}) :grid-hierarchy-cache {}))))
; но с метаинформацией о решеточной структуре


Каждый используемый набор тегов должен быть зарегистрирован в иерархии — для него создан и включен в правильное место иерархии символ, и дабавлена соответствующая запись в таблицу, что бы этот символ можно было найти. Вычисление правильного места в иерархии — основа этого метода управления наследованием.
(defn register-grid-node "Регистрация нового узла решетки в иерархии" [h o]
  (let [nl (get (meta h) :grid-hierarchy-cache {})]
   (if-let [s (nl o)]
; а не зарегистрирован ли он уже
     [h s]
; тогда возвращаем старую иерархию и символ узла
     (let [s (symbol (str o))
; новый узел - создадим ему символ
           hn (reduce (fn [h [tr n]]
; пройдем по существующим узлам
                  (if (and (subobj? tr o)
; а не надо ли этот узел унаследовать от нашего
                              (not (isa? h s n)))
; Clojure нервно реагирует на попытку регистрации связи,
; которую сама может вывести
                    (derive h s n)
                    (if (and (subobj? o tr) (not (isa? h n s)))
; или наш узел унаследовать от этого
                      (derive h n s)
                      h)))
                h nl)]
        [(with-meta hn
; добавляем метаинформацию о новом узле в обновленную иерархию
                    (assoc (or (meta h) {})
                         :grid-hierarchy-cache (assoc nl o s)))
         s]))))
; и возвращаем вместе с символом нового узла


Теперь надо научиться связать тип из некоторого узла решетки, задаваемый набором тегов, с данными, которые, мы считаем, принадлежат этому типу.
(defn with-grid-node "создает функцию, добавляющую метаинформацию об узле к объекту" [n h]
  (let [s (get-grid-node n h)]
   (fn [v]
      (with-meta v (assoc (or (meta v) {}) :grid-node s)))))

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

Функции диспечеризации получаются простые.
(defn grid-dispatch "Создает диспетчер по всем аргументам метода"
        [] (fn [& v] (vec (map (fn [a] (:grid-node (meta a))) v))))
(defn grid-dispatch1 "Создает диспетчер по первому аргументу"
        [] (fn [v & _] (:grid-node (meta v))))


Такое наследование я уже пробовал реализовать на Common Lisp. Но устройство MOP я не знаю, и та реализация не встроена в CLOS и не слишком эффективна.