Existe-t-il un modèle pour une manière plus «naturelle» d'ajouter des éléments aux collections? [fermé]

13

Je pense que la façon la plus courante d'ajouter quelque chose à une collection est d'utiliser une sorte de Addméthode fournie par une collection:

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

et il n'y a en fait rien d'inhabituel à cela.

Je me demande cependant pourquoi ne le faisons-nous pas de cette façon:

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

il semble que ce soit plus naturel que la première méthode. Cela aurait l'andvantange que lorsque la Itemclasse a une propriété comme Parent:

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

vous pouvez rendre le setter privé. Dans ce cas, bien sûr, vous ne pouvez pas utiliser une méthode d'extension.

Mais peut-être que je me trompe et que je n'ai jamais vu ce modèle avant parce que c'est si rare? Savez-vous s'il existe un tel schéma?

Dans C#une méthode d'extension serait utile pour cela

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

Et les autres langues? Je suppose que dans la plupart d'entre eux, la Itemclasse devait fournir une ICollectionIteminterface appelons cela .


Update-1

J'y ai réfléchi un peu plus et ce modèle serait vraiment utile par exemple si vous ne voulez pas qu'un élément soit ajouté à plusieurs collections.

ICollectableinterface de test :

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

et un exemple d'implémentation:

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
}

usage:

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);
t3chb0t
la source
13
item.AddTo(items)supposons que vous ayez un langage sans méthodes d'extension: naturel ou non, pour prendre en charge addTo, chaque type aurait besoin de cette méthode et la fournirait pour chaque type de collection prenant en charge l'ajout. C'est comme le meilleur exemple d'introduction de dépendances entre tout ce que j'ai jamais entendu: P - Je pense que la fausse prémisse ici est d'essayer de modéliser une abstraction de programmation à la «vraie» vie. Cela tourne souvent mal.
stijn
1
@ t3chb0t Hehe, bon point. Je me demandais si votre première langue parlée influence ce qui semble plus naturel. Pour moi, le seul qui semble naturel serait add(item, collection), mais ce n'est pas un bon style OO.
Kilian Foth du
3
@ t3chb0t En langage parlé, vous pouvez lire les choses normalement ou par réflexe. "John, s'il te plaît, ajoute cette pomme à ce que tu portes." vs "La pomme devrait être ajoutée à ce que vous portez, John." Dans le monde de la POO, la réflexion n'a pas beaucoup de sens. Vous ne demandez pas à quelque chose d'être affecté par un autre objet en général. Le modèle le plus proche dans OOP est le modèle de visiteur . Il y a une raison pour laquelle ce n'est pas la norme.
Neil
3
Cela met un joli arc autour d'une mauvaise idée .
Telastyn
3
@ t3chb0t Vous pourriez trouver le Royaume des Noms déclamé une lecture intéressante.
paul

Réponses:

51

Non, item.AddTo(items)ce n'est pas plus naturel. Je pense que vous mélangez cela avec les éléments suivants:

t3chb0t.Add(item).To(items)

Vous avez raison, ce items.Add(item)n'est pas très proche de la langue anglaise naturelle. Mais vous n'entendez pas non plus item.AddTo(items)en anglais naturel, n'est-ce pas ? Normalement, il y a quelqu'un qui est censé ajouter l'élément à la liste. Que ce soit lorsque vous travaillez dans un supermarché ou lorsque vous cuisinez et ajoutez des ingrédients.

Dans le cas des langages de programmation, nous avons fait en sorte qu'une liste fasse les deux: stocker ses éléments et se charger de les ajouter à elle-même.

Le problème avec votre approche est qu'un élément doit être conscient qu'une liste existe. Mais un élément pourrait exister même s'il n'y avait pas de listes du tout, non? Cela montre qu'il ne doit pas être au courant des listes. Les listes ne doivent en aucun cas apparaître dans son code.

Les listes n'existent cependant pas sans éléments (au moins ils seraient inutiles). Par conséquent, c'est bien s'ils connaissent leurs articles - au moins sous une forme générique.

valenterry
la source
4

