Wie finde ich bei einer Herde von Pferden die durchschnittliche Hornlänge aller Einhörner?

30

Die obige Frage ist ein abstraktes Beispiel für ein häufiges Problem, auf das ich im Legacy-Code stoße, oder genauer gesagt, Probleme, die sich aus früheren Versuchen zur Lösung dieses Problems ergeben.

Ich kann mir mindestens eine .NET Framework-Methode vorstellen, die dieses Problem lösen soll, wie die Enumerable.OfType<T>Methode. Aber die Tatsache, dass Sie letztendlich den Typ eines Objekts zur Laufzeit abfragen, passt nicht zu mir.

Darüber hinaus fragen jedes Pferd "Bist du ein Einhorn?" Die folgenden Ansätze kommen auch in den Sinn:

  • Eine Ausnahme auslösen, wenn versucht wird, die Länge des Horns eines Nicht-Einhorns zu ermitteln
  • Rückgabe eines Standard- oder Zauberwerts für die Länge des Horns eines Nicht-Einhorns (erfordert Standardprüfungen in jedem Code, der die Hornstatistik einer Gruppe von Pferden, die alle Nicht-Einhörner sein könnten, überprüfen soll)
  • Beseitigen Sie die Vererbung und erstellen Sie ein separates Objekt auf einem Pferd, das Ihnen sagt, ob das Pferd ein Einhorn ist oder nicht (was möglicherweise dasselbe Problem in einer Ebene nach unten drückt).

Ich habe das Gefühl, dass dies am besten mit einem "Nicht-Antworten" beantwortet wird. Aber wie gehen Sie dieses Problem an, und wenn es davon abhängt, in welchem ​​Kontext stehen Sie zu Ihrer Entscheidung?

Ich würde mich auch für Erkenntnisse interessieren, ob dieses Problem im Funktionscode noch vorhanden ist (oder vielleicht nur in Funktionssprachen, die die Wandlungsfähigkeit unterstützen?).

Dies wurde als mögliches Duplikat der folgenden Frage gekennzeichnet: Wie vermeide ich Downcasting?

Die Beantwortung dieser Frage setzt voraus, dass man im Besitz eines ist, HornMeasureran dem alle Hornmessungen vorgenommen werden müssen. Aber das ist eine ziemliche Zumutung für eine Codebasis, die nach dem egalitären Prinzip gebildet wurde, dass jeder frei sein sollte, das Horn eines Pferdes zu messen.

Ohne a HornMeasurerspiegelt der Ansatz der akzeptierten Antwort den oben aufgeführten ausnahmebasierten Ansatz wider.

Es gab auch einige Verwirrung in den Kommentaren darüber, ob Pferde und Einhörner beide Pferde sind oder ob ein Einhorn eine magische Unterart des Pferdes ist. Beide Möglichkeiten sollten in Betracht gezogen werden - vielleicht ist eine der anderen vorzuziehen?

Wasserboilerplatte
quelle
22
Pferde haben keine Hörner, daher ist der Durchschnitt undefiniert (0/0).
Scott Whitlock
3
@moarboilerplate Überall von 10 bis unendlich.
Kindermädchen
4
@StephenP: Das würde in diesem Fall mathematisch nicht funktionieren. Alle diese Nullen würden den Durchschnitt verzerren.
Mason Wheeler
3
Wenn Ihre Frage am besten mit einer Nichtantwort beantwortet wird, gehört sie nicht zu einer Q & A-Site. reddit, quora oder andere diskussionsbasierte Sites wurden für nicht antwortende Inhalte erstellt. Ich denke, es ist eindeutig zu beantworten, wenn Sie nach dem Code suchen, den @MasonWheeler angegeben hat. Andernfalls habe ich meiner Meinung nach keine Ahnung was Sie versuchen zu fragen ..
Jimmy Hoffa
3
@JimmyHoffa "Du machst es falsch" ist zufällig eine akzeptable "Nicht-Antwort" und oftmals besser als "Nun, hier ist eine Möglichkeit, wie du es machen kannst" - es ist keine ausführliche Diskussion erforderlich.
Moarboilerplate

Antworten:

11

Angenommen, Sie möchten eine Unicornals eine besondere Art von behandeln Horse, dann gibt es grundsätzlich zwei Möglichkeiten, wie Sie sie modellieren können. Der traditionellere Weg ist die Unterklassenbeziehung. Sie können vermeiden, den Typ und das Downcasting zu überprüfen, indem Sie einfach Ihren Code überarbeiten, um die Listen immer in den Kontexten zu trennen, in denen es darauf ankommt, und sie nur in den Kontexten zu kombinieren, in denen Sie sich nie um UnicornMerkmale kümmern . Mit anderen Worten, Sie arrangieren es so, dass Sie nie in die Situation geraten, in der Sie Einhörner aus einer Herde von Pferden extrahieren müssen. Dies scheint zunächst schwierig zu sein, ist jedoch in 99,99% der Fälle möglich und führt in der Regel dazu, dass Ihr Code viel sauberer wird.

