Upload
others
View
19
Download
0
Embed Size (px)
Citation preview
Динамический полиморфизм
Денис С. Мигинский
Основные принципы ООП
Абстракция
+
Инкапсуляция
+
Модульность
= SoC + KISS
Наследование
+
(динамический) Полиморфизм
= специфичные для ООП механизмы SoC
Вариант общего определения полиморфизма
Полиморфизм - способность объектов различных типов обладать одним и тем же публичным контрактом
Разновидности полиморфизма
Специальный (ad-hoc) – перегрузка функций. Имеет смысл только в системах со строгой типизацией, иначе вырождается в динамический полиморфизм
Параметрический (parametric) – параметризация типов другими типами. Имеет смысл только в системах со строгой типизацией. Примечание: в ФП и теории типов называется просто полиморфизмом
Динамический (subtype) – единый контракт для типов, связанных отношением генерализации. Примечание: в ООП называется просто полиморфизмом
Определение наследования
Следующие утверждения эквивалентны:
1. Если некоторое утверждение истинно для всех объектов типа T, то это же утверждение истинно для всех объектов типа S.
2. S является подтипом T.
(принцип подстановки Барбары Лисков, LSP)
1=>2: естественное отношения наследования.
Пример: утиная типизация. Если нечто выглядит как утка, крякает как утка и плавает как утка, значит это утка.
LSP: пояснение
2=>1: декларируя отношение наследования S <: T накладываем ограничения на объекты класса S не меньшие, чем на объекты класса T.
В частности:
Для объектов подтипа должны быть доступны все операции супертипа
Параметры методов подтипа не должны быть ниже по иерархии наследования, чем параметры методов супертипа
Возвращаемые значения методов подтипа не должны быть выше по иерархии возвращаемых значений в супертипе
Нельзя усиливать предусловия
Нельзя ослаблять постусловия
Должны сохраняться инварианты
Нельзя возбуждать новые типы исключений (помимо подтипов исключений супертипа)
Разделение ответственностей через полиморфизм
Вызов обычной функции:
зависимость только от контракта функции, но не от ее реализации;
реализация в системе ровно одна.
Вызов полиморфной функции (обобщенная функция, посылка сообщения):
зависимость только от контракта функции, но не от ее реализации;
для параметрического полиморфизма: одна реализация, параметризованная типом (-ами)
для специального и динамического полиморфизма: реализаций в системе несколько, отличаются типами параметров.
только для динамического полиморфизма: реализация функции может комбинироваться из частей, которые мало знают (или вообще ничего не знают) друг про друга. Простейшая форма комбинации – через super.
«Многомерный» полиморфизм
Базовый полиморфизм:
одиночное наследование, диспетчеризация по одному параметру (посылка сообщений)
Расширения:
множественное наследование (multiple inheritance)
диспетчеризация по нескольким параметрам(multiple dispatch)
вспомогательные методы (auxiliary methods)
CLOS: Common Lisp Object
SystemОсобенности: Поддерживает все перечисленные формы
динамического полиморфизма
Поведение определяется в форме обобщенных функций
Нет сокрытия состояния (из-за предыдущего пункта –больше ответственности на программисте)
Можно изменить иерархию классов в динамике, изменить класс объекта «на лету» и т.д.
В большинстве реализаций в основе имеет Meta-Object Protocol, что позволяет менять и саму объектную модель (изменить понятие класса, метода, наследования и т.д.)
Множественное наследование
Некоторые формы множественного наследования:
Множественная имплементация интерфейсов
Аннотирование с помощью интерфейсов (строго говоря, к ООП отношения не имеет)
Включение примесей
Наследование от нескольких классов, не связанных логически (не имеют общих предков, сообщений/функций) – представление композиции через наследование
Наследование от связанных классов (ромбовидная иерархия)
Иерархия классов
+capabilities()
Vehicle
FlyingVehicle FloatingVehicle LandVehicle SimpleVehicle
Armour
AmphibianVehicle
Plane Bicycle
Вызов capabilities должен печатать возможности данного транспорта. Летающее средство – летает, плавающее –плавает, простое может управляться даже обезьяной.
CLOS: определение иерархии;;;базовый класс, потомок standard-object(defclass vehicle ()
((name :initarg :name:accessor vehicle-name)))
;;;наследник vehicle(defclass land-vehicle (vehicle) ())
;;;наследник land-vehicle и floating-vehicle(defclass amphibian-vehicle
(land-vehicle floating-vehicle) ());;;остальные классы аналогично;;;...;;;обобщенная функция, не привязанная к классу(defgeneric capabilities (vh))
Экземпляры классов
(defparameter t-90 (make-instance 'armour :name "T-90"))
(defparameter my-bicycle (make-instance 'bicycle :name "Bicycle"))
(defparameter il-86 (make-instance 'plane :name "Il-86"))
Методы функции capabilities(defmethod capabilities ((vh vehicle))(format t "~a allows to:~%" (vehicle-name vh)))
(defmethod capabilities ((vh flying-vehicle))(call-next-method)(format t "fly~%"))
(defmethod capabilities ((vh land-vehicle))(call-next-method)(format t "move on land~%"))
(defmethod capabilities ((vh floating-vehicle))(call-next-method)(format t "sail~%"))
(defmethod capabilities ((vh simple-vehicle))(call-next-method)(format t "be driven by anyone~%"))
;;что напечатается?(capabilities t-90)
Поведение call-next-method
(capabilities t-90)>>T-90 allows to: (3)sail (2)move on land (1)
(capabilities my-bicycle)>>Bicycle allows to:be driven by anyonemove on land
Алгоритм диспетчеризации при вызове:
выбираются все подходящие методы
выбранные методы упорядочиваютсяв порядке обхода в ширину
вызывается наиболее специфичный метод
call-next-method вызывает следующий метод по порядку (не обязательно метод суперкласса)
+capabilities()
Vehicle
FloatingVehicle LandVehicle
Armour
AmphibianVehicle
1 2
3
Особенности методов в иерархии с множественным наследованием
При вызове call-next-method метод может:
опираться на общие правила, определяемые контрактом обобщенной функции;
опираться на тот факт, что методы суперклассов будут вызваны позже него;
считать, что всегда можно вызвать call-next-method; на корень иерархии правило не распространяется.
При проектировании иерархии следует:
при использовании комбинирования методов (call-next-method) определять методы для базового класса иерархии (даже если они пустые);
делать все методы коммутативными (по крайней мере для несвязанных наследованием классов);
без необходимости избегать наследования в ромбовидной форме (хотя бояться его тоже не надо).
Классическая задача
Есть иерархия фигур (бизнес-логика).
Есть несколько системных представлений фигур для разный графических фреймворков:
SWT, Swing, MySuperFramework.
Задача:
Сделать, чтобы разные фигуры могли рисоваться в разных фреймворках.
Проблема:
Решение «в лоб» - иерархия с множественным наследованием из классов типа
SWTRectangle <: SWTShape, Rectangle
SwingCircle <: SwingShape, Circle
приводит к комбинаторному взрыву.
+draw()
Shape
Polygon Circle
Triangle Rectangle
Классическое решение: мост
+draw()
Shape
Polygon Circle
Triangle Rectangle
+drawPoint()
+drawLine()
SystemShape
SWTShape SwingShape
+drawCircle()
MySuperShape
1 1
Проблема:
Мы вынуждены довольствоваться минимальным базисом для всех фигур, хотя знаем, что MySuperFramework умеет рисовать еще и круги.(про SWT и Swing мы этого не знаем, потому как настоящие ковбои документацию не читают)
Хотим иметь специальный метод draw для Circle + MySuperShape, не ломая при этом всей иерархии.
Другая задача: идем кататься!
+capabilities()
Vehicle
FlyingVehicle FloatingVehicle LandVehicle SimpleVehicle
Armour
AmphibianVehicle
Plane Bicycle
Driver
AnimalDriver HumanDriver
Pilot
CLOS: определение иерархии
(defclass driver ()((name :initarg :name
:accessor driver-name)))
(defclass human-driver (driver) ())
(defclass pilot-driver (human-driver) ())
(defclass animal-driver (driver) ())
;;;vehicle об этом может ничего не знать(defgeneric ride (dr vh))
Экземпляры классов
(defparameter monkey (make-instance 'animal-driver :name "Monkey"))
(defparameter anonymous (make-instance 'human-driver
:name "Anonymous"))
(defparameter pirx(make-instance 'pilot-driver :name "Pirx"))
Поехали!;;;метод по умолчанию(defmethod ride ((dr driver) (vh vehicle))(format t "~a rides ~a~%"
(driver-name dr) (vehicle-name vh)))
;;;научили обезьяну кататься на велосипеде(defmethod ride ((dr animal-driver)
(vh simple-vehicle))(format t "~a is smart and rides ~a~%"
(driver-name dr) (vehicle-name vh)))
;;;но не на всем остальном;;;(менее специфичный метод, чем предыдущий)(defmethod ride ((dr animal-driver) (vh vehicle))(format t "~a is not smart enough to ride ~a~%"
(driver-name dr) (vehicle-name vh)))
А теперь полетели!
;;;кого попало за штурвал не садим(defmethod ride ((dr driver) (vh flying-vehicle))(format t "~a requires special training to fly ~a~%"
(driver-name dr) (vehicle-name vh)))
;;;а вот пилота - садим(defmethod ride ((dr pilot-driver) (vh flying-vehicle))(format t "~a flies on ~a~%"
(driver-name dr) (vehicle-name vh)))
Диспетчеризация по двум параметрам в действии
(ride monkey my-bicycle)>>Monkey is smart and rides Bicycle
(ride monkey t-90)>>Monkey is not smart enough to ride T-90
(ride anonymous il-86)>>Anonymous requires special training to fly Il-86
(ride pirx il-86)>>Pirx flies on Il-86
Проблемы с приоретизацией(defclass base1 () ())(defclass derived1 (base1) ())
(defclass base2 () ())(defclass derived2 (base2) ())
(defparameter p1 (make-instance 'derived1))(defparameter p2 (make-instance 'derived2))
(defgeneric mix (p1 p2))
(defmethod mix ((p1 derived1) (p2 base2))(format t "1st param specific"))
(defmethod mix ((p1 base1) (p2 derived2))(format t "2nd param specific"))
;;;что будет напечатано?(mix p1 p2)
Решение проблемы с приоретизацией
Если используется комбинирование (call-next-method работает точно также!) и мы соблюдаем все правила (в т.ч. коммутативность) – проблем нет.
Если не используется комбинирование, то либо не допускаем неоднозначностей, либо считаем, что нам не важно, какой метод будет вызван
CLOS в этом случае вызовет какой-то из методов с наибольшей специфичностью. В Clojure требуется явно установить приоритет.
Примечание: для множественного наследования проблема также актуальна
Вспомогательные методы: готовим и обслуживаем транспорт;;методы :before и :after комбинируются сами;;call-next-method не нужен(defmethod ride :before ((dr driver) (vh land-vehicle))(format t "Fuel the tank~%"))
(defmethod ride :after ((dr driver) (vh land-vehicle))(format t "Turn on alarm~%"))
(defmethod ride :before ((dr driver) (vh floating-vehicle))(format t "Set sails~%"))
(defmethod ride :after ((dr driver) (vh floating-vehicle))(format t "Take in sails~%"))
(defmethod ride :before ((dr driver) (vh flying-vehicle))(format t "Check parachute~%"))
(defmethod ride :after ((dr driver) (vh flying-vehicle))(format t "Be happy with successful landing~%"))
Грузим в танк боеприпасы и наблюдаем за процессом
(defmethod ride :before ((dr driver) (vh armour))(format t "Load ammunition~%"))
(defmethod ride :after ((dr driver) (vh armour))(format t "Leave vehicle without hurts~%"))
(defmethod ride :around ((dr driver) (vh vehicle))(format t "Start observing the show~%");;стандартный паттерн :around-метода:;;запоминаем результат, и потом его возвращаем;;call-next-method вызывает то что «завернули»(let ((result (call-next-method)))(format t "Finish observing the show~%")result))
;;;что будет напечатано?(ride pirx t-90)
Результат поездки в танке
(ride pirx t-90)>>Start observing the showLoad ammunitionFuel the tankSet sailsPirx rides T-90Take in sailsTurn on alarmLeave vehicle without hurtsFinish observing the show
Алгоритм стандартного комбинатора методов
Для каждого типа методов стоится упорядоченный по специфичности список (с учетом наследования по всем управляемым параметрам)
Порядок вызова:
1. :around методы начиная с наиболее специфичного (явно, через call-next-method)
2. :before методы начиная с наиболее специфичного (неявно)
3. основные методы, начиная с наиболее специфичного (явно, через call-next-method)
4. :after методы начиная с наименее специфичного (неявно)
Замечания по производительности: может быть слишком накладно вычислять комбинацию при
каждом вызове комбинация методов зависит только от классов параметров –
можно использовать кеширование (сбрасывая кэш при изменении иерархии или декларации новых методов)
при диспетчеризации по одному параметру можно вычислять комбинацию на стадии декларации классов и методов
Ошибка в проектировании: обезьяна с гранатой
(ride monkey t-90)>>Start observing the showLoad ammunitionFuel the tankSet sailsMonkey is not smart enough to ride T-90Take in sailsTurn on alarmLeave vehicle without hurtsFinish observing the show
;;может, надо было заранее проверить насчет обезьяны?
Использование вспомогательных методов по назначению
:around – единственный, кто может отменить или подменить выполнение всей комбинации методов. Самая естественная его функция – проверка контракта (пред-, пост-условия), авторизация
:before, :after – операции, не связанные с бизнес-логикой(блокировки, открытие/закрытие соединений, отложенная инициализация, отладка и профилирование). Все то же самое можно сделать через :around, но сложнее и опаснее.
Основные методы – основная логика
Примечание: все вышеперечисленно догмой не является, имеет право на существование любое использование, соблюдающее SoCи KISS
Что все это дает?Плюсы:
За счет обобщенных функций не привязанных к классам и разных форм динамического полиморфизма –широкие возможности по разделению функциональности.
Атомарная единица модульности – меньше класса и даже меньше функции (в обычном понимании). Это основа для АОП.
Механизмы не привязаны к специфике Lisp и вообще динамических языков.
Минусы:
Дилемма: как всю эту «мелочь» правильно упаковать в разумное число модулей. Принципы проектирования пакетов (Common Closure, Common Reuse, Release-Reuse Equivalency) – в помощь
Код из-за раздробленности сложнее читать, требуются удобные IDE, визуализирующие переплетения аспектов
Требуется высокая культура программирования, иначе получаем богатые возможности «выстрелить себе в ногу» (и не только себе)
Полиморфизм в Clojure: определение иерархии
;;;вместо деклараций классов - метки типов
(derive ::floating-vehicle ::vehicle)(derive ::land-vehicle ::vehicle)(derive ::amphibian-vehicle ::floating-vehicle)(derive ::amphibian-vehicle ::land-vehicle)(derive ::armour ::amphibian-vehicle)(derive ::simple-vehicle ::vehicle)(derive ::bicycle ::land-vehicle)(derive ::bicycle ::simple-vehicle)
(derive ::animal-driver ::driver)(derive ::human-driver ::driver)
Определение обобщенных функций и мультиметодов в Clojure;;;аналог defgeneric в CLOS(defmulti ride ;;;функция вычисления типа(fn [driver vehicle] [(:type driver) (:type vehicle)]))
(defmethod ride [::driver ::vehicle] [driver vehicle](println (:name driver) "rides" (:name vehicle)))
(defmethod ride [::animal-driver ::simple-vehicle] [driver vehicle](println (:name driver) "is smart and rides" (:name vehicle)))
(defmethod ride [::animal-driver ::vehicle] [driver vehicle](println (:name driver) "is not smart enough to ride"(:name vehicle)))
И снова поехали!
(let [monkey {:type ::animal-driver,:name "Monkey"}
anonymous {:type ::human-driver,:name "Anonymous"}
t-90 {:type ::armour,:name "T-90"}
bicycle {:type ::bicycle,:name "Bicycle"}]
(ride monkey bicycle)(ride monkey t-90)(ride anonymous t-90))
>>Monkey is smart and rides BicycleMonkey is not smart enough to ride T-90Anonymous rides T-90
Отличия от CLOSПлюсы:
Полиморфизм не привязан к определенной объектной модели: понятие типа определяет пользователь
Работает в т.ч. для объектной модели Java
Функция вычисления типа не обязана возвращать тип в иерархии. В качестве “типа” может быть произвольный объект, который сравнивается с образцом при диспетчеризации
Минусы:
Нет возможности комбинирования методов
Автоматически не разрешаются конфликты при множественном наследовании или диспетчеризации по нескольким параметрам