З огляду на стадо коней, як я можу знайти середню довжину рогів у всіх єдинорогів?


30

Питання вище - це абстрактний приклад поширеної проблеми, з якою я стикаюся у застарілому коді, а точніше - проблем, що виникають внаслідок попередніх спроб вирішення цієї проблеми.

Я можу придумати хоча б один .NET Framework метод, який призначений для вирішення цієї проблеми, як Enumerable.OfType<T>метод. Але той факт, що ви врешті-решт допитуєтесь типу об’єкта під час виконання, не відповідає мені.

Поза тим, щоб запитати кожного коня "Ти єдиноріг?" наступні підходи також приходять до тями:

  • Накиньте виняток, коли робиться спроба отримати довжину рога не єдинорога (викриває функціональність, не підходяща для кожної коні)
  • Повернути значення за замовчуванням або магічне значення довжини рогу не єдинорога (потрібні перевірки за замовчуванням, що перебувають у будь-якому коді, який хоче розчавити статистику рога на групі коней, які можуть бути не єдинорогами)
  • Усуньте спадщину і створіть на коні окремий об’єкт, який скаже вам, чи кінь є єдиноріг чи ні (що потенційно може висунути ту саму проблему вниз по шару)

У мене таке відчуття, що найкраще відповісти «невідповіддю». Але як ви підходите до цієї проблеми і якщо вона залежить від того, який контекст є вашим рішенням?

Мене також зацікавлять будь-які уявлення про те, чи існує ця проблема все-таки у функціональному коді (чи, можливо, вона існує лише у функціональних мовах, які підтримують змінність?)

Це було позначено як можливий дублікат наступного питання: Як уникнути зриву?

Відповідь на це запитання передбачає, що є власник, за HornMeasurerдопомогою якого слід проводити всі вимірювання рогів. Але це цілком накладення на кодову базу, сформовану за егалітарним принципом, що кожен повинен вільно вимірювати ріг коня.

Якщо немає HornMeasurer, підхід прийнятої відповіді відображає підхід, заснований на винятках, перерахований вище.

У коментарях також з’явилася певна плутанина щодо того, чи є коні та єдинороги обома одиницями, чи єдиноріг - це магічний підвид коня. Необхідно враховувати обидві можливості - можливо, одна краща за іншу?


22
У коней немає рогів, тому середнє значення не визначене (0/0).
Скотт Вітлок

3
@moarboilerplate Десь від 10 до нескінченності.
няня

4
@StephenP: Це не працюватиме математично для цього випадку; всі ці 0 зігнули б середнє значення.
Мейсон Уілер

3
Якщо на ваше питання найкраще відповісти без відповіді, він не належить на веб-сайті Q&A; Reddit, квора чи інші веб-сайти, що базуються на дискусіях, створені для матеріалів, що не відповідають відповідям ... але, мовляв, це може бути зрозуміло, якщо ви шукаєте код @MasonWheeler, якщо ні, я не думаю, що я не маю уявлення що ви намагаєтеся запитати ..
Джиммі Хоффа

3
@JimmyHoffa "ти робиш це неправильно", буває прийнятним "невідповіддю", а часто кращим, ніж "ну ось один спосіб, як ти міг би це зробити" - не потрібно розширювати обговорення.
moarboilerplate

Відповіді:


11

Якщо припустити, що ви хочете ставитися до Unicornособливого виду Horse, існують два способи моделювання. Більш традиційний спосіб - відношення підкласу. Ви можете уникнути перевірки типу та зменшення вмісту, просто переробляючи свій код, щоб завжди тримати списки окремо в контекстах, де це має значення, і комбінувати їх лише в контекстах, де вас ніколи не цікавлять Unicornознаки. Іншими словами, ви влаштовуєте це, щоб ніколи не потрапляти в ситуацію, коли вам потрібно видобути єдинорогів зі стада коней в першу чергу. Спочатку це здається важким, але це можливо в 99,99% випадків і зазвичай насправді робить ваш код набагато чистішим.

Інший спосіб, як ви могли б моделювати єдинорога, - це лише надавши всім коням необов’язкові довжини рогів. Тоді ви можете перевірити, чи є це єдиноріг, перевіривши, чи має він довжину рога, і знайти середню довжину рога всіх єдинорогів за (у Scala):

case class Horse(val hornLength: Option[Double])

val horse = Horse(None)
val unicorn = Horse(Some(12.0))
val anotherUnicorn = Horse(Some(6.0))