Die andere Möglichkeit, ein Einhorn zu modellieren, besteht darin, allen Pferden eine optionale Hornlänge zuzuweisen. Dann können Sie testen, ob es sich um ein Einhorn handelt, indem Sie prüfen, ob es eine Hornlänge hat, und die durchschnittliche Hornlänge aller Einhörner von (in Scala) ermitteln:

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

Diese Methode hat den Vorteil, dass sie mit einer einzelnen Klasse einfacher ist, aber den Nachteil, dass sie viel weniger erweiterbar ist und eine Art Umweg über die Überprüfung auf "Einhörnigkeit" bietet. Der Trick bei dieser Lösung besteht darin, zu erkennen, wann Sie beginnen, sie häufig zu erweitern, und zu einer flexibleren Architektur überzugehen. Diese Art von Lösung ist in funktionalen Sprachen weitaus beliebter, in denen Sie einfache und leistungsstarke Funktionen haben, mit denen Sie flatMapdie NoneElemente leicht herausfiltern können.

Karl Bielefeldt
quelle
7
Dies setzt natürlich voraus, dass der einzige Unterschied zwischen einem normalen Pferd und einem Einhorn das Horn ist. Ist dies nicht der Fall, wird es sehr schnell komplizierter.
Mason Wheeler
@MasonWheeler nur in der zweiten vorgestellten Methode.
moarboilerplate
1
Entscheiden Sie sich für die Kommentare, wie Nicht-Einhörner und Einhörner in einem Vererbungsszenario niemals zusammen geschrieben werden sollten, bis Sie sich in einem Kontext befinden, in dem Sie sich nicht für Einhörner interessieren. Sicher, .OfType () kann das Problem lösen und die Dinge zum Laufen bringen, aber es löst ein Problem, das eigentlich gar nicht existieren sollte. Der zweite Ansatz funktioniert, da Optionen weitaus besser sind, als sich auf Null zu verlassen, um etwas zu implizieren. Ich denke, der zweite Ansatz kann in OO mit einem Kompromiss erreicht werden, wenn Sie die Einhornmerkmale in einer eigenständigen Eigenschaft einkapseln und äußerst wachsam sind.
Moarboilerplate
1
Gehen Sie Kompromisse ein, wenn Sie die Einhornmerkmale in einer eigenständigen Eigenschaft einkapseln und äußerst wachsam sind - warum sollten Sie sich das Leben schwer machen? Verwenden Sie Typeof direkt und sparen Sie eine Menge zukünftiger Ausgaben.
gbjbaanb
@gbjbaanb Ich würde diesen Ansatz nur für Szenarien in Betracht ziehen, in denen ein Anämiker Horseeine IsUnicornEigenschaft und eine Art UnicornStuffEigenschaft mit der Hornlänge darauf hatte (beim Skalieren für den in Ihrer Frage erwähnten Fahrer / Glitzer).
moarboilerplate
38

Sie haben so ziemlich alle Optionen abgedeckt. Wenn Sie ein Verhalten haben, das von einem bestimmten Subtyp abhängt und mit anderen Typen gemischt ist, muss Ihr Code diesen Subtyp kennen. das ist einfach logisch.

Persönlich würde ich einfach mitgehen horses.OfType<Unicorn>().Average(u => u.HornLength). Es drückt die Absicht des Codes sehr deutlich aus, was oft das Wichtigste ist, da es später von jemandem gepflegt werden muss.

Mason Wheeler
quelle
Bitte vergib mir, wenn meine Lambda-Syntax nicht stimmt. Ich bin kein großer C # -Codierer, und ich kann solch arkane Details niemals klarstellen. Es sollte jedoch klar sein, was ich meine.
Mason Wheeler
1
Keine Sorge, das Problem ist so gut wie gelöst, wenn die Liste Unicornsowieso nur s enthält (für den Datensatz, den Sie weglassen könnten return).
Moarboilerplate
4
Dies ist die Antwort, für die ich mich entscheiden würde, wenn ich das Problem schnell lösen wollte. Aber nicht die Antwort, wenn ich den Code umgestalten wollte, um ihn plausibler zu machen.
Andy
6
Dies ist definitiv die Antwort, es sei denn, Sie benötigen ein absurdes Maß an Optimierung. Durch die Klarheit und Lesbarkeit ist so ziemlich alles andere umstritten.
David sagt Reinstate Monica
1
@DavidGrinberg Was wäre, wenn Sie beim Schreiben dieser sauberen, lesbaren Methode zuerst eine Vererbungsstruktur implementieren müssten, die es zuvor nicht gab?
moarboilerplate
9

Es ist nichts falsch in .NET mit:

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

Die Verwendung des Linq-Äquivalents ist ebenfalls in Ordnung:

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

Basierend auf der Frage, die Sie im Titel gestellt haben, ist dies der Code, den ich erwarten würde, zu finden. Wenn die Frage so etwas wie "Was ist der Durchschnitt von Tieren mit Hörnern" stellen würde, wäre das anders:

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