@valenterry a tout à fait raison sur le problème de la séparation des préoccupations. Les classes ne doivent rien savoir des différentes collections qui pourraient les contenir. Vous ne voudriez pas avoir à modifier la classe d' élément chaque fois que vous créez un nouveau type de collection.

Cela dit...

1. Pas Java

Java ne vous aide pas ici, mais certains langages ont une syntaxe plus flexible et extensible.

Dans Scala, items.add (item) ressemble à ceci:

  item :: items

:: est un opérateur qui ajoute des éléments aux listes. Cela semble très bien ce que vous voulez. C'est en fait du sucre syntaxique pour

  items.::(item)

:: est une méthode de liste Scala qui est effectivement équivalente à list.add de Java . Ainsi, alors qu'il semble dans la première version que l' élément s'ajoute aux éléments , en réalité, les éléments font l'ajout. Les deux versions sont valides Scala

Cette astuce se fait en deux étapes:

  1. Dans Scala, les opérateurs ne sont vraiment que des méthodes. Si vous importez des listes Java dans Scala, puis Items.Add (point) peuvent également être écrits articles ajouter l' article . En Scala, 1 + 2 est en fait 1. + (2) . Dans la version avec des espaces plutôt que des points et des crochets, Scala voit le premier objet, recherche une méthode correspondant au mot suivant et (si la méthode prend des paramètres) passe la ou les choses suivantes à cette méthode.
  2. Dans Scala, les opérateurs qui se terminent par deux points sont associatifs à droite plutôt qu'à gauche. Ainsi, lorsque Scala voit la méthode, il recherche le propriétaire de la méthode à droite et introduit la valeur à gauche.

Si vous n'êtes pas à l'aise avec de nombreux opérateurs et que vous voulez votre addTo , Scala propose une autre option: les conversions implicites. Cela nécessite deux étapes

  1. Créez une classe wrapper appelée, par exemple, ListItem qui prend un élément dans son constructeur et possède une méthode addTo qui peut ajouter cet élément à une liste donnée.
  2. Créez une fonction qui prend un élément comme paramètre et renvoie un ListItem contenant cet élément . Marquez-le comme implicite .

Si les conversions implicites sont activées et que Scala voit

  item addTo items

ou

  item.addTo(items)

et l'élément n'a pas addTo , il recherchera dans la portée actuelle toutes les fonctions de conversion implicites. Si l' une des fonctions de conversion retourne un type qui fait avoir une addTo méthode, cela se produit:

  ListItem.(item).addTo(items)

Maintenant, vous pouvez trouver la route des conversions implicites plus à votre goût, mais cela ajoute du danger, car le code peut faire des choses inattendues si vous n'êtes pas clairement au courant de toutes les conversions implicites dans un contexte donné. C'est pourquoi cette fonctionnalité n'est plus activée par défaut dans Scala. (Cependant, bien que vous puissiez choisir de l'activer ou non dans votre propre code, vous ne pouvez rien faire concernant son statut dans les bibliothèques d'autres personnes. Voici des dragons).

Les opérateurs terminés par deux points sont plus laids mais ne constituent pas une menace.

Les deux approches préservent le principe de la séparation des préoccupations. Vous pouvez donner l' impression que l' élément connaît la collection, mais le travail se fait en fait ailleurs. Toute autre langue qui souhaite vous offrir cette fonctionnalité doit être au moins aussi prudente.

2. Java

En Java, où la syntaxe n'est pas extensible par vous, le mieux que vous puissiez faire est de créer une sorte de classe wrapper / décorateur qui connaît les listes. Dans le domaine où vos articles sont placés dans des listes, vous pouvez les envelopper. Quand ils quittent ce domaine, retirez-les. Peut-être que le modèle de visiteur est le plus proche.

3. tl; dr

Scala, comme certaines autres langues, peut vous aider avec une syntaxe plus naturelle. Java ne peut vous offrir que des motifs laids et baroques.

itsbruce
la source
2

