Gibt es ein Muster für ein „natürlicheres“ Hinzufügen von Elementen zu Sammlungen? [geschlossen]


13

Ich denke, die gebräuchlichste AddMethode, einer Sammlung etwas hinzuzufügen, ist die Verwendung einer Methode, die eine Sammlung bietet:

class Item {}    
var items = new List<Item>();
items.Add(new Item());

und daran ist eigentlich nichts ungewöhnliches.

Ich frage mich jedoch, warum wir das nicht so machen:

var item = new Item();
item.AddTo(items);

es scheint irgendwie natürlicher zu sein als die erste Methode. Dies hätte den Vorteil, dass, wenn die ItemKlasse eine Eigenschaft wie folgt hat Parent:

class Item 
{
    public object Parent { get; private set; }
} 

Sie können den Setter privat machen. In diesem Fall können Sie natürlich keine Erweiterungsmethode verwenden.

Aber vielleicht irre ich mich und habe dieses Muster noch nie gesehen, weil es so ungewöhnlich ist? Wissen Sie, ob es ein solches Muster gibt?

In C#einer Erweiterungsmethode wäre das sinnvoll

public static T AddTo(this T item, IList<T> list)
{
    list.Add(item);
    return item;
}

Wie wäre es mit anderen Sprachen? Ich denke, in den meisten von ihnen musste die ItemKlasse eine ICollectionItemSchnittstelle bereitstellen, die wir sie nennen .


Update-1

Ich habe ein bisschen mehr darüber nachgedacht und dieses Muster wäre zum Beispiel wirklich nützlich, wenn Sie nicht möchten, dass ein Artikel mehreren Sammlungen hinzugefügt wird.

Testschnittstelle ICollectable:

interface ICollectable<T>
{
    // Gets a value indicating whether the item can be in multiple collections.
    bool CanBeInMultipleCollections { get; }

    // Gets a list of item's owners.
    List<ICollection<T>> Owners { get; }

    // Adds the item to a collection.
    ICollectable<T> AddTo(ICollection<T> collection);

    // Removes the item from a collection.
    ICollectable<T> RemoveFrom(ICollection<T> collection);

    // Checks if the item is in a collection.
    bool IsIn(ICollection<T> collection);
}

und eine Beispielimplementierung:

class NodeList : List<NodeList>, ICollectable<NodeList>
{
    #region ICollectable implementation.

    List<ICollection<NodeList>> owners = new List<ICollection<NodeList>>();

    public bool CanBeInMultipleCollections
    {
        get { return false; }
    }

    public ICollectable<NodeList> AddTo(ICollection<NodeList> collection)
    {
        if (IsIn(collection))
        {
            throw new InvalidOperationException("Item already added.");
        }

        if (!CanBeInMultipleCollections)
        {
            bool isInAnotherCollection = owners.Count > 0;
            if (isInAnotherCollection)
            {
                throw new InvalidOperationException("Item is already in another collection.");
            }
        }
        collection.Add(this);
        owners.Add(collection);
        return this;
    }

    public ICollectable<NodeList> RemoveFrom(ICollection<NodeList> collection)
    {
        owners.Remove(collection);
        collection.Remove(this);
        return this;
    }

    public List<ICollection<NodeList>> Owners
    {
        get { return owners; }
    }

    public bool IsIn(ICollection<NodeList> collection)
    {
        return collection.Contains(this);
    }

    #endregion
}

Verwendung:

var rootNodeList1 = new NodeList();
var rootNodeList2 = new NodeList();

var subNodeList4 = new NodeList().AddTo(rootNodeList1);

// Let's move it to the other root node:
subNodeList4.RemoveFrom(rootNodeList1).AddTo(rootNodeList2);

// Let's try to add it to the first root node again... 
// and it will throw an exception because it can be in only one collection at the same time.
subNodeList4.AddTo(rootNodeList1);

13
item.AddTo(items)Angenommen, Sie haben eine Sprache ohne Erweiterungsmethoden: natürlich oder nicht, um addTo zu unterstützen, würde jeder Typ diese Methode benötigen und für jede Art von Sammlung bereitstellen, die das Anhängen unterstützt. Das ist wie das beste Beispiel für die Einführung von Abhängigkeiten zwischen allem, was ich jemals gehört habe: P - Ich denke, die falsche Prämisse hier ist der Versuch, eine Programmierabstraktion für das „echte“ Leben zu modellieren. Das geht oft schief.
15.