Beachten Sie, dass bei Verwendung von Linq Average(und Minund Max) eine Ausnahme ausgelöst wird, wenn die Aufzählung leer und der Typ T nicht nullwertfähig ist. Das liegt daran, dass der Durchschnitt wirklich nicht definiert ist (0/0). Also wirklich brauchst du so etwas:

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...
}

Bearbeiten

Ich denke, dies muss hinzugefügt werden. Einer der Gründe, warum eine solche Frage bei objektorientierten Programmierern nicht gut ankommt, ist die Annahme, dass wir Klassen und Objekte zum Modellieren einer Datenstruktur verwenden. Die ursprüngliche objektorientierte Idee für Smalltalk war, Ihr Programm aus Modulen zu strukturieren, die als Objekte instanziiert wurden und Dienste für Sie ausführten, als Sie ihnen eine Nachricht schickten. Die Tatsache, dass wir auch Klassen und Objekte zum Modellieren einer Datenstruktur verwenden können, ist ein (nützlicher) Nebeneffekt, aber sie sind zwei verschiedene Dinge. Ich glaube nicht einmal , diese sollten die objektorientierte Programmierung in Betracht gezogen werden, da Sie könnten das gleiche mit einem tun struct, aber es wäre einfach nicht so schön sein.

Wenn Sie objektorientierte Programmierung verwenden, um Dienste zu erstellen, die Dinge für Sie tun, wird die Frage, ob es sich bei diesem Dienst tatsächlich um einen anderen Dienst oder um eine konkrete Implementierung handelt, im Allgemeinen aus guten Gründen verpönt. Sie erhielten eine Schnittstelle (in der Regel durch Abhängigkeitsinjektion) und sollten diese Schnittstelle / diesen Vertrag codieren.

Wenn Sie andererseits die Ideen für Klassen, Objekte und Schnittstellen (falsch) verwenden, um eine Datenstruktur oder ein Datenmodell zu erstellen, sehe ich persönlich kein Problem darin, die is-a-Idee in vollem Umfang zu nutzen. Wenn Sie definiert haben, dass Einhörner eine Unterart von Pferden sind und dies in Ihrer Domäne durchaus Sinn macht, fragen Sie unbedingt die Pferde in Ihrer Herde nach den Einhörnern. Schließlich versuchen wir in einem solchen Fall in der Regel, eine domänenspezifische Sprache zu erstellen, um die Lösungen der Probleme, die wir lösen müssen, besser auszudrücken. In diesem Sinne ist an .OfType<Unicorn>()etc. nichts auszusetzen .

Letztendlich ist es nur eine funktionale Programmierung, eine Sammlung von Elementen zu nehmen und sie nach Typ zu filtern, und keine objektorientierte Programmierung. Zum Glück können Sprachen wie C # beide Paradigmen jetzt problemlos handhaben.

Scott Whitlock
quelle
7
Sie wissen bereits, dass animal das ein ist Unicorn ; Gießen Sie sie einfach, anstatt sie zu verwenden as, oder verwenden Sie sie möglicherweise sogar noch besser, as und suchen Sie dann nach null.
Philip Kendall
3

Aber die Tatsache, dass Sie letztendlich den Typ eines Objekts zur Laufzeit abfragen, passt nicht zu mir.

Das Problem bei dieser Anweisung ist, dass Sie das Objekt unabhängig von dem verwendeten Mechanismus immer abfragen, um festzustellen, um welchen Typ es sich handelt. Das kann RTTI sein oder es kann eine Union oder eine einfache Datenstruktur sein, nach der Sie fragen if horn > 0. Die genauen Einzelheiten ändern sich geringfügig, aber die Absicht ist dieselbe - Sie fragen das Objekt in irgendeiner Weise nach sich selbst, um zu sehen, ob Sie es weiter abfragen sollten.

In Anbetracht dessen ist es sinnvoll, die Unterstützung Ihrer Sprache zu verwenden, um dies zu tun. In .NET würden Sie typeofzum Beispiel verwenden.

Der Grund dafür ist nicht nur, dass Sie Ihre Sprache gut verwenden. Wenn Sie ein Objekt haben, das aussieht wie ein anderes, aber eine kleine Veränderung aufweist, werden Sie mit der Zeit wahrscheinlich weitere Unterschiede feststellen. In Ihrem Beispiel für Einhörner / Pferde sagen Sie vielleicht, dass es nur eine Hornlänge gibt ... aber morgen werden Sie prüfen, ob ein potenzieller Reiter eine Jungfrau ist oder ob der Poop glitzert. (Ein klassisches Beispiel aus der Praxis wären GUI-Widgets, die von einer gemeinsamen Basis abgeleitet sind und bei denen Sie nach Kontrollkästchen und Listboxen unterschiedlich suchen müssen. Die Anzahl der Unterschiede wäre zu groß, um einfach ein einziges Superobjekt zu erstellen, das alle möglichen Datenpermutationen enthält ).

