Похоже, ваш семантический домен имеет отношение IS-A, но вы немного опасаетесь использовать подтипы / наследование для моделирования этого - особенно из-за отражения типа времени выполнения. Однако я думаю, что вы боитесь не того, что подтипирование действительно сопряжено с опасностями, но тот факт, что вы запрашиваете объект во время выполнения, не является проблемой. Вы поймете, что я имею в виду.
Объектно-ориентированное программирование довольно сильно опирается на понятие отношений IS-A, возможно, оно слишком сильно опирается на него, что приводит к двум известным критическим концепциям:
Но я думаю, что есть еще один, более функционально-ориентированный способ взглянуть на отношения IS-A, который, возможно, не имеет таких трудностей. Во-первых, мы хотим смоделировать лошадей и единорогов в нашей программе, поэтому у нас будет тип Horse
и Unicorn
тип. Каковы значения этих типов? Ну, я бы сказал это:
- Значения этих типов являются представлениями или описаниями лошадей и единорогов (соответственно);
- Это схематические представления или описания - они не в свободной форме, они построены в соответствии с очень строгими правилами.
Это может показаться очевидным, но я думаю, что один из способов, с помощью которого люди начинают сталкиваться с такими проблемами, как проблема кругового эллипса, заключается в том, что они недостаточно внимательно следят за этими вопросами. Каждый круг является эллипсом, но это не означает, что каждое схематичное описание круга автоматически представляет собой схематическое описание эллипса в соответствии с другой схемой. Другими словами, только потому , что круг является эллипсом , не означает , что Circle
это Ellipse
, так сказать. Но это значит, что:
- Существует общая функция, которая преобразует любое
Circle
(описание схемы круга) в Ellipse
(описание другого типа), которое описывает те же круги;
- Существует частичная функция, которая принимает
Ellipse
и, если описывает круг, возвращает соответствующую Circle
.
Итак, в терминах функционального программирования ваш Unicorn
тип вообще не должен быть подтипом Horse
, вам просто нужны такие операции:
-- Convert any unicorn-description of into a horse-description that
-- describes the same unicorns.
toHorse :: Unicorn -> Horse
-- If the horse described by the given horse-description is a unicorn,
-- then return a unicorn-description of that unicorn, otherwise return
-- nothing.
toUnicorn :: Horse -> Maybe Unicorn
И toUnicorn
должен быть правым обратным к toHorse
:
toUnicorn (toHorse x) = Just x
Maybe
Тип Haskell - это то, что другие языки называют типом «option». Например, Optional<Unicorn>
тип Java 8 - это Unicorn
или ничего. Обратите внимание, что две из ваших альтернатив - выбрасывание исключения или возвращение «значения по умолчанию или магического значения» - очень похожи на типы опций.
Поэтому в основном то, что я здесь сделал, это реконструкция концепции IS-A с точки зрения типов и функций без использования подтипов или наследования. Что бы я от этого отнял:
- Ваша модель должна иметь
Horse
тип;
- В
Horse
потребности типа для кодирования достаточно информации , чтобы однозначно определить , описывает ли какое - либо значение единорога;
- Некоторые операции этого
Horse
типа должны предоставлять эту информацию, чтобы клиенты этого типа могли наблюдать, является ли данный Horse
текст единорогом;
- Клиенты этого
Horse
типа должны будут использовать эти последние операции во время выполнения, чтобы различать единорогов и лошадей.
Так что это принципиально модель «спроси каждого Horse
, единорог». Вы настороженно относитесь к этой модели, но я так думаю. Если я дам вам список Horse
s, все, что гарантирует тип, это то, что вещи, которые описывают элементы в списке, являются лошадьми - так что вам неизбежно понадобится что-то сделать во время выполнения, чтобы сказать, кто из них является единорогом. Так что я думаю, что обойти это невозможно - вам нужно реализовать операции, которые сделают это за вас.
В объектно-ориентированном программировании знакомый способ сделать это заключается в следующем:
- Иметь
Horse
тип;
- Иметь
Unicorn
в качестве подтипа Horse
;
- Используйте отражение типа среды выполнения в качестве доступной для клиента операции, которая определяет, является ли данный
Horse
объект типом Unicorn
.
Это имеет большую слабость, когда вы смотрите на это с позиции «вещь против описания», которую я представил выше:
- Что если у вас есть
Horse
экземпляр, который описывает единорога, но не является Unicorn
экземпляром?
Возвращаясь к началу, это то, что я считаю действительно страшной частью использования подтипов и даункастов для моделирования этих отношений IS-A - не факт, что вы должны выполнять проверку во время выполнения. Немного злоупотребляя типографикой, спрашивать, является Horse
ли это Unicorn
экземпляром, не является синонимом вопроса о Horse
том, является ли это единорогом (является ли это Horse
описанием лошади, которая также является единорогом). Нет, если ваша программа не пошла на многое, чтобы инкапсулировать код, Horses
который создает, так что каждый раз, когда клиент пытается создать объект Horse
, описывающий единорога, создается Unicorn
экземпляр класса. По моему опыту, программисты редко делают это осторожно.
Так что я бы пошел с подходом, где есть явная, не пониженная операция, которая преобразует Horse
s в Unicorn
s. Это может быть метод Horse
типа:
interface Horse {
// ...
Optional<Unicorn> toUnicorn();
}
... или это может быть внешний объект (ваш "отдельный объект на лошади, который говорит вам, является ли лошадь единорогом или нет"):
class HorseToUnicornCoercion {
Optional<Unicorn> convert(Horse horse) {
// ...
}
}
Выбор между ними зависит от того, как организована ваша программа - в обоих случаях у вас есть эквивалент моей Horse -> Maybe Unicorn
операции сверху, вы просто упаковываете ее по-разному (что, по общему признанию, будет иметь волновой эффект на то, какие операции Horse
нужны типу) выставлять своим клиентам).