Avec votre méthode d'extension, vous devez toujours appeler Add () afin de ne rien ajouter d'utile à votre code. À moins que votre méthode addto n'ait effectué un autre traitement, il n'y a aucune raison qu'elle existe. Et l'écriture de code qui n'a aucun but fonctionnel est mauvaise pour la lisibilité et la maintenabilité.

Si vous le laissez non générique, les problèmes deviennent encore plus apparents. Vous avez maintenant un élément qui doit connaître les différents types de collections dans lesquelles il peut se trouver. Cela viole le principe de responsabilité unique. Un élément est non seulement un support pour ses données, mais il manipule également les collections. C'est pourquoi nous laissons la responsabilité d'ajouter des éléments aux collections jusqu'au morceau de code qui les consomme tous les deux.

contrôle
la source
Si un cadre devait reconnaître différents types de références d'objets (par exemple, en faisant la distinction entre celles qui encapsulent la propriété et celles qui ne le font pas), il pourrait être judicieux de disposer d'un moyen par lequel une référence qui encapsule la propriété pourrait renoncer à la propriété d'une collection qui était capable de le recevoir. Si chaque chose se limite à avoir un seul propriétaire, le transfert de propriété via thing.giveTo(collection)[et nécessitant la collectionmise en œuvre d'une interface "acceptOwnership"] aurait plus de sens que d'avoir collectioninclus une méthode qui a saisi la propriété. Bien sûr ...
supercat
... la plupart des choses en Java n'ont pas de concept de propriété, et une référence d'objet peut être stockée dans une collection sans impliquer l'objet identifié.
supercat
1

Je pense que vous êtes obsédé par le mot "ajouter". Essayez plutôt de rechercher un autre mot, tel que "collecter", qui vous donnerait une syntaxe plus conviviale en anglais sans changement de modèle:

items.collect(item);

La méthode ci-dessus est exactement la même que items.add(item);, avec la seule différence étant un choix de mots différent, et s'écoule très bien et "naturellement" en anglais.

Parfois, le simple fait de renommer des variables, des méthodes, des classes, etc. empêchera la reconception fiscale du modèle.

Repose en paix
la source
collectest parfois utilisé comme nom pour les méthodes qui réduisent une collection d'éléments en une sorte de résultat singulier (qui peut également être une collection). Cette convention a été adoptée par l' API Java Steam , qui a rendu l'utilisation du nom dans ce contexte extrêmement populaire. Je n'ai jamais vu le mot utilisé dans le contexte que vous mentionnez dans votre réponse. Je préfère m'en tenir à addouput
toniedzwiedz
@toniedzwiedz, alors considérez pushou enqueue. Le point n'est pas le nom d'exemple spécifique, mais le fait qu'un nom différent pourrait être plus proche du désir du PO pour une grammaire anglaise.
Brian S
1