Wenn die Überprüfung des Objekttyps zur Laufzeit nicht funktioniert, können Sie die verschiedenen Objekte von Anfang an aufteilen. Anstatt eine einzelne Herde von Einhörnern / Pferden zu speichern, verfügen Sie über zwei Sammlungen, eine für Pferde und eine für Einhörner . Dies kann sehr gut funktionieren, auch wenn Sie sie in einem speziellen Container speichern (z. B. einer Multimap, bei der der Schlüssel der Objekttyp ist). Aber obwohl wir sie in zwei Gruppen speichern, sind wir gleich wieder beim Abfragen des Objekttyps !)

Ein ausnahmebasierter Ansatz ist sicherlich falsch. Die Verwendung von Ausnahmen als normaler Programmablauf ist ein Codegeruch (wenn Sie eine Herde Einhörner und einen Esel mit einer Muschel am Kopf haben, die sich hineinschleicht), dann würde ich sagen, dass ein ausnahmebasierter Ansatz in Ordnung ist, aber wenn Sie eine Herde Einhörner haben und Pferde, die dann jeweils auf Einhörnigkeit prüfen, sind nicht unerwartet. Ausnahmen sind für außergewöhnliche Umstände keine komplizierte ifAussage). In jedem Fall wird der Objekttyp zur Laufzeit nur abgefragt, wenn Ausnahmen für dieses Problem verwendet werden. Nur hier wird die Sprachfunktion missbraucht, um nach Nicht-Einhorn-Objekten zu suchen. Sie können auch aif horn > 0 und zumindest Ihre Sammlung schnell, klar und mit weniger Codezeilen zu verarbeiten und Probleme zu vermeiden, die entstehen, wenn andere Ausnahmen ausgelöst werden (z. B. eine leere Sammlung oder der Versuch, die Muschel dieses Esels zu messen).

gbjbaanb
quelle
In einem alten Kontext if horn > 0ist es so ziemlich die Art und Weise, wie dieses Problem zuerst gelöst wird. Die Probleme, die normalerweise auftauchen, sind, wenn Sie Fahrer und Glitzer überprüfen möchten, und horn > 0sie sind überall in nicht verwandtem Code vergraben (auch der Code leidet unter mysteriösen Fehlern, da nicht geprüft wird , ob die Hupe 0 ist). Außerdem ist es in der Regel am teuersten, ein Pferd nachträglich zu klassifizieren, weshalb ich normalerweise nicht dazu geneigt bin, wenn sie am Ende des Refaktors noch zusammen eingepfercht sind. So wird es sicherlich "wie hässlich sind die Alternativen"
Moarboilerplate
@moarboilerplate du sagst es selbst, gehst mit der billigen und einfachen Lösung und es wird zu einem Durcheinander. Aus diesem Grund wurden OO-Sprachen als Lösung für diese Art von Problem erfunden. Pferde unterzuordnen mag auf den ersten Blick teuer erscheinen, macht sich aber bald bezahlt. Mit der einfachen, aber schlammigen Lösung fortzufahren, kostet im Laufe der Zeit immer mehr.
gbjbaanb
3

Da die Frage mit einem functional-programmingTag versehen ist, können wir einen Summentyp verwenden, um die beiden Geschmacksrichtungen von Pferden widerzuspiegeln, und einen Mustervergleich, um zwischen ihnen zu unterscheiden. Zum Beispiel in 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

FP hat gegenüber OOP den Vorteil einer Daten- / Funktionstrennung, die Sie möglicherweise vor dem (ungerechtfertigten?) "Schlechten Gewissen" bewahrt, die Abstraktionsebene zu verletzen, wenn Sie aus einer Liste von Objekten eines Supertyps auf bestimmte Subtypen heruntertragen.

Im Gegensatz zu den in anderen Antworten vorgeschlagenen OO-Lösungen bietet Pattern Matching auch einen einfacheren Erweiterungspunkt, falls Equineeines Tages eine andere Horned-Art auftaucht.

guillaume31
quelle
2

Die Kurzform derselben Antwort am Ende erfordert das Lesen eines Buches oder eines Webartikels.

Besuchermuster

Das Problem ist eine Mischung aus Pferden und Einhörnern. (Die Verletzung des Liskov-Substitutionsprinzips ist ein häufiges Problem in älteren Codebasen.)

Fügen Sie dem Pferd und allen Unterklassen eine Methode hinzu

Horse.visit(EquineVisitor v)

Das Benutzerinterface von Equine sieht in Java / C # ungefähr so ​​aus.

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

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

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

Um Hörner zu messen schreiben wir jetzt ....

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;
}

Das Besuchermuster wird dafür kritisiert, Refactoring und Wachstum zu erschweren.

Kurze Antwort: Verwenden Sie das Designmuster Besucher, um doppelten Versand zu erhalten.

Siehe auch https://en.wikipedia.org/wiki/Visitor_pattern

Siehe auch http://c2.com/cgi/wiki?VisitorPattern zur Diskussion der Besucher.