val herd = List(horse, unicorn, anotherUnicorn)
val hornLengths = herd flatMap {_.hornLength}
val averageLength = hornLengths.sum / hornLengths.size

Цей метод має перевагу в тому, що він є більш простим, з єдиним класом, але недоліком є ​​набагато менш розширюваний та мати на зразок кругового способу перевірки на "єдинорога". Трюк, якщо ви йдете з цим рішенням, полягає в тому, щоб визнати, коли ви починаєте часто розширювати його, що вам потрібно перейти до більш гнучкої архітектури. Таке рішення набагато популярніше у функціональних мовах, де у вас є прості та потужні функції, як flatMapлегко відфільтрувати Noneелементи.


7
Звичайно, це передбачає, що єдина різниця між звичайним конем та єдинорогом - це ріг. Якщо це не так, то справи дуже швидко ускладнюються.
Мейсон Уілер

@MasonWheeler лише у другому способі, представленому.
moarboilerplate

1
Розмовляйте з коментарями про те, як не єдинорогів і єдинорогів ніколи не слід брати разом у сценарій спадкування, поки ви не опинитесь у контексті, коли вам не байдуже про єдинорогів. Звичайно, .OfType () може вирішити проблему і налагодити роботу, але це вирішення проблеми, яка навіть не повинна існувати в першу чергу. Щодо другого підходу, він працює, тому що варіанти набагато кращі, ніж покладатися на нуль, щоб щось натякнути. Я думаю, що другий підхід може бути досягнутий в ОО з компромісом, якщо ви вкладете в єдину властивість риси єдинорога і будете надзвичайно пильні.
moarboilerplate

1
йти на компроміс, якщо ти вкладаєш у себе єдинорогі риси в окрему власність і надзвичайно пильний - навіщо ускладнювати життя собі. Використовуйте typeof безпосередньо та заощадите багато майбутніх випусків.
gbjbaanb

@gbjbaanb Я вважаю, що такий підхід дійсно підходить лише для сценаріїв, коли анемік Horseмав IsUnicornвластивість та якесь UnicornStuffвластивість із довжиною рогу на ньому (коли масштабування для вершника / блиску, зазначеного у вашому запитанні).
moarboilerplate

38

Ви майже охопили всі варіанти. Якщо у вас поведінка, яка залежить від конкретного підтипу, і вона змішана з іншими типами, ваш код повинен знати про цей підтип; це прості логічні міркування.

Особисто я просто пішов би horses.OfType<Unicorn>().Average(u => u.HornLength). Це дуже чітко висловлює намір коду, що найчастіше є найважливішим, оскільки комусь в кінцевому підсумку доведеться підтримувати його згодом.


Вибачте, будь ласка, якщо мій лямбда-синтаксис неправильний; Я не дуже кодер C #, і я ніколи не можу тримати таємні деталі, як це прямо. Має бути зрозуміло, що я маю на увазі.
Мейсон Уілер

1
Не хвилюйтесь, проблема в значній мірі вирішена, як тільки в списку Unicornвсе одно є s (для запису, який ви могли опустити return).
moarboilerplate

4
Це відповідь, на яку я б пішов, Якби хотів швидко вирішити проблему. Але не відповідь, якщо я хотів би переробити код на більш правдоподібний.
Енді

6
Це, безумовно, відповідь, якщо вам не потрібен якийсь абсурдний рівень оптимізації. Чіткість та зрозумілість цього тексту значною мірою робить все іншим.
Девід каже, що повернеться до Моніки

1
@DavidGrinberg, що якщо написання цього чистого, читаного методу означало, що спочатку вам доведеться реалізувати структуру спадкування, яка раніше не існувала?
moarboilerplate

9

У .NET нічого поганого немає:

var unicorn = animal as Unicorn;
if(unicorn != null)
{
    sum += unicorn.HornLength;
    count++;
}

Використання еквівалента Linq теж добре:

var averageUnicornHornLength = animals
    .OfType<Unicorn>()
    .Select(x => x.HornLength)
    .Average();

Виходячи з питання, яке ви задали в заголовку, це код, який я б очікував знайти. Якби запитання задало щось на кшталт "що в середньому у тварин із рогами", це було б інакше:

var averageHornedAnimalHornLength = animals
    .OfType<IHornedAnimal>()
    .Select(x => x.HornLength)
    .Average();