1
@ t3chb0t Hehe, guter Punkt. Ich habe mich gefragt, ob Ihre erste gesprochene Sprache das Konstrukt beeinflusst, das natürlicher erscheint. Für mich ist das Einzige, was mir ganz natürlich vorkommt add(item, collection), aber das ist kein guter OO-Stil.
Kilian Foth

3
@ t3chb0t In gesprochener Sprache können Sie Dinge normal oder reflexiv lesen. "John, bitte füge diesen Apfel zu dem hinzu, was du bei dir hast." vs "Der Apfel sollte zu dem hinzugefügt werden, was du bei dir hast, John." In der Welt von OOP ist reflexiv wenig sinnvoll. Sie bitten im Allgemeinen nicht darum, dass etwas von einem anderen Objekt beeinflusst wird. Das nächstliegende in OOP ist das Besuchermuster . Es gibt einen Grund, warum dies nicht die Norm ist.
Neil

3
Dies ist eine schöne Verbeugung vor einer schlechten Idee .
Telastyn

3
@ t3chb0t Vielleicht finden Sie im Königreich der Nomen eine interessante Lektüre.
Paul

Antworten:


51

Nein, item.AddTo(items)natürlicher geht es nicht. Ich denke, Sie vermischen dies mit Folgendem:

t3chb0t.Add(item).To(items)

Sie haben Recht damit, dass items.Add(item)es der natürlichen englischen Sprache nicht sehr nahe kommt. Aber Sie hören auch nicht item.AddTo(items)in natürlicher englischer Sprache, oder? Normalerweise gibt es jemanden, der den Artikel zur Liste hinzufügen soll. Sei es bei der Arbeit in einem Supermarkt oder beim Kochen und Hinzufügen von Zutaten.

Im Fall von Programmiersprachen haben wir es so gemacht, dass eine Liste beides tut: Speichern ihrer Elemente und dafür verantwortlich, sie sich selbst hinzuzufügen.

Das Problem bei Ihrer Vorgehensweise ist, dass ein Element wissen muss, dass eine Liste vorhanden ist. Aber ein Gegenstand könnte existieren, selbst wenn es überhaupt keine Listen gäbe, oder? Dies zeigt, dass es Listen nicht berücksichtigen sollte. Listen sollten in ihrem Code überhaupt nicht vorkommen.

Listen existieren jedoch nicht ohne Elemente (zumindest wären sie unbrauchbar). Daher ist es in Ordnung, wenn sie über ihre Artikel Bescheid wissen - zumindest in allgemeiner Form.


4

@valenterry hat völlig recht mit dem Problem der Trennung von Bedenken. Klassen sollten nichts über die verschiedenen Sammlungen wissen, die sie enthalten könnten. Sie müssen die Elementklasse nicht jedes Mal ändern, wenn Sie eine neue Art von Sammlung erstellen.

Das gesagt...

1. Nicht Java

Java ist für Sie hier keine Hilfe, aber einige Sprachen verfügen über eine flexiblere, erweiterbare Syntax.

In Scala sieht items.add (item) folgendermaßen aus:

  item :: items

Wobei :: ein Operator ist, der Elemente zu Listen hinzufügt. Das scheint genau das zu sein, was du willst. Es ist eigentlich syntaktischer Zucker für

  items.::(item)

wo :: ist eine Scala Liste Methode , die effektiv äquivalent zu Java ist list.add . So , während es in der ersten Version sieht aus , als ob Artikel selbst ist das Hinzufügen Artikel , in Wahrheit Artikeln der Zugabe tut. Beide Versionen sind gültige Scala

Dieser Trick erfolgt in zwei Schritten:

  1. Operatoren sind in Scala eigentlich nur Methoden. Wenn Sie Java-Listen in Scala importieren, kann items.add (item) auch als item add item geschrieben werden . In Scala ist 1 + 2 tatsächlich 1. + (2) . In der Version mit Leerzeichen anstelle von Punkten und Klammern sieht Scala das erste Objekt, sucht nach einer Methode, die mit dem nächsten Wort übereinstimmt, und übergibt (wenn die Methode Parameter akzeptiert) das nächste (oder die nächsten) Element (e) an diese Methode.
  2. In Scala sind Operatoren, die mit einem Doppelpunkt enden, eher rechtsassoziativ als links. Wenn Scala die Methode sieht, sucht sie rechts nach dem Methodenbesitzer und gibt den Wert links ein.