siehe auch Design Patterns von Gamma et al.

Tim Williscroft
quelle
Ich wollte gerade selbst mit dem Besuchermuster antworten. Musste auf überraschende Weise nach unten scrollen, um herauszufinden, ob jemand es bereits erwähnt hatte!
Ben Thurley
0

Angenommen, in Ihrer Architektur sind Einhörner eine Unterart von Pferden und Sie begegnen Orten, an denen Sie eine Sammlung von Stellen finden, an Horsedenen sich einige befinden könnten Unicorn, würde ich persönlich mit der ersten Methode ( .OfType<Unicorn>()...) arbeiten, da dies die einfachste Art ist, Ihre Absicht auszudrücken . Für jeden, der später vorbeikommt (einschließlich sich selbst in 3 Monaten), ist sofort klar, was Sie mit diesem Code erreichen wollen: Wählen Sie die Einhörner unter den Pferden aus.

Die anderen Methoden, die Sie aufgelistet haben, sind nur eine andere Möglichkeit, die Frage "Sind Sie ein Einhorn?" Zu stellen. Wenn Sie beispielsweise eine ausnahmebasierte Methode zum Messen von Hörnern verwenden, könnte der Code so aussehen:

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

Jetzt wird die Ausnahme zum Indikator dafür, dass etwas kein Einhorn ist. Und jetzt ist dies keine Ausnahmesituation mehr, sondern Teil des normalen Programmablaufs. Und die Verwendung einer Ausnahme anstelle einer ifscheint noch schmutziger zu sein, als nur die Typprüfung durchzuführen.

Nehmen wir an, Sie gehen den Weg der magischen Werte, um Hörner bei Pferden zu überprüfen. So, jetzt sehen deine Klassen ungefähr so ​​aus:

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

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

Jetzt muss Ihre HorseKlasse über die UnicornKlasse Bescheid wissen und zusätzliche Methoden haben, um mit Dingen umzugehen, die ihr egal sind. Stellen Sie sich jetzt vor, Sie haben auch Pegasuss und Zebras, die von erben Horse. Nun Horsemuss ein FlyVerfahren sowie MeasureWings, CountStripesetc. Und dann die Unicornbekommt Klasse auch diese Methoden. Jetzt müssen alle Ihre Klassen voneinander wissen und Sie haben die Klassen mit einer Reihe von Methoden verschmutzt, die nicht vorhanden sein sollten, nur um zu vermeiden, das Typensystem zu fragen: "Ist das ein Einhorn?"

Wie wäre Horsees also, wenn Sie etwas zu s hinzufügen, um zu sagen, ob etwas a ist, Unicornund alle Hornmessungen durchführen? Nun müssen Sie prüfen, ob dieses Objekt existiert, um festzustellen, ob es sich um ein Einhorn handelt (das nur einen Scheck durch einen anderen ersetzt). Es trübt auch das Wasser ein wenig, dass Sie jetzt vielleicht eine habenList<Horse> unicornsdas hält wirklich alle Einhörner, aber das Typsystem und der Debugger können Ihnen das nicht leicht sagen. "Aber ich weiß, es sind alles Einhörner", sagst du, "der Name sagt es sogar." Was ist, wenn etwas einen schlechten Namen hat? Oder sagen Sie, Sie haben etwas mit der Annahme geschrieben, dass es wirklich nur Einhörner sind, aber dann haben sich die Anforderungen geändert und jetzt könnte auch Pegasi eingemischt werden? (Weil nichts dergleichen jemals passiert, insbesondere bei Legacy-Software / Sarkasmus.) Jetzt wird das Typensystem Ihre Pegasi gerne bei Ihren Einhörnern einsetzen. Wenn Ihre Variable als List<Unicorn>Compiler (oder als Laufzeitumgebung) deklariert worden wäre, würde dies passen, wenn Sie versuchen würden, Pegasi oder Pferde einzumischen.

Schließlich sind alle diese Methoden nur ein Ersatz für die Typprüfung. Persönlich würde ich das Rad hier lieber nicht neu erfinden und hoffen, dass mein Code genauso gut funktioniert wie etwas, das eingebaut ist und von Tausenden anderen Programmierern tausendfach getestet wurde.

Letztendlich muss der Code für Sie verständlich sein . Der Computer wird es herausfinden, unabhängig davon, wie Sie es schreiben. Sie sind derjenige, der es debuggen und darüber nachdenken muss. Treffen Sie die Wahl, die Ihnen die Arbeit erleichtert. Wenn aus irgendeinem Grund eine dieser anderen Methoden einen Vorteil für Sie darstellt, der klareren Code an den Stellen überwiegt, an denen er auftauchen würde, greifen Sie zu. Aber das hängt von Ihrer Codebasis ab.

