Похоже, ваш семантический домен имеет отношение 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, единорог». Вы настороженно относитесь к этой модели, но я так думаю. Если я дам вам список Horses, все, что гарантирует тип, это то, что вещи, которые описывают элементы в списке, являются лошадьми - так что вам неизбежно понадобится что-то сделать во время выполнения, чтобы сказать, кто из них является единорогом. Так что я думаю, что обойти это невозможно - вам нужно реализовать операции, которые сделают это за вас.
В объектно-ориентированном программировании знакомый способ сделать это заключается в следующем:
- Иметь
Horseтип;
- Иметь
Unicornв качестве подтипа Horse;
- Используйте отражение типа среды выполнения в качестве доступной для клиента операции, которая определяет, является ли данный
Horseобъект типом Unicorn.
Это имеет большую слабость, когда вы смотрите на это с позиции «вещь против описания», которую я представил выше:
- Что если у вас есть
Horseэкземпляр, который описывает единорога, но не является Unicornэкземпляром?
Возвращаясь к началу, это то, что я считаю действительно страшной частью использования подтипов и даункастов для моделирования этих отношений IS-A - не факт, что вы должны выполнять проверку во время выполнения. Немного злоупотребляя типографикой, спрашивать, является Horseли это Unicornэкземпляром, не является синонимом вопроса о Horseтом, является ли это единорогом (является ли это Horseописанием лошади, которая также является единорогом). Нет, если ваша программа не пошла на многое, чтобы инкапсулировать код, Horsesкоторый создает, так что каждый раз, когда клиент пытается создать объект Horse, описывающий единорога, создается Unicornэкземпляр класса. По моему опыту, программисты редко делают это осторожно.
Так что я бы пошел с подходом, где есть явная, не пониженная операция, которая преобразует Horses в Unicorns. Это может быть метод Horseтипа:
interface Horse {
// ...
Optional<Unicorn> toUnicorn();
}
... или это может быть внешний объект (ваш "отдельный объект на лошади, который говорит вам, является ли лошадь единорогом или нет"):
class HorseToUnicornCoercion {
Optional<Unicorn> convert(Horse horse) {
// ...
}
}
Выбор между ними зависит от того, как организована ваша программа - в обоих случаях у вас есть эквивалент моей Horse -> Maybe Unicornоперации сверху, вы просто упаковываете ее по-разному (что, по общему признанию, будет иметь волновой эффект на то, какие операции Horseнужны типу) выставлять своим клиентам).