Ну, це здається, що ваш семантичний домен має відносини 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
типу виставляти своїм клієнтам).