Becuzz
quelle
Die stille Ausnahme ist definitiv schlecht - mein Vorschlag war eine Überprüfung, bei der if(horse.IsUnicorn) horse.MeasureHorn();Ausnahmen abgefangen würden und nicht - sie würden entweder ausgelöst, wenn !horse.IsUnicornSie sich in einem Einhorn-Messkontext befinden oder sich MeasureHornauf einem Nicht-Einhorn befinden. Auf diese Weise wird die Ausnahme, wenn Sie keine Fehler maskieren, vollständig in die Luft gesprengt und ist ein Zeichen dafür, dass etwas behoben werden muss. Natürlich ist es nur für bestimmte Szenarien geeignet, aber es ist eine Implementierung, die keine Ausnahmebedingung verwendet, um einen Ausführungspfad zu bestimmen.
moarboilerplate
0

Nun, es hört sich so an, als ob Ihre semantische Domäne eine IS-A-Beziehung hat, aber Sie sind ein bisschen vorsichtig, Subtypen / Vererbung zu verwenden, um dies zu modellieren - insbesondere aufgrund der Laufzeit-Typreflexion. Ich glaube jedoch, dass Sie Angst vor dem Falschen haben - Subtypisierung birgt zwar Gefahren, aber die Tatsache, dass Sie ein Objekt zur Laufzeit abfragen, ist nicht das Problem. Du wirst sehen, was ich meine.

Die objektorientierte Programmierung hat sich ziemlich stark auf den Begriff der IS-A-Beziehungen gestützt, wohl zu stark darauf, was zu zwei berühmten kritischen Konzepten führte:

Aber ich denke, es gibt eine andere, funktionalere, auf der Programmierung basierende Möglichkeit, IS-A-Beziehungen zu betrachten, die diese Schwierigkeiten möglicherweise nicht haben. Zuerst wollen wir Pferde und Einhörner in unserem Programm modellieren, also werden wir einen Horseund einen UnicornTyp haben. Was sind die Werte dieser Typen? Nun, ich würde das sagen:

  1. Die Werte dieser Typen sind Darstellungen oder Beschreibungen von Pferden bzw. Einhörnern.
  2. Es handelt sich um schematisierte Darstellungen oder Beschreibungen - sie sind nicht frei formuliert, sondern nach sehr strengen Regeln aufgebaut.

Das mag offensichtlich klingen, aber ich denke, einer der Wege, wie Menschen in Probleme wie das Kreis-Ellipsen-Problem geraten, besteht darin, diese Punkte nicht sorgfältig genug zu beachten. Jeder Kreis ist eine Ellipse, aber das bedeutet nicht, dass jede schematisierte Beschreibung eines Kreises automatisch eine schematisierte Beschreibung einer Ellipse nach einem anderen Schema ist. Mit anderen Worten, nur weil ein Kreis eine Ellipse ist, heißt das nicht, dass a sozusagen eine Circleist Ellipse. Aber es bedeutet, dass:

  1. Es gibt eine Gesamtfunktion , die eine beliebige Circle(schematisierte Kreisbeschreibung) in eine Ellipse(andere Art von Beschreibung) umwandelt , die dieselben Kreise beschreibt.
  2. Es gibt eine Teilfunktion , die eine nimmt Ellipseund, wenn sie einen Kreis beschreibt, die entsprechende zurückgibt Circle.

In Bezug auf die funktionale Programmierung muss Ihr UnicornTyp also nicht unbedingt ein Subtyp sein, sondern HorseSie benötigen nur Operationen wie die folgenden:

-- 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

Und toUnicornmuss eine Umkehrung von sein toHorse:

toUnicorn (toHorse x) = Just x

Haskells MaybeTyp ist der Typ, den andere Sprachen als "Option" bezeichnen. Beispielsweise ist der Java 8- Optional<Unicorn>Typ entweder ein Unicornoder nichts. Beachten Sie, dass zwei Ihrer Alternativen - Auslösen einer Ausnahme oder Zurückgeben eines "Standard- oder Zauberwerts" - Optionstypen sehr ähnlich sind.

Im Grunde genommen habe ich hier das Konzept der IS-A-Beziehung in Bezug auf Typen und Funktionen rekonstruiert, ohne Subtypen oder Vererbung zu verwenden. Was ich davon wegnehmen würde, ist:

  1. Ihr Modell muss einen HorseTyp haben.
  2. Der HorseTyp muss genügend Informationen codieren, um eindeutig zu bestimmen, ob ein Wert ein Einhorn beschreibt.
  3. Einige Operationen des HorseTyps müssen diese Informationen offenlegen, damit Clients des Typs beobachten können, ob eine gegebene Horseein Einhorn ist.
  4. Die Clients des HorseTyps müssen diese letzteren Operationen zur Laufzeit verwenden, um zwischen Einhörnern und Pferden zu unterscheiden.

Das ist also im Grunde genommen ein "frage jeden, Horseob es ein Einhorn ist" -Modell. Sie sind vorsichtig mit diesem Modell, aber ich denke falsch. Wenn ich Ihnen eine Liste von Horses gebe , ist alles, was der Typ garantiert, dass die Dinge, die in der Liste beschrieben werden, Pferde sind - Sie müssen also zwangsläufig zur Laufzeit etwas tun, um festzustellen, welche von ihnen Einhörner sind. Ich glaube, daran führt kein Weg vorbei - Sie müssen Operationen implementieren, die dies für Sie erledigen.

