Решетчатое наследование
Наследование, при кажущейся простоте, часто приводит к сложным, сопротивляющимся изменениям структурам. Иерархии классов растут как самый настоящий лес.Целью наследование является привязка кода (набора методов) к минимальному набору свойств сущности (как правило — объекта), которые он обеспечивает и которые ему требуются. Это упрощает повторное использование, тестирование и анализ кода. Но наборы свойств со временем становятся очень большими, начинают пересекаться нетривиальным образом. И в структуре классов появляются миксины и прочее множественное наследование.Внести изменения в глубине иерархии становится проблематично, приходится думать заранее о «внедрении зависимостей», разрабатывать и использовать сложные инструменты рефакторинга.Возможно ли всего этого избежать? Стоит попытаться — пусть методы будут привязаны к множеству характерных свойств объекта (тегов), а иерархия наследования выстраивается автоматически по вложенности этих множеств.
Пусть мы разрабатывает иерархию для игровых персонажей. Часть кода будет общая для всех персонажей — она привязана к пустому набору свойств. Код, отвечающий за их отображение будет представлен в виде вариантов для 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 и не слишком эффективна.