Зауважте, що при використанні Linq, AverageMinі Max) буде викидати виняток, якщо нумерація порожня, а тип T не зводиться до нуля. Це тому, що середній дійсно не визначений (0/0). Тож справді вам потрібно щось подібне:

var hornedAnimals = animals
    .OfType<IHornedAnimal>()
    .ToList();
if(hornedAnimals.Count > 0)
{
    var averageHornLengthOfHornedAnimals = hornedAnimals
        .Average(x => x.HornLength);
}
else
{
    // deal with it in your own way...
}

Редагувати

Я просто думаю, що для цього потрібно додати ... Однією з причин такий питання, як це, не співпадає з об'єктно-орієнтованими програмістами, є те, що він передбачає, що ми використовуємо класи та об'єкти для моделювання структури даних. Початкова ідея, орієнтована на об'єкт Smalltalk, полягала в тому, щоб побудувати свою програму з модулів, які були створені як об'єкти та виконували послуги для вас, коли ви надсилали їм повідомлення. Те, що ми також можемо використовувати класи та об’єкти для моделювання структури даних, є (корисним) побічним ефектом, але це дві різні речі. Я навіть не думаю, що останнє слід вважати об'єктно-орієнтованим програмуванням, оскільки ви можете зробити те саме, що і з a struct, але це було б не так красиво.

Якщо ви використовуєте об'єктно-орієнтоване програмування для створення служб, які роблять для вас справи, то запитання, чи справді ця послуга є якоюсь іншою службою чи конкретною реалізацією, як правило, насувається з поважних причин. Вам дали інтерфейс (як правило, через ін'єкцію залежності), і вам слід ввести код цього інтерфейсу / контракту.

З іншого боку, якщо ви (неправильно) використовуєте ідеї класу / об'єкта / інтерфейсу для створення структури даних або моделі даних, я особисто не бачу проблеми з використанням ідеї "ідея" в повній мірі. Якщо ви визначили, що єдинороги є підтипом коней, і це цілком має сенс у вашому домені, тоді абсолютно продовжуйте запитувати коней у вашому стаді, щоб знайти єдинорогів. Зрештою, у такому випадку ми, як правило, намагаємось створити специфічну для домену мову, щоб краще виразити рішення проблем, які ми маємо вирішити. У цьому сенсі немає нічого поганого і .OfType<Unicorn>()т.д.

Зрештою, взяти колекцію елементів і відфільтрувати її за типом - це справді лише функціональне програмування, а не об'єктно-орієнтоване програмування. На щастя, такі мови, як C #, зручно обробляти обидві парадигми.


7
Ви вже знаєте, що animal це Unicorn ; просто киньте замість того, щоб використовувати as, або потенційно навіть краще використовувати, as а потім перевірити на null.
Філіп Кендалл

3

Але той факт, що ви врешті-решт допитуєтесь типу об’єкта під час виконання, не відповідає мені.

Проблема з цим твердженням полягає в тому, що незалежно від того, яким механізмом ви користуєтесь, ви завжди будете допитувати об’єкт, щоб сказати, якого типу він є. Це може бути RTTI або це може бути об'єднання або звичайна структура даних, де ви запитуєте if horn > 0. Точна специфіка незначно змінюється, але наміри однакові - ви запитуєте об'єкт про себе певним чином, щоб побачити, чи варто допитуватись далі.

З огляду на це, для цього є сенс використовувати підтримку вашої мови. У .NET, яку ви використовуєте, typeofнаприклад.

Причина для цього виходить за рамки простого використання вашої мови. Якщо у вас є об'єкт, схожий на інший, але для невеликих змін, швидше за все, ви знайдете більше відмінностей у часі. У вашому прикладі єдинорогів / коней ви можете сказати, що довжина рогів є просто… але завтра ви будете перевіряти, чи потенційний вершник - незайманий, чи коник блискучий. (класичним прикладом у реальному світі можуть бути віджети GUI, які походять із загальної бази, і ви повинні шукати прапорці та поля списків по-різному. Кількість відмінностей було б занадто великим, щоб просто створити єдиний супероб'єкт, який містив усі можливі перестановки даних ).

Якщо перевірка типу об’єктів під час виконання не вдається, то ваша альтернатива - розділити різні об'єкти з самого початку - замість того, щоб зберігати одне стадо єдинорогів / коней, ви тримаєте 2 колекції - одну для коней, одну для єдинорогів . Це може працювати дуже добре, навіть якщо ви зберігаєте їх у спеціалізованому контейнері (наприклад, мультимапа, де ключовим є тип об'єкта ... але тоді, хоча ми зберігаємо їх у двох групах, ми знову повертаємось при допиті типу об'єкта !)