In der objektorientierten Programmierung ist dies wie folgt bekannt:

  • Habe einen HorseTyp;
  • Habe Unicornals Untertyp Horse;
  • Verwenden Sie die Laufzeit-Typreflexion als die auf den Client zugreifbare Operation, die erkennt, ob eine gegebene Horseeine ist Unicorn.

Dies hat eine große Schwäche, wenn man es aus dem Blickwinkel "Ding vs. Beschreibung" betrachtet, den ich oben vorgestellt habe:

  • Was ist, wenn Sie eine HorseInstanz haben, die ein Einhorn beschreibt, aber keine UnicornInstanz ist?

Zurück zum Anfang, dies ist meiner Meinung nach der wirklich beängstigende Teil der Verwendung von Subtyping und Downcasts zur Modellierung dieser IS-A-Beziehung - nicht die Tatsache, dass Sie eine Laufzeitprüfung durchführen müssen. Die Typografie ein wenig zu missbrauchen und zu fragen, Horseob es sich um eine UnicornInstanz handelt, ist nicht gleichbedeutend mit der Frage, Horseob es sich um ein Einhorn handelt (ob es sich um eine HorseBeschreibung eines Pferdes handelt, das auch ein Einhorn ist). Es sei denn, Ihr Programm hat große Anstrengungen unternommen, um den Code zu kapseln, der erstellt wird, Horsessodass Horsedie UnicornKlasse jedes Mal instanziiert wird, wenn ein Client versucht, ein Einhorn zu erstellen. Nach meiner Erfahrung tun Programmierer Dinge selten so sorgfältig.

Ich würde also bei dem Ansatz vorgehen, bei dem es eine explizite Operation ohne Downcast gibt, die Horses in Unicorns konvertiert . Dies kann entweder eine Methode des HorseTyps sein:

interface Horse {
    // ...
    Optional<Unicorn> toUnicorn();
}

... oder es könnte ein externes Objekt sein (Ihr "separates Objekt auf einem Pferd, das Ihnen sagt, ob das Pferd ein Einhorn ist oder nicht"):

class HorseToUnicornCoercion {
    Optional<Unicorn> convert(Horse horse) {
       // ...
    }
}

Die Wahl zwischen diesen Optionen hängt davon ab, wie Ihr Programm organisiert ist - in beiden Fällen haben Sie das Äquivalent zu meiner Horse -> Maybe UnicornOperation von oben, Sie verpacken sie nur auf unterschiedliche Weise (was zugegebenermaßen Welligkeitseffekte auf die Operationen hat, die der HorseTyp benötigt seinen Kunden aussetzen).

Sacundim
quelle
-1

Der Kommentar von OP in einer anderen Antwort hat die Frage geklärt, dachte ich

Das ist ein Teil dessen, was die Frage auch stellt. Wenn ich eine Herde Pferde habe und einige davon konzeptionell Einhörner sind, wie sollten sie existieren, damit das Problem sauber und ohne zu viele negative Auswirkungen gelöst werden kann?

So formuliert, glaube ich, brauchen wir mehr Informationen. Die Antwort hängt wahrscheinlich von einer Reihe von Dingen ab:

  • Unsere Spracheinrichtungen. ZB würde ich das wahrscheinlich in Ruby und Javascript und Java anders angehen.
  • Die Konzepte selbst: Was ist ein Pferd und was ist ein Einhorn? Welche Daten sind mit jedem verknüpft? Sind sie bis auf das Horn genau gleich oder haben sie andere Unterschiede?
  • Wie sonst verwenden wir sie, abgesehen von Durchschnittswerten für die Hornlänge? Und was ist mit Herden? Vielleicht sollten wir sie auch modellieren? Verwenden wir sie woanders? herd.averageHornLength()scheint unserem konzeptuellen Modell zu entsprechen.
  • Wie entstehen die Objekte Pferd und Einhorn? Liegt die Änderung dieses Codes im Rahmen unseres Refactorings?

Im Allgemeinen würde ich hier jedoch nicht einmal über Vererbung und Subtypen nachdenken. Sie haben eine Liste von Objekten. Einige dieser Objekte können als Einhörner identifiziert werden, vielleicht weil sie eine hornLength()Methode haben. Filtern Sie die Liste anhand dieser einhornspezifischen Eigenschaft. Jetzt wurde das Problem auf die Mittelung der Hornlänge einer Liste von Einhörnern reduziert.

OP, lass es mich wissen, wenn ich immer noch Missverständnisse habe ...