Wenn Sie mit vielen Operatoren nicht vertraut sind und Ihr AddTo benötigen , bietet Scala eine weitere Option: implizite Konvertierungen. Dies erfordert zwei Schritte

  1. Erstellen Sie eine Wrapper-Klasse mit dem Namen " ListItem", die ein Element in ihrem Konstruktor annimmt und über eine addTo- Methode verfügt, mit der dieses Element einer bestimmten Liste hinzugefügt werden kann.
  2. Erstellen Sie eine Funktion, die ein Element als Parameter verwendet und ein ListItem mit diesem Element zurückgibt . Markiere es als implizit .

Wenn implizite Konvertierungen aktiviert sind und Scala dies sieht

  item addTo items

oder

  item.addTo(items)

Wenn item kein AddTo hat , durchsucht es den aktuellen Bereich nach impliziten Konvertierungsfunktionen. Wenn eine der Umwandlungsfunktionen einen Typ zurückgibt , die tut eine haben addTo Methode, dies geschieht:

  ListItem.(item).addTo(items)

Möglicherweise entspricht der Weg impliziter Konvertierungen eher Ihrem Geschmack, erhöht jedoch die Gefahr, da Code unerwartete Aktionen ausführen kann, wenn Sie nicht alle impliziten Konvertierungen in einem bestimmten Kontext genau kennen. Aus diesem Grund ist diese Funktion in Scala standardmäßig nicht mehr aktiviert. (Während Sie entscheiden können, ob Sie es in Ihrem eigenen Code aktivieren möchten oder nicht, können Sie nichts gegen den Status in den Bibliotheken anderer tun. Hier sind Drachen).

Die durch Doppelpunkte abgeschlossenen Operatoren sind hässlicher, stellen jedoch keine Bedrohung dar.

Bei beiden Ansätzen wird das Prinzip der Trennung der Anliegen beibehalten. Sie können den Eindruck erwecken , dass der Artikel über das Sammeln Bescheid weiß, die Arbeit jedoch an einem anderen Ort ausgeführt wird. Jede andere Sprache, die Ihnen diese Funktion bieten möchte, sollte mindestens genauso vorsichtig sein.

2. Java

In Java, wo die Syntax von Ihnen nicht erweiterbar ist, können Sie am besten eine Art Wrapper / Decorator-Klasse erstellen, die sich mit Listen auskennt. In der Domäne, in der Ihre Artikel in Listen abgelegt sind, können Sie sie darin einschließen. Wenn sie diese Domain verlassen, nehmen Sie sie raus. Vielleicht ist das Besuchermuster am nächsten.

3. tl, dr

Scala kann Ihnen wie einige andere Sprachen mit einer natürlicheren Syntax behilflich sein. Java kann Ihnen nur hässliche, barocke Muster anbieten.


2

Bei Ihrer Erweiterungsmethode müssen Sie immer noch Add () aufrufen, damit Ihrem Code nichts Nützliches hinzugefügt wird. Sofern Ihre Add-To-Methode keine andere Verarbeitung durchgeführt hat, gibt es keinen Grund, warum sie existiert. Und das Schreiben von Code, der keinen funktionalen Zweck hat, ist schlecht für die Lesbarkeit und Wartbarkeit.

Wenn Sie es nicht generisch lassen, werden die Probleme noch offensichtlicher. Sie haben jetzt ein Element, das über verschiedene Arten von Sammlungen Bescheid wissen muss, in denen es sein kann. Dies verstößt gegen das Prinzip der einmaligen Verantwortung. Ein Objekt ist nicht nur Inhaber seiner Daten, sondern manipuliert auch Sammlungen. Aus diesem Grund behalten wir uns die Verantwortung für das Hinzufügen von Elementen zu Sammlungen bis zu dem Code vor, der beide Elemente belegt.