Звичайно, підхід, заснований на винятках, є неправильним. Використання винятків як звичайного програмного потоку - це кодовий запах (якщо у вас стадо єдинорогів та осел із черепашкою, притуленою до голови, тоді я б сказав, що підхід, що базується на винятках, нормальний, але якщо у вас є стадо єдинорогів і коні потім перевірка кожного на єдинорог не є несподіваним. Винятки становлять виняткові обставини, а не складне ifтвердження). У будь-якому випадку, використовувати винятки для цієї проблеми - це просто допит типу об’єкта під час виконання, лише тут ви зловживаєте мовною функцією для перевірки на об'єкти, що не є єдинорогами. Ви також можете ввести код уif horn > 0 і принаймні обробляйте свою колекцію швидко, чітко, використовуючи менше рядків коду та уникаючи будь-яких проблем, які виникають з моменту, коли викидаються інші винятки (наприклад, порожня колекція або намагаються виміряти черепашку осла)


В давньому контексті if horn > 0це спосіб спочатку вирішити цю проблему. Тоді проблеми, які зазвичай виникають, виникають, коли ви хочете перевірити вершників та блищити, і horn > 0він похований у всьому місці у незв'язаному коді (також код страждає від таємничих помилок через відсутність перевірок, коли ріг дорівнює 0). Крім того, субкласифікація коня після факту, як правило, є найдорожчою пропозицією, тому я, як правило, не схильний робити це, якщо вони все-таки закладаються разом у кінці рефактора. Таким чином, це, безумовно, стає "наскільки потворними є альтернативи"
moarboilerplate

@moarboilerplate ви самі це говорите, ідіть з дешевим і простим рішенням, і це перетвориться на безлад. Ось чому були винайдені мови ОО, як вирішення подібної проблеми. підкласовий кінь може здатися спочатку дорогим, але незабаром окупається. Продовження простого, але каламутного рішення з часом коштує все більше і більше.
gbjbaanb

3

Оскільки в питанні є functional-programmingтег, ми могли б використовувати тип суми для відображення двох ароматів коней та відповідності шаблону, щоб розмежувати їх між собою. Наприклад, у F #:

type Equine =
| Horse
| Unicorn of hornLength: float

module equines =

  let averageHornLength (equines : Equine list) =
    equines 
    |> List.choose (fun x -> 
      match x with
      | Unicorn u -> Some(u)
      | _ -> None)
    |> List.average

let herd = [ Horse ; Horse ; Unicorn(35.0) ; Horse ; Unicorn(50.0) ]

printfn "Average horn length in herd : %f" (equines.averageHornLength herd) // prints 42.5

У відношенні OOP FP має перевагу поділу даних / функцій, що, можливо, позбавляє вас від (невиправданої?) "Винної совісті" щодо порушення рівня абстракції при спуску на конкретні підтипи зі списку об'єктів супертипу.

На відміну від рішень OO, запропонованих в інших відповідях, відповідність шаблонів також забезпечує більш просту точку розширення, якщо інший вид рогатої форми Equineз’явиться один день.


2

Коротка форма такої самої відповіді в кінці вимагає читання книги чи веб-статті.

Шаблон відвідувачів

Проблема має суміш коней та єдинорогів. (Порушення принципу заміни Ліскова є поширеною проблемою у застарілих кодових базах.)

Додайте метод до коня та всіх підкласів

Horse.visit(EquineVisitor v)

Інтерфейс для коней для відвідувачів виглядає приблизно так у java / c #

interface EquineVisitor {
  void visitHorse(Horse z);
  void visitUnicorn(Unicorn z);
}

Unicorn.visit(EquineVisitor v){
   v.visitUnicorn(this);
}

Horse.visit(EquineVisitor v){
   v.visitHorse(this);
}

Для вимірювання рогів ми зараз пишемо….

class HornMeasurer implements EquineVistor {
    void visitHorse(Horse h){} // ignore horses
    void visitUnicorn(Unicorn u){
         double len = u.getHornLength();
         totalLength+=len;
         unicornCount++;
    }

    double getAverageLength(){
          return totalLength/unicornCount;
    }

    double totalLength=0;
    int unicornCount=0;
}

