Meilleures pratiques pour renvoyer un objet en lecture seule

11

J'ai une question sur les "meilleures pratiques" concernant la POO en C # (mais elle s'applique en quelque sorte à tous les langages).

Envisagez d'avoir une classe de bibliothèque avec un objet qui doit être exposé au public, par exemple via un accesseur de propriété, mais nous ne voulons pas que le public (les personnes utilisant cette classe de bibliothèque) le modifie.

class A
{
    // Note: List is just example, I am interested in objects in general.
    private List<string> _items = new List<string>() { "hello" }

    public List<string> Items
    {
        get
        {
            // Option A (not read-only), can be modified from outside:
            return _items;

            // Option B (sort-of read-only):
            return new List<string>( _items );

            // Option C, must change return type to ReadOnlyCollection<string>
            return new ReadOnlyCollection<string>( _items );
        }
    }
}

Évidemment, la meilleure approche est "l'option C", mais très peu d'objets ont une variante ReadOnly (et certainement aucune classe définie par l'utilisateur ne l'a).

Si vous étiez l’utilisateur de la classe A, vous attendriez-vous à des changements

List<string> someList = ( new A() ).Items;

se propager à l'objet original (A)? Ou est-il correct de renvoyer un clone à condition qu'il ait été écrit ainsi dans les commentaires / documentation? Je pense que cette approche clone pourrait conduire à des bugs assez difficiles à suivre.

Je me souviens qu'en C ++, nous pouvions retourner des objets const et vous ne pouviez appeler que des méthodes marquées comme const dessus. Je suppose qu'il n'y a pas une telle fonctionnalité / modèle dans le C #? Sinon, pourquoi ne l'incluraient-ils pas? Je crois que cela s'appelle la constance.

Mais là encore, ma principale question concerne «à quoi vous attendriez-vous» ou comment gérer l'option A vs B.

NeverStopLearning
la source
2
Jetez un œil à cet article sur cet aperçu des nouvelles collections immuables pour .NET 4.5: blogs.msdn.com/b/bclteam/archive/2012/12/18/… Vous pouvez déjà les obtenir via NuGet.
Kristof Claes

Réponses:

10

Outre la réponse de @ Jesse, qui est la meilleure solution pour les collections: l'idée générale est de concevoir votre interface publique de A de manière à ce qu'elle ne revienne que

  • types de valeurs (comme int, double, struct)

  • objets immuables (comme "chaîne" ou classes définies par l'utilisateur qui n'offrent que des méthodes en lecture seule, tout comme votre classe A)

  • (seulement si vous devez): des copies d'objets, comme le montre votre "Option B"

IEnumerable<immutable_type_here> est immuable en soi, il ne s'agit donc que d'un cas particulier de ce qui précède.

Doc Brown
la source
19

Notez également que vous devez programmer des interfaces et, dans l'ensemble, ce que vous souhaitez retourner à partir de cette propriété est un IEnumerable<string>. A List<string>est un peu un non-non car il est généralement modifiable et j'irai jusqu'à dire qu'en ReadOnlyCollection<string>tant qu'implémentateur de, il ICollection<string>semble qu'il viole le principe de substitution de Liskov.

Jesse C. Slicer
la source
6
+1, utiliser un IEnumerable<string>est exactement ce que l'on veut ici.
Doc Brown
2
Dans .Net 4.5, vous pouvez également utiliser IReadOnlyList<T>.
svick
3
Remarque pour les autres parties intéressées. Lorsque vous considérez l'accesseur de propriété: si vous retournez simplement IEnumerable <string> au lieu de List <string> en faisant return _list (+ transtypage implicite en IEnumerable), cette liste peut être convertie en List <string> et modifiée et les modifications seront se propager dans l'objet principal. Vous pouvez renvoyer une nouvelle liste <string> (_list) (+ transtypage implicite ultérieur en IEnumerable) qui protégera la liste interne. Plus d'informations à ce sujet ici: stackoverflow.com/questions/4056013/…
NeverStopLearning
1
Est-il préférable d'exiger que tout destinataire d'une collection qui doit y accéder par index soit prêt à copier les données dans un type qui facilite cela, ou est-il préférable d'exiger que le code renvoyant la collection la fournisse sous une forme qui est accessible par index? À moins que le fournisseur ne soit susceptible de l'avoir sous une forme qui ne facilite pas l'accès par index, je considérerais que la valeur de libérer les destinataires de la nécessité de faire leur propre copie redondante dépasserait la valeur de libérer le fournisseur d'avoir à fournir un formulaire d'accès aléatoire (par exemple en lecture seule IList<T>)
supercat
2
Une chose que je n'aime pas dans IEnumerable est que vous ne savez jamais s'il fonctionne comme une coroutine ou s'il s'agit simplement d'un objet de données. S'il s'exécute comme une coroutine, cela peut entraîner des bogues subtils, c'est donc bon à savoir parfois. C'est pourquoi j'ai tendance à préférer ReadOnlyCollection ou IReadOnlyList etc.
Steve Vermeulen
3

J'ai rencontré un problème similaire il y a quelque temps et ma solution était en dessous, mais cela ne fonctionne vraiment que si vous contrôlez également la bibliothèque:


Commencez avec votre classe A

public class A
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Dérivez ensuite une interface de ceci avec seulement la partie get des propriétés (et toutes les méthodes de lecture) et ayez un implémentation qui

public interface IReadOnlyA
{
    public int N { get; }
}
public class A : IReadOnlyA
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Bien sûr, lorsque vous souhaitez utiliser la version en lecture seule, une simple conversion suffit. C'est exactement ce que votre solution C est, vraiment, mais j'ai pensé offrir la méthode avec laquelle je l'ai réalisée

Andy Hunt
la source
2
Le problème avec cela est qu'il est possible de le restituer à une version mutable. Et sa violation de LSP, l'état de bacause observable via les méthodes de la classe de base (dans ce cas, l'interface) peut être modifié par de nouvelles méthodes de la sous-classe. Mais elle communique au moins l'intention, même si elle ne la fait pas respecter.
user470365
1

Pour tous ceux qui trouvent cette question et qui sont toujours intéressés par la réponse - il existe maintenant une autre option qui expose ImmutableArray<T>. Ce sont quelques points pour lesquels je pense que c'est mieux alors IEnumerable<T>ou ReadOnlyCollection<T>:

  • il indique clairement qu'il est immuable (API expressive)
  • il indique clairement que la collection est déjà formée (en d'autres termes, elle n'est pas initialisée paresseusement et il est correct de l'itérer plusieurs fois)
  • si le nombre d'articles est connu, il ne cache pas que le savoir comme le IEnumerable<T>fait
  • puisque le client ne sait pas quelle est l'implémentation de IEnumerableou IReadOnlyCollectil / elle ne peut pas supposer que cela ne change jamais (en d'autres termes, il n'y a aucune garantie que les éléments d'une collection n'ont pas été modifiés alors que la référence à cette collection reste inchangée)

De plus, si APi doit informer le client des modifications apportées à la collection, j'exposerais un événement en tant que membre distinct.

Notez que je ne dis pas que IEnumerable<T>c'est mauvais en général. Je l'emploierais toujours lorsque je passais une collection en argument ou retournais une collection initialisée paresseusement (dans ce cas particulier, il est logique de masquer le nombre d'éléments car il n'est pas encore connu).

Pavels Ahmadulins
la source