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