Візерунок критикується за те, що він ускладнює рефакторинг та зростання.

Короткий відповідь: Використовуйте шаблон відвідувача дизайну, щоб отримати подвійну розсилку

дивіться також https://en.wikipedia.org/wiki/Visitor_pattern

Дивіться також http://c2.com/cgi/wiki?VisitorPattern для обговорення відвідувачів.

див. також Шаблони дизайну від Gamma et al.


Я збирався сам відповісти з малюнком відвідувачів. Довелося прокрутити вниз дивовижний спосіб виявити, чи хтось про це вже згадував!
Бен Терлі

0

Якщо припустити, що у вашій архітектурі єдинороги - це підвид коня, і ви зустрічаєте місця, де ви отримуєте колекцію, Horseде деякі з них можуть бути Unicorn, я б особисто пішов із першим методом ( .OfType<Unicorn>()...), оскільки це найпростіший спосіб висловити свій намір . Для кожного, хто піде пізніше (включаючи себе через 3 місяці), відразу очевидно, що ви намагаєтеся зробити з цим кодом: вибирайте єдинорогів з коней.

Інші перераховані вами методи почуваються як просто інший спосіб задати питання "Ти єдиноріг?". Наприклад, якщо ви використовуєте якийсь метод вимірювання рогів, заснований на винятках, у вас може бути код, який виглядатиме приблизно так:

foreach (var horse in horses)
{
    try
    {
        var length = horse.MeasureHorn();
        //...
    }
    catch (NoHornException e)
    {
        continue;
    }
}

Тож тепер винятком стає показник того, що щось не є єдинорогом. І тепер це вже не є винятковою ситуацією, але є частиною нормального потоку програми. І використовувати виняток замість того, що ifздається ще бруднішим, ніж просто робити перевірку типу.

Скажімо, ви йдете магічним шляхом для перевірки рогів на конях. Тож тепер ваші заняття виглядають приблизно так:

class Horse
{
    public double MeasureHorn() { return -1; }
    //...
}

class Unicorn : Horse
{
    public override double MeasureHorn { return _hornLength; }
    //...
}

Тепер ваш Horseклас повинен знати про Unicornклас і мати додаткові методи для вирішення речей, які його не цікавлять. А тепер уявіть, що у вас є також Pegasusі Zebras, які успадковують Horse. Тепер Horseпотрібен Flyметод, а також MeasureWingsі CountStripesт. Д. І тоді Unicornклас отримує і ці методи. Тепер ваші заняття повинні знати один про одного, і ви забруднили класи безліччю методів, яких не повинно бути там, щоб не запитувати систему типу "Це єдиноріг?"

Отже, що робити з додаванням чогось Horses, щоб сказати, якщо щось є, Unicornі обробити всі вимірювання ріжка? Ну, тепер вам доведеться перевірити наявність цього об’єкта, щоб знати, чи щось є єдинорогом (який просто замінює одну перевірку на іншу). Це також трохи замулює води в тому, що тепер у вас може бутиList<Horse> unicornsце дійсно вміщує всі єдинороги, але система типу та налагоджувач не можуть вам це легко сказати. "Але я знаю, що це все єдинороги", ви кажете, "ім'я навіть так говорить". Ну що робити, якщо щось погано назвали? Або скажіть, ви щось писали з припущенням, що це дійсно все єдинороги, але потім змінилися вимоги, і тепер у нього можуть бути змішані пегаси? (Тому що нічого подібного ніколи не відбувається, особливо в застарілому програмному забезпеченні / сарказмі.) Тепер система типу радісно поставить ваших пегаси разом з вашими єдинорогами. Якби ваша змінна була оголошена List<Unicorn>компілятором (або середовищем виконання), підходила б, якщо ви намагалися змішати пегаси чи коней.

Нарешті, всі ці методи є лише заміною для перевірки системи типу. Я особисто не хотів би тут винаходити колесо і сподіваюся, що мій код працює так само добре, як те, що вбудовано і перевірено тисячами інших кодерів тисячі разів.

Зрештою, код повинен бути зрозумілим для вас . Комп'ютер розбере це незалежно від того, як ви його пишете. Вам належить виправити це налагодження та мати можливість міркувати про це. Зробіть вибір, який полегшить вашу роботу. Якщо з якихось причин один з цих інших методів пропонує вам перевагу, що переважає більш чіткий код у парі, де він з'явиться, перейдіть до цього. Але це залежить від вашої кодової бази.