Wenn ein Framework verschiedene Arten von Objektreferenzen erkennen soll (z. B. Unterscheidung zwischen solchen, die das Eigentum einschließen, und solchen, die dies nicht tun), kann es sinnvoll sein, über ein Mittel zu verfügen, mit dem eine Referenz, die das Eigentum einschließt, das Eigentum an eine Sammlung abgibt, die sich im Besitz befindet fähig, es zu empfangen. Wenn jede Sache auf einen Eigentümer beschränkt ist, ist die Übertragung des Eigentums über thing.giveTo(collection)[und die collectionImplementierung einer "acceptOwnership" -Schnittstelle] sinnvoller, als collectioneine Methode einzuschließen, mit der das Eigentum ergriffen wird. Natürlich ...
Supercat

... die meisten Dinge in Java haben kein Konzept des Eigentums, und eine Objektreferenz kann in einer Sammlung gespeichert werden, ohne das dadurch identifizierte Objekt einzubeziehen.
Supercat

1

Ich denke, Sie sind mit dem Wort "hinzufügen" besessen. Versuchen Sie stattdessen, nach einem alternativen Wort wie "collect" zu suchen, das Ihnen eine englischfreundlichere Syntax ohne Änderung des Musters bietet:

items.collect(item);

Die obige Methode ist genau die gleiche wie items.add(item);, mit dem einzigen Unterschied, dass es sich um eine andere Wortwahl handelt, und sie fließt sehr schön und "natürlich" in Englisch.

Manchmal verhindert das bloße Umbenennen von Variablen, Methoden, Klassen usw. das anstrengende Umgestalten des Musters.


collectwird manchmal als Name für Methoden verwendet, die eine Auflistung von Elementen in eine Art singuläres Ergebnis reduzieren (das auch eine Auflistung sein kann). Diese Konvention wurde von der Java Steam API übernommen , was die Verwendung des Namens in diesem Zusammenhang immens populär machte. Ich habe das Wort, das Sie in Ihrer Antwort erwähnt haben, noch nie gesehen. Ich addput
bleibe

@toniedzwiedz, also überlege pushoder enqueue. Der Punkt ist nicht der spezifische Beispielname, sondern die Tatsache, dass ein anderer Name eher dem Wunsch des OP nach einer englischsprachigen Grammatik entspricht.
Brian S

1

Obwohl es für den allgemeinen Fall (wie von anderen erläutert) kein solches Muster gibt, ist der Kontext, in dem ein ähnliches Muster seine Vorzüge hat, eine bidirektionale Eins-zu-Viele-Zuordnung in ORM.

In diesem Zusammenhang hätten Sie zwei Entitäten, sagen wir Parentund Child. Ein Elternteil kann viele Kinder haben, aber jedes Kind gehört einem Elternteil. Wenn die Anwendung erfordert, dass die Zuordnung von beiden Seiten navigierbar ist (bidirektional), haben Sie eine List<Child> childrenin der übergeordneten Klasse und eine Parent parentin der untergeordneten Klasse.

Die Assoziation sollte natürlich immer wechselseitig sein. ORM-Lösungen erzwingen dies normalerweise für Entitäten, die sie laden. Wenn die Anwendung jedoch eine Entität manipuliert (entweder persistent oder neu), muss sie dieselben Regeln durchsetzen. Nach meiner Erfahrung finden Sie am häufigsten eine Parent.addChild(child)Methode, die aufgerufen Child.setParent(parent)wird und nicht für den öffentlichen Gebrauch bestimmt ist. In letzterem finden Sie normalerweise die gleichen Prüfungen, die Sie in Ihrem Beispiel implementiert haben. Die andere Route kann aber auch implementiert werden: Child.addTo(parent)welche Anrufe Parent.addChild(child). Welche Route die beste ist, ist von Situation zu Situation unterschiedlich.


1

In Java, hält eine typische Sammlung nicht Dinge --Es hält Verweise auf Dinge, oder genauer gesagt Kopien von Referenzen zu den Dingen. Im Gegensatz dazu wird die Dot-Member-Syntax im Allgemeinen verwendet, um auf das durch eine Referenz identifizierte Objekt einzuwirken, und nicht auf die Referenz selbst.

Wenn Sie einen Apfel in eine Schüssel geben, ändert dies einige Aspekte des Apfels (z. B. seine Position). Wenn Sie jedoch einen Zettel mit der Aufschrift "Objekt Nr. 29521" in eine Schüssel geben, ändert dies auch dann nichts an dem Apfel, wenn dies der Fall ist Objekt # 29521. Da in Sammlungen Kopien von Referenzen enthalten sind, gibt es kein Java-Äquivalent dazu, einen Zettel mit der Aufschrift "Objekt # 29521" in eine Schüssel zu legen. Stattdessen kopiert man die Nummer 29521 auf einen neuen Zettel und gibt sie in die Schüssel, um eine eigene Kopie zu erstellen. Das Original bleibt davon unberührt.

