2011-06-24 1 views
3

Il s'agit d'une décision de conception simple qui semble avoir des passions significatives de chaque côté. J'ai du mal à vraiment comprendre quel design a les conséquences les moins négatives.Décision de conception: Communiquer à partir d'une méthode

J'ai une méthode pour ajouter une banane:

public Banana AddBanana(string name) 
{ 
    // add the banana 
    var _Banana = new Banana { Name = name }; 
    this.Bananas.Add(_Banana); 
    return _Banana; 
} 

Mais je ne peux pas ajouter dans certains cas, une banane, quelque chose comme ceci:

public Banana AddBanana(string name) 
{ 
    // test the request 
    if (this.Bananas.Count > 5) 
     return null; 
    if (this.Bananas.Where(x => x.Name == name).Any()) 
     return null; 
    // add the banana 
    var _Banana = new Banana { Name = name }; 
    this.Bananas.Add(_Banana); 
    return _Banana; 
} 

Maintenant, je veux communiquer à l'appelant Pourquoi ils ne peuvent pas.

Quelle est la meilleure approche?

Approche 1: communiquer avec une exception

public Banana AddBanana(string name) 
{ 
    // test the request 
    if (this.Bananas.Count > 5) 
     throw new Exception("Already 5 Bananas"); 
    if (this.Bananas.Where(x => x.Name == name).Any()) 
     throw new Exception("Banana Already in List"); 
    // add the banana 
    var _Banana = new Banana { Name = name }; 
    this.Bananas.Add(_Banana); 
    return _Banana; 
} 

Approche 2: communiquer avec un test

public Class CanAddBananaResult 
{ 
    public bool Allowed { get; set; } 
    public string Message { get; set; } 
} 

public CanAddBananaResult CanAddBanana(string name) 
{ 
    // test the request 
    if (this.Bananas.Count > 5) 
     return new CanAddBananaResult { 
      Allowed = false, 
      Message = "Already 5 Bananas" 
     }; 
    if (this.Bananas.Where(x => x.Name == name).Any()) 
     return new CanAddBananaResult { 
      Allowed = false, 
      Message = "Banana Already in List" 
     }; 
    return new CanAddBananaResult { Allowed = true }; 
} 

public Banana AddBanana(string name) 
{ 
    // test the request 
    if (!CanAddBanana(name).Allowed) 
     throw new Exception("Cannot Add Banana"); 
    // add the banana 
    var _Banana = new Banana { Name = name }; 
    this.Bananas.Add(_Banana); 
    return _Banana; 
} 

Dans l'approche 1, le consommateur connaît le problème sur la base Exception.Message.

Dans l'approche 2, le consommateur peut empêcher l'exception plutôt que de l'attraper.

Quelle approche est la meilleure dans l'ensemble? Je lis ceci: Classes de conception afin qu'une exception ne soit jamais levée en utilisation normale. Par exemple, une classe FileStream expose une autre façon de déterminer si la fin du fichier a été atteinte. Cela évite l'exception qui est levée si vous lisez après la fin du fichier. Mais l'approche "exceptionnelle" semble être moins le code. Cela signifie-t-il plus simple/meilleur?

+1

n'est pas en mesure d'ajouter une banane un cas exceptionnel ou peut-il se produire en fonctionnement normal? – BrokenGlass

+0

Ce n'est pas exceptionnel, regardez les tests. Dupliquer et limiter. Rien de trop exceptionnel à ce sujet. "Les exceptions sont pour des situations exceptionnelles" est une expression populaire, mais quel est le vrai point de décision ici? –

Répondre

1

Vous devez utiliser les deux.

S'il est possible de déterminer si la méthode est appelée ou non, l'utilisation d'une méthode Can() (ou une propriété si les conditions d'échec ne sont pas spécifiques aux arguments de la méthode) est appropriée et utile . Il y a de nombreux exemples dans le framework: TypeConverter.CanConvertFrom/To vient immédiatement à l'esprit. Toutefois, vous ne pouvez pas garantir qu'un appelant utilisera la méthode que vous lui avez fournie avant d'appeler votre méthode. Donc, vous devez toujours lancer des exceptions appropriées si la méthode est appelée de manière incorrecte (TypeConverter.ConvertFrom lancera dans les mêmes conditions que TypeConverter.CanConvertFrom retournera false).