Мовчазний виняток, безумовно, поганий - моя пропозиція була перевірка, яка буде, if(horse.IsUnicorn) horse.MeasureHorn();і винятки не будуть спіймані - вони або будуть спрацьовувати, коли !horse.IsUnicornви знаходитесь в контексті вимірювання єдинорога, або всередині MeasureHornне-єдинорога. Таким чином, коли викид викинуто, ви не маскуєте помилок, він повністю вибухає і є знаком, який потрібно виправити. Очевидно, що це підходить лише для певних сценаріїв, але це реалізація, яка не використовує кидання виключень для визначення шляху виконання.
moarboilerplate

0

Ну, це здається, що ваш семантичний домен має відносини IS-A, але ви трохи насторожено використовуєте підтипи / успадкування для моделювання цього, особливо через відображення типу виконання. Я думаю, однак, що ти боїшся неправильної речі - підтипування дійсно призводить до небезпеки, але те, що ти запитуєш об’єкт під час виконання, не є проблемою. Ви побачите, що я маю на увазі.

Об'єктно-орієнтоване програмування досить сильно спирається на поняття відносин IS-A, воно, мабуть, занадто сильно спирається на нього, що призводить до двох відомих критичних концепцій:

Але я думаю, що існує інший, більш функціональний, заснований на програмуванні спосіб розглянути відносини IS-A, які, можливо, не мають цих труднощів. По-перше, ми хочемо моделювати коней та єдинорогів у нашій програмі, тому у нас буде тип Horseі Unicornтип. Які значення мають ці типи? Ну, я б сказав це:

  1. Цінністю цих типів є зображення або описи коней та єдинорогів (відповідно);
  2. Вони є схематизованими поданнями або описами - вони не вільної форми, вони побудовані за дуже суворими правилами.

Це може здатися очевидним, але я вважаю, що один із способів людей потрапляти до таких питань, як проблема еліпса кола, - це недостатньо уважно ставитись до цих питань. Кожне коло - це еліпс, але це не означає, що кожен схематизований опис кола автоматично є схематизованим описом еліпса за різною схемою. Іншими словами, те, що коло - це еліпс, не означає, що а Circleє Ellipse, так би мовити. Але це означає, що:

  1. Існує загальна функція, яка перетворює будь-який Circle(схематизований опис кола) в Ellipse(різний тип опису), який описує ті самі кола;
  2. Існує часткова функція, яка займає 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 з точки зору типів та функцій, не використовуючи підтипів чи успадкування. Що я б від цього забрав:

  1. Ваша модель повинна мати Horseтип;
  2. В Horseпотреби типу для кодування достатньо інформації , щоб однозначно визначити , чи описує яке - небудь значення єдинорога;
  3. У деяких операціях Horseтипу потрібно розкрити цю інформацію, щоб клієнти такого типу могли спостерігати, чи є дана Horseєдинорогом;
  4. Клієнтам цього 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типу виставляти своїм клієнтам).


-1

Коментар ОП в іншій відповіді уточнив питання, я подумав

це частина того, що задається і питанням. Якщо у мене є стадо коней, а деякі з них є концептуально єдинорогами, як вони повинні існувати, щоб проблему можна було чітко вирішити без надто багато негативних наслідків?

Фразовано таким чином, я думаю, нам потрібна додаткова інформація. Відповідь, ймовірно, залежить від кількох речей:

  • Наші мовні засоби. Наприклад, я б, мабуть, підходив до цього по-різному в рубіні та javascript та Java.
  • Самі поняття: що таке кінь і що таке єдиноріг? Які дані пов’язані з кожним? Вони абсолютно однакові, окрім рогу, чи мають інші відмінності?
  • Як ще ми їх використовуємо, окрім прийняття середніх значень довжини рогів? А як же стада? Може, ми також повинні їх моделювати? Чи використовуємо ми їх деінде? herd.averageHornLength()здається, відповідає нашій концептуальній моделі.
  • Як створюються предмети коня та єдинорога? Чи змінюється цей код в межах нашого рефакторингу?

Взагалі, однак, я б навіть не думав про спадщину та підтипи. У вас є список об’єктів. Деякі з цих об’єктів можна ідентифікувати як єдинороги, можливо, тому що вони мають hornLength()метод. Фільтруйте список на основі цієї унікальної властивості єдиноріг. Тепер проблема зводилася до усереднення довжини рогів у списку єдинорогів.