Jona
quelle
1
Faire Punkte. Um zu verhindern, dass das Problem noch abstrakter wird, müssen wir einige vernünftige Annahmen treffen: 1) eine stark typisierte Sprache 2) die Herde beschränkt die Pferde auf einen Typ, wahrscheinlich aufgrund einer Sammlung 3) Techniken wie das Typisieren von Enten sollten wahrscheinlich vermieden werden . Als für das, was geändert werden kann, gibt es nicht unbedingt irgendwelche Einschränkungen, aber jede Art von Veränderung hat seine eigenen einzigartigen Folgen ...
moarboilerplate
Wenn die Herde die Anzahl der Pferde auf einen Typ beschränkt, können wir nicht nur die Vererbung (mag diese Option nicht) oder ein Umhüllungsobjekt (sagen HerdMemberwir) festlegen, das entweder mit einem Pferd oder mit einem Einhorn initialisiert wird (um Pferd und Einhorn von der Notwendigkeit einer Untertypbeziehung zu befreien) ). HerdMemberist dann frei zu implementieren, isUnicorn()aber es sieht so aus, und die Filterlösung, die ich vorschlage, folgt.
Jonah
In einigen Sprachen kann hornLength () eingemischt werden, und wenn dies der Fall ist, kann es eine gültige Lösung sein. In Sprachen, in denen das Tippen weniger flexibel ist, müssen Sie jedoch auf hackige Techniken zurückgreifen, um das Gleiche zu tun, oder Sie müssen so etwas wie eine Hornlänge auf ein Pferd legen, bei der es zu Verwirrung im Code kommen kann, weil ein Pferd dies nicht tut. Ich habe konzeptionell keine Hörner. Auch wenn Sie mathematische Berechnungen durchführen, können Standardwerte die Ergebnisse
verzerren
Mixins sind jedoch nur Vererbung unter einem anderen Namen, es sei denn, sie werden zur Laufzeit ausgeführt. Ihre Bemerkung "Ein Pferd hat konzeptionell keine Hörner" bezieht sich auf meine Bemerkung, dass wir mehr darüber wissen müssen, was sie sind, wenn unsere Antwort beinhalten muss, wie wir Pferde und Einhörner modellieren und wie ihre Beziehung zueinander ist. Jede Lösung, die Standardwerte enthält, ist falsch.
Jonah
Um eine präzise Lösung für eine bestimmte Manifestation dieses Problems zu erhalten, benötigen Sie viel Kontext. Um Ihre Frage zu einem Pferd mit einem Horn zu beantworten und es wieder mit Mixins zu verknüpfen, dachte ich an ein Szenario, in dem eine Hornlänge, die einem Pferd beigemischt wird, das kein Einhorn ist, ein Fehler ist. Betrachten Sie ein Scala-Merkmal mit einer Standardimplementierung für hornLength, die eine Ausnahme auslöst. Ein Einhorn-Typ kann diese Implementierung außer Kraft setzen, und wenn ein Pferd es jemals in einen Kontext schafft, in dem hornLength ausgewertet wird, ist dies eine Ausnahme.
Moarboilerplate
-2

Eine Methode GetUnicorns (), die eine IEnumerable zurückgibt, scheint mir die eleganteste, flexibelste und universellste Lösung zu sein. Auf diese Weise können Sie mit jeder (Kombination von) Merkmalen umgehen, die bestimmen, ob ein Pferd als Einhorn gilt, und nicht nur mit dem Klassentyp oder dem Wert einer bestimmten Eigenschaft.

Martin Maat
quelle
Ich stimme dem zu. Mason Wheeler hat auch eine gute Lösung in seiner Antwort, aber wenn Sie Einhörner aus vielen verschiedenen Gründen an verschiedenen Orten herausgreifen müssen, enthält Ihr Code viele horses.ofType<Unicorn>...Konstrukte. Eine GetUnicornsFunktion wäre ein Einzeiler, aber sie wäre aus Sicht des Anrufers widerstandsfähiger gegen Änderungen der Beziehung zwischen Pferd und Einhorn.
Shaz
@Ryan Wenn Sie ein zurückgeben IEnumerable<Horse>, obwohl sich Ihre Einhornkriterien an einer Stelle befinden, ist es gekapselt, sodass Ihre Anrufer Vermutungen anstellen müssen, warum sie Einhörner benötigen. Ich bekomme es morgen, wenn ich das Gleiche tue. Außerdem müssen Sie einen Standardwert für eine Hupe auf dem Horse. Wenn Unicornes sich um einen eigenen Typ handelt, müssen Sie einen neuen Typ erstellen und Typzuordnungen pflegen, was zu einem Mehraufwand führen kann.
Moarboilerplate
1
@moarboilerplate: Wir betrachten all dies als unterstützend für die Lösung. Das Schöne daran ist, dass es unabhängig von Implementierungsdetails des Einhorns ist. Unabhängig davon, ob Sie anhand eines Datenelements, einer Klasse oder einer Tageszeit unterscheiden (diese Pferde können sich alle um Mitternacht in Einhörner verwandeln, wenn der Mond meines Wissens der richtige ist), bleibt die Lösung erhalten und die Benutzeroberfläche dieselbe.
Martin Maat