+0

Et si Can() est cher? –

+0

Vous pouvez toujours l'implémenter et laisser le soin à l'appelant de décider si le coût en vaut la peine. Un appelant peut être en mesure de changer le flux de contrôle ou l'interface utilisateur si une méthode donnée est indisponible, donc être en mesure de déterminer si elle est disponible ou non peut toujours être utile. De toute évidence, cela dépend fortement du contexte. Si vous voulez poster plus de détails sur votre situation spécifique, je peux vous donner une réponse plus détaillée. –

+0

Je suis d'accord avec votre réponse; c'est aussi le design que nous avons choisi. –

0

Je dirais que cela dépend de la situation.

Si les consommateurs de votre classe savent quoi faire lorsqu'une banane ne peut pas être ajoutée, ou si vous ne pouvez pas ajouter de bananes, utilisez la deuxième option. En revanche, si la logique de gestion "can not add banana" est plusieurs couches dans la chaîne d'appel, ou si le fait de ne pas pouvoir traiter les bananes est rare (seulement dans le cas d'une panne de réseau, erreur de logique, erreur de disque, etc.), puis dépendre plus fortement de l'exception - c'est ce qu'ils sont pour!

+0

Je pense que vous devez supposer que les consommateurs ne connaissent jamais. L'équipe de développement d'aujourd'hui pourrait le savoir. Mais le développeur de maintenance de demain ne le fera probablement pas. –

2

La deuxième approche semble inutilement complexe, donc j'ai tendance à préférer la première.

Pour donner à l'appelant la possibilité d'effectuer les contrôles lui-même, je rajouterais les membres suivants:

public bool IsFull 
{ 
    get { return this.Bananas.Count > 5; } 
} 

public bool ContainsBanana(string name) 
{ 
    return this.Bananas.Any(b => b.Name == name); 
} 

je serais probablement revenir aussi la banane existante si le nom existe déjà:

public Banana GetOrAddBanana(string name) 
{ 
    var banana = this.Bananas.FirstOrDefault(b => b.Name == name); 
    if (banana == null) 
    { 
     if (this.IsFull) throw new Exception("Collection is full"); 
     banana = new Banana { Name = name }; 
     this.Bananas.Add(banana); 
    } 
    return banana; 
} 

Si ne pas être en mesure d'ajouter une banane n'est pas un cas exceptionnel, mais peut se produire pendant le fonctionnement normal, j'utiliserais le Try ...modèle:

public bool TryGetOrAddBanana(string name, out Banana banana) 
{ 
    banana = this.Bananas.FirstOrDefault(b => b.Name == name); 
    if (banana == null) 
    { 
     if (this.IsFull) return false; 
     banana = new Banana { Name = name }; 
     this.Bananas.Add(banana); 
    } 
    return true; 
} 
+0

Votre approche est lourde si le nombre de tests comme IsFull/ContainsBanana est une liste super longue OU si elle peut changer avec le temps. C'est aussi juste plus de codage pour le consommateur qu'une seule méthode Can() à tester. Simpler API semble plus convivial pour vos consommateurs; moins susceptibles d'être des erreurs. –

+0

@Jerry Nixon: Votre question est difficile à répondre. Vous semblez avoir un cas d'utilisation très élaboré qui ne se reflète pas dans votre exemple simplifié. J'ai donné une réponse sur la façon dont je concevais votre exemple simplifié, pas votre cas d'utilisation spécifique. La seule réponse générale que je peux donner est: Cela dépend. Cela dépend de l'utilisation de votre classe, de son utilisation, de son évolution, du coût des contrôles, de ce que l'appelant peut faire en cas d'échec d'une opération, des performances requises, etc. Analysez votre cas d'utilisation et choisissez la solution la mieux adaptée. vous avez besoin. – dtb

+0

merci. Je cherche simplement des points de discussion pour aider au processus de prise de décision. Il y a forcément une règle générale quelque part. –

Questions connexes