ОП, дайте мені знати, якщо я все ще нерозумію ...


1
Справедливі бали. Щоб проблема не стала ще більш абстрактною, ми повинні зробити кілька розумних припущень: 1) сильно набрана мова 2) стадо обмежує коней до одного типу, ймовірно, завдяки колекції; 3) таких методів, як набирання качок, слід, мабуть, уникати. . Щодо того, що можна змінити, тут не обов'язково існують обмеження, але кожен тип змін має свої унікальні наслідки ...
moarboilerplate

Якщо стадо обмежує коней до одного типу, це не єдиний наш спадковий вибір (не подобається цей варіант) або предмет обгортки (скажімо HerdMember), який ми ініціалізуємо або з конем, або з єдинорогом (звільнення коня та єдинорога від необхідності підтипових відносин ). HerdMemberТоді він вільний до впровадження, isUnicorn()проте вважає за потрібне, і рішення, яке я пропоную для фільтрування, наступне.
Йона

У деяких мовах hornLength () можна змішати, і якщо це так, це може бути правильним рішенням. Однак у мовах, де введення тексту менш гнучко, вам доводиться вдаватися до якихось хакерських прийомів, щоб зробити те ж саме, або вам доведеться робити щось на кшталт того, щоб покласти коня на коня, де це може призвести до плутанини в коді, тому що коня немає ' t концептуально не мають ріжків. Крім того, якщо математичні обчислення, включаючи значення за замовчуванням, можуть перекосити результати (див. Коментарі під початковим запитанням)
moarboilerplate

Міксини, однак, якщо вони не виконуються під час виконання, - це лише спадщина під іншою назвою. Ваш коментар "у коня концептуально немає жодних рогів" стосується мого коментаря про необхідність дізнатися більше про те, що вони є, якщо наша відповідь повинна включати те, як ми моделюємо коней та єдинорогів та які їх стосунки один до одного. Будь-яке рішення, що включає значення за замовчуванням, перебуває поза помилковим зображенням.
Іона

Ви правильно в цьому, щоб отримати точне рішення конкретного прояву цієї проблеми, вам потрібно мати багато контексту. Щоб відповісти на ваше запитання про коня з рогом і прив’язати його до міксин, я думав про сценарій, коли рігЛенгт змішався з конем, який не є єдинорогом, є помилкою. Розглянемо ознаку Scala, яка має за замовчуванням реалізацію hornLength, яка видає виняток. Тип єдинорога може перекрити цю реалізацію, і якщо коня коли-небудь перетворює на контекст, де оцінюється hornLength, це виняток.
moarboilerplate

-2

Метод GetUnicorns (), який повертає IEnumerable, здається мені найелегантнішим, гнучким та універсальним рішенням. Таким чином, ви могли б мати справу з будь-якими (поєднаннями) ознаками, які визначають, чи перейде коня як єдиноріг, а не лише тип класу чи значення певної властивості.


Я згоден з цим. У Мейсона Уілера є і хороше рішення у своїй відповіді, але якщо вам потрібно виділити єдинороги з багатьох різних причин у різних місцях, ваш код матиме багато horses.ofType<Unicorn>...конструкцій. Маючи aGetUnicorns функцію було б однолінійним, але воно буде надалі стійким до змін відносин кінь / єдиноріг з точки зору позивуючого.
Шаз

@Ryan Якщо ви повернете IEnumerable<Horse>, хоча ваші критерії єдинорога є в одному місці, він інкапсульований, тому ваші абоненти повинні робити припущення про те, для чого їм потрібні єдинороги (я можу отримати молюска з молюсками, замовивши сьогоднішній суп, але це не так " т. значить, я отримаю це завтра, зробивши те саме). Крім того, ви повинні виставити значення за замовчуванням для ріжка на Horse. ЯкщоUnicorn це власний тип, вам потрібно створити новий тип та підтримувати відображення типів, які можуть вводити накладні витрати.
moarboilerplate

1
@moarboilerplate: Ми вважаємо, що все це підтримує рішення. Краса полягає в тому, що вона не залежить від будь-якої деталі реалізації єдинорога. Незалежно від того, чи дискримінуєте ви на основі даних даних, класу чи часу доби (ці коні можуть перетворитися на єдинорогів опівночі, якщо Місяць є правильним для всіх, що я знаю), рішення стоїть, інтерфейс залишається тим самим.
Мартін Маат
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.