Bien qu'il n'y ait pas un tel modèle pour le cas général (comme expliqué par d'autres), le contexte dans lequel un modèle similaire a ses avantages est une association un-à-plusieurs bidirectionnelle dans ORM.

Dans ce contexte, vous auriez deux entités, disons Parentet Child. Un parent peut avoir plusieurs enfants, mais chaque enfant appartient à un parent. Si l'application nécessite que l'association soit navigable des deux côtés (bidirectionnelle), vous auriez un List<Child> childrendans la classe parent et un Parent parentdans la classe enfant.

L'association doit toujours être réciproque bien sûr. Les solutions ORM appliquent généralement cela aux entités qu'elles chargent. Mais lorsque l'application manipule une entité (persistante ou nouvelle), elle doit appliquer les mêmes règles. D'après mon expérience, vous trouverez le plus souvent une Parent.addChild(child)méthode qui invoque Child.setParent(parent), qui n'est pas à l'usage du public. Dans ce dernier, vous trouverez généralement les mêmes vérifications que celles que vous avez mises en œuvre dans votre exemple. L'autre itinéraire peut cependant aussi être implémenté: Child.addTo(parent)qui appelle Parent.addChild(child). Le meilleur itinéraire diffère d'une situation à l'autre.

Maarten Winkels
la source
1

En Java, une collection typique ne contient pas de choses - elle contient des références à des choses, ou plus précisément des copies de références à des choses. La syntaxe des membres de points, en revanche, est généralement utilisée pour agir sur la chose identifiée par une référence, plutôt que sur la référence elle-même.

Alors que placer une pomme dans un bol modifierait certains aspects de la pomme (par exemple, son emplacement), placer une feuille de papier qui dit "objet # 29521" dans un bol ne changerait en rien la pomme, même si elle se trouve être objet # 29521. De plus, parce que les collections contiennent des copies de références, il n'y a pas d'équivalent Java à placer un morceau de papier disant "objet # 29521" dans un bol. Au lieu de cela, on copie le numéro # 29521 sur une nouvelle feuille de papier et le montre (ne le donne pas) au bol, qui les fera faire sa propre copie, en laissant l'original inchangé.

En effet, étant donné que les références d'objets (les "bouts de papier") en Java peuvent être observées passivement, ni les références, ni les objets ainsi identifiés, n'ont de moyen de savoir ce qui en est fait. Il existe certains types de collections qui, au lieu de simplement contenir un tas de références à des objets qui peuvent ne rien savoir de l'existence de la collection, établissent plutôt une relation bidirectionnelle avec les objets qui y sont encapsulés. Avec certaines de ces collections, il peut être judicieux de demander à une méthode de s’ajouter à une collection (surtout si un objet ne peut se trouver que dans une seule collection à la fois). En général, cependant, la plupart des collections ne correspondent pas à ce modèle et peuvent accepter des références à des objets sans aucune implication de la part des objets identifiés par ces références.

supercat
la source
ce post est assez difficile à lire (mur de texte). Cela vous dérangerait de le modifier sous une meilleure forme?
moucher
@gnat: Est-ce mieux?
supercat
1
@gnat: Désolé - un hoquet réseau a fait échouer ma tentative de publication de la modification. Aimez-vous mieux maintenant?
supercat
-2

item.AddTo(items)n'est pas utilisé car il est passif. Cf "L'article est ajouté à la liste" et "la pièce est peinte par le décorateur".

Les constructions passives sont bien, mais, au moins en anglais, nous avons tendance à préférer les constructions actives.

Dans la programmation OO, le pardigme donne la prééminence aux acteurs, pas à ceux sur lesquels on a agi, donc nous l'avons fait items.Add(item). La liste est la chose qui fait quelque chose. C'est l'acteur, c'est donc la voie la plus naturelle.

Matt Ellen
la source
Cela dépend de l'endroit où vous vous situez ;-) - la théorie de la relativité - vous pourriez en dire autant de list.Add(item) un élément est ajouté à la liste ou une liste ajoute un élément ou à propos de item.AddTo(list) l'élément s'ajoute à la liste ou j'ajoute l'élément à la liste ou comme vous l'avez dit, l'élément est ajouté à la liste - tous sont corrects. La question est donc de savoir qui est l'acteur et pourquoi? Un élément est également un acteur et il veut rejoindre un groupe (liste) et non le groupe veut avoir cet élément ;-) l'élément agit activement pour être dans un groupe.
t3chb0t
L'acteur est la chose qui fait la chose active. "Vouloir" est une chose passive. En programmation, c'est la liste qui change lorsqu'un élément lui est ajouté, l'élément ne doit pas du tout changer.
Matt Ellen
... bien qu'il aime souvent quand il a une Parentpropriété. Certes, l'anglais n'est pas la langue maternelle, mais pour autant que je sache, vouloir n'est pas passif à moins que je ne veuille ou qu'il veuille que je fasse quelque chose ; -]
t3chb0t
Quelle action effectuez-vous lorsque vous voulez quelque chose? C'est mon point. Ce n'est pas nécessairement grammaticalement passif mais sémantiquement. Attributs parent WRT, bien sûr, c'est une exception, mais toutes les heuristiques en ont.
Matt Ellen
Mais List<T>(au moins celui trouvé dans System.Collections.Generic) ne met à jour aucune Parentpropriété. Comment pourrait-il, s'il est censé être générique? Il est généralement de la responsabilité du conteneur d'ajouter son contenu.
Arturo Torres Sánchez