Da Objektreferenzen (die "Zettel") in Java passiv beobachtet werden können, haben weder die Referenzen noch die dadurch identifizierten Objekte irgendeine Möglichkeit zu wissen, was mit ihnen gemacht wird. Es gibt einige Arten von Sammlungen, die nicht nur eine Reihe von Verweisen auf Objekte enthalten, die möglicherweise nichts von der Existenz der Sammlung wissen, sondern stattdessen eine bidirektionale Beziehung zu den darin eingekapselten Objekten herstellen. Bei einigen solchen Sammlungen kann es sinnvoll sein, eine Methode anzufordern, die sich selbst zu einer Sammlung hinzufügt (insbesondere, wenn ein Objekt jeweils nur zu einer solchen Sammlung gehören kann). Im Allgemeinen passen die meisten Sammlungen jedoch nicht zu diesem Muster und akzeptieren möglicherweise Verweise auf Objekte, ohne dass die durch diese Verweise identifizierten Objekte betroffen sind.


Dieser Beitrag ist ziemlich schwer zu lesen (Textwand). Hätten Sie etwas dagegen bearbeiten sie in eine bessere Form ing?
gnat

@gnat: Ist das besser?
Superkatze

1
@gnat: Entschuldigung - mein Versuch, die Bearbeitung zu veröffentlichen, schlug aufgrund eines Netzwerkproblems fehl. Gefällt es dir jetzt besser
Supercat

-2

item.AddTo(items)wird nicht verwendet, weil es passiv ist. Vgl. "Der Artikel wird zur Liste hinzugefügt" und "Der Raum wird vom Dekorateur bemalt".

Passive Konstruktionen sind in Ordnung, aber zumindest auf Englisch bevorzugen wir aktive Konstruktionen.

Bei der OO-Programmierung spielt das Pardigma eine herausragende Rolle für Schauspieler, nicht für diejenigen, auf die gehandelt wurde items.Add(item). Die Liste ist die Sache, die etwas tut. Es ist der Schauspieler, das ist also der natürlichere Weg.


Es kommt darauf an, wo du stehst ;-) - die Relativitätstheorie - man könnte das gleiche über list.Add(item) ein Element sagen, das zur Liste hinzugefügt wird oder eine Liste fügt ein Element hinzu oder über item.AddTo(list) das Element, das sich selbst zur Liste hinzufügt, oder ich füge das Element hinzu Zur Liste hinzufügen oder wie Sie bereits gesagt haben, wird der Artikel zur Liste hinzugefügt - alle sind korrekt. Die Frage ist also, wer der Schauspieler ist und warum? Ein Gegenstand ist auch ein Schauspieler und er möchte einer Gruppe (Liste) beitreten, nicht die Gruppe möchte diesen Gegenstand haben ;-) Der Gegenstand handelt aktiv, um in einer Gruppe zu sein.
15.

Der Schauspieler ist das, was das Aktive tut. "Wollen" ist eine passive Sache. Bei der Programmierung ändert sich die Liste, wenn ein Element hinzugefügt wird. Das Element sollte sich überhaupt nicht ändern.
Matt Ellen

... obwohl es oft gefällt, wenn es eine ParentEigenschaft hat. Zwar ist Englisch kann nicht Muttersprache , aber soweit ich weiß , will nicht passiv , es sei denn ich will mich oder er will , dass mir etwas tun ; -]
t3chb0t

Welche Aktion führen Sie aus, wenn Sie etwas wollen? Das ist mein Punkt. Es ist nicht unbedingt grammatisch passiv, sondern semantisch. Übergeordnete WRT-Attribute, das ist sicher eine Ausnahme, aber alle Heuristiken haben sie.
Matt Ellen

Aber List<T>(zumindest der in gefundene System.Collections.Generic) aktualisiert überhaupt keine ParentEigenschaft. Wie könnte es sein, wenn es generisch sein soll? Es liegt normalerweise in der Verantwortung des Containers, seinen Inhalt hinzuzufügen.
Arturo Torres Sánchez
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.