Наследование, при кажущейся простоте, часто приводит к сложным, сопротивляющимся изменениям структурам. Иерархии классов растут как самый настоящий лес.
Целью наследование является привязка кода (набора методов) к минимальному набору свойств сущности (как правило — объекта), которые он обеспечивает и которые ему требуются. Это упрощает повторное использование, тестирование и анализ кода. Но наборы свойств со временем становятся очень большими, начинают пересекаться нетривиальным образом. И в структуре классов появляются миксины и прочее множественное наследование.
Внести изменения в глубине иерархии становится проблематично, приходится думать заранее о «внедрении зависимостей», разрабатывать и использовать сложные инструменты рефакторинга.
Возможно ли всего этого избежать? Стоит попытаться — пусть методы будут привязаны к множеству характерных свойств объекта (тегов), а иерархия наследования выстраивается автоматически по вложенности этих множеств.
Пусть мы разрабатывает иерархию для игровых персонажей. Часть кода будет общая для всех персонажей — она привязана к пустому набору свойств. Код, отвечающий за их отображение будет представлен в виде вариантов для OpenGL и DirectX разных версий. Что-то будет зависеть от расы персонажа, что-то от наличия и вида магических способностей и тп. Теги персонажа первичны. Они перечисляются явно, а не наследуются. А реализация наследуется в зависимости от набора тегов (по вложенности). Таким образом умение стрелять из ПЗРК не окажется у кенгуру, потому что его унаследовали от пехотинца.
Идея такого подхода была предложена Дмитрием Кимом. Автор не стал ее воплощать в код, я попробую исправить это упущение.
Реализация такого подхода на Clojure, как обычно, на github.
Реализация этого метода наследования сделана поверх системы обобщенных функций (мультиметодов) Clojure.
С каждым мультиметодом, определенным с помощью defmulti, связана иерархия и функция диспечеризации, которая отображает аргументы в элементы (или массив элементов) иерархии. Обычно элементами иерархии являются типы данных, но в своих иерархиях можно использовать так же «ключевые слова» и «символы», которыми будут отмечены данные, отнесенные к нужному типу.
Конкретная реализация метода для элемента иерархии задается с помощью defmetod.
Выглядит это так:
Как это устроено:
Для начала надо создать иерархию — это будет обычная иерархия Clojure, с таблицей, отображающей набор тегов (в виде ассоциативного массива) в участующий в иерархии символ. Таблица изначально пустая и хранится в метаинформации объекта-иерархии.
Каждый используемый набор тегов должен быть зарегистрирован в иерархии — для него создан и включен в правильное место иерархии символ, и дабавлена соответствующая запись в таблицу, что бы этот символ можно было найти. Вычисление правильного места в иерархии — основа этого метода управления наследованием.
Теперь надо научиться связать тип из некоторого узла решетки, задаваемый набором тегов, с данными, которые, мы считаем, принадлежат этому типу.
Что бы избежать повторных поисков по таблице узлов, ��та функция получает символ, соответствующий узлу, и возвращает замыкание, добавляющее этот символ в метаинформацию своего аргумента.
Функции диспечеризации получаются простые.
Такое наследование я уже пробовал реализовать на Common Lisp. Но устройство MOP я не знаю, и та реализация не встроена в CLOS и не слишком эффективна.
Целью наследование является привязка кода (набора методов) к минимальному набору свойств сущности (как правило — объекта), которые он обеспечивает и которые ему требуются. Это упрощает повторное использование, тестирование и анализ кода. Но наборы свойств со временем становятся очень большими, начинают пересекаться нетривиальным образом. И в структуре классов появляются миксины и прочее множественное наследование.
Внести изменения в глубине иерархии становится проблематично, приходится думать заранее о «внедрении зависимостей», разрабатывать и использовать сложные инструменты рефакторинга.
Возможно ли всего этого избежать? Стоит попытаться — пусть методы будут привязаны к множеству характерных свойств объекта (тегов), а иерархия наследования выстраивается автоматически по вложенности этих множеств.
Пусть мы разрабатывает иерархию для игровых персонажей. Часть кода будет общая для всех персонажей — она привязана к пустому набору свойств. Код, отвечающий за их отображение будет представлен в виде вариантов для 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 и не слишком эффективна.