Cet article a fait l'objet d'une traduction automatique.

Meilleures pratiques du langage C#

Risques de violation des principes SOLID en C#

Brannon King

Comme le processus d'écriture de logiciels a évolué du domaine théorique pour devenir une véritable discipline de génie, un certain nombre de principes ont vu le jour. Et quand je dis le principe, je me réfère à une fonctionnalité du code informatique qui aide à maintenir la valeur de ce code. Modèle fait référence à un scénario de code commun, qu'elle soit bonne ou mauvaise.

Par exemple, vous pourriez la valeur de code informatique qui fonctionne en toute sécurité dans un environnement multithread. Vous pouvez la valeur code informatique qui n'est pas se bloquer lorsque vous modifiez le code dans un autre emplacement. En effet, vous pouvez beaucoup de qualités utiles de valeur dans votre code d'ordinateur, mais rencontre le contraire sur une base quotidienne.

Il y a eu quelques principes de développement de logiciel fantastique capturés sous l'acronyme solide — unique responsabilité, ouvert pour l'extension et fermé pendant la modification, la substitution de Liskov, Interface ségrégation et injection de dépendance. Vous devriez avoir connaissance de ces principes, que je vais démontrer une variété de c#-profils spécifiques qui violent ces principes. Si vous n'êtes pas familier avec les principes solides, vous pourriez vouloir examiner rapidement avant de continuer. Je vais également supposer quelque familiarité avec les expressions architecturales modèle et ViewModel.

L'acronyme solide et les principes recouvrent ne provient pas avec moi. Je vous remercie, Robert C. Martin, Michael Feathers, Bertrand Meyer, James Coplien et autres, de partager votre sagesse avec le reste d'entre nous. Beaucoup d'autres livres et blogs ont exploré et raffinée de ces principes. J'espère pouvoir aider à amplifier l'application de ces principes.

Ayant côtoyé et formé de nombreux ingénieurs en logiciel junior, j'ai découvert il y a un écart important entre le premier professionnel de codage code durable et efforts. Dans cet article, je vais essayer de combler cette lacune d'une manière enjouée. Les exemples sont un peu idiotes dans le but de vous aider à reconnaître que vous pouvez appliquer les principes solides à toutes les formes de logiciels.

L'environnement de développement professionnel apporte de nombreux défis pour les aspirants ingénieurs logiciel. Vos études vous a enseigné à réfléchir à des problèmes dans une perspective de haut en bas. Tu vas prendre une approche top-down de vos affectations initiales dans le monde des logiciels d'entreprise taille, copieux. Vous trouverez bientôt que votre fonction de niveau supérieur a atteint une taille difficile à manier. Pour faire le moindre changement nécessite la pleine connaissance de l'ensemble du système, et il est peu de le tenir en échec. Principes de logiciels (dont seul un jeu partiel est mentionné ici) aidera à garder la structure de l'étroit de sa fondation.

Le principe de responsabilité unique

Le principe de responsabilité unique est souvent défini comme : Un objet n'ait une raison de changer ; plus le fichier ou classe, le plus difficile ce sera pour y parvenir. Avec cette définition en tête, regardez ce code :

public IList<IList<Nerd>> ComputeNerdClusters(
  List<Nerd> nerds,
  IPlotter plotter = null) {
  ...
foreach (var nerd in nerds) {
    ...
if (plotter != null)
      plotter.Draw(nerd.Location, 
      Brushes.PeachPuff, radius: 10);
    ...
}
  ...
}

Quel est le problème avec ce code ? Logiciel se trouve écrit ou Déboguer ? C'est peut-être que ce code de dessin particulier est réservé aux fins de débogage. C'est bien qu'il est dans un service connu seulement par l'interface, mais il n'appartient pas. La brosse est un bon indice. Aussi belle et répandue selon bouffées de pêche, il est spécifique à la plateforme. C'est en dehors de la hiérarchie des types de ce modèle de calcul. Il existe de nombreuses façons de séparer le calcul et les utilitaires de débogage associées. À tout le moins, vous pouvez exposer les données nécessaires par voie d'héritage ou d'événements. Garder les tests et les vues de test séparé.

Voici un autre exemple défectueux :

class Nerd {
  public int IQ { get; protected set; }
  public double SuspenderTension { get; set; }
  public double Radius { get; protected set; }
  /// <summary>Get books for growing IQ</summary>
  public event Func<Nerd, IBook> InTheMoodForBook;
  /// <summary>Get recommendations for growing Radius</summary>
  public event Func<Nerd, ISweet> InTheMoodForTwink;
  public IList<Nerd> FitNerdsIntoPaddedRoom(
    IList<Nerd> nerds, IList<Point> boundary)
  {
    ...
}
}

Quel est le problème avec ce code ? Il mêle ce qu'on appelle « matières scolaires. » Vous vous souvenez comment vous avez appris sur différents sujets dans différentes classes à l'école ? Il est important de maintenir cette séparation du code — non pas parce qu'ils sont entièrement indépendants, mais comme un effort d'organisation. En général, ne mettez pas tout deux de ces éléments dans la même classe : mathématiques, modèles, grammaire, vues, adaptateurs de physique ou de la plate-forme, code personnalisé et ainsi de suite.

Vous pouvez voir une analogie générale à vous construisez des choses à l'école en sculpture, bois et métal. Ils ont besoin de mesures, analyse, instruction et ainsi de suite. L'exemple précédent mélange math et modèle — FitNerdsIntoPaddedRoom n'appartient pas. Cette méthode pourrait facilement être déplacée vers une classe utilitaire, même un statique. Vous ne devriez pas instancier des modèles dans vos routines de test de mathématiques.

Voici un autre exemple de responsabilités multiples :

class AvatarBotPath
{
  public IReadOnlyList<ISegment> Segments { get; private set; }
  public double TargetVelocity { get; set; }
  public bool IsReverse { get { return TargetVelocity < 0; } }
  ...
}
public interface ISegment // Elsewhere
{
  Point Start { get; }
  Point End { get; }
  ...
}

Ce qui ne va pas ici ? Il y a clairement deux abstractions différentes représentées par un seul objet. L'un d'entre eux concerne traversant une forme, l'autre représente la forme géométrique elle-même. Ceci est fréquent dans le code. Vous avez une représentation et des paramètres spécifiques à l'utilisation séparées qui vont avec cette représentation.

L'héritage est votre ami ici. Vous pouvez déplacer les propriétés TargetVelocity et IsReverse à un héritier et les capturer dans une interface concise de la IHasTravelInfo. Alternativement, vous pouvez ajouter une collection générale des caractéristiques de la forme. Ceux qui ont besoin de vitesse interroge alors la collection de fonctionnalités pour savoir si elle est définie sur une forme particulière. Vous pouvez également utiliser un autre mécanisme de collection aux représentations de paire avec les paramètres de voyage.

le principe Ouvert Fermé

Cela nous amène au principe suivant : ouverte de prolongation, fermée pour la modification. Comment fait-on ? Préférence pas comme ceci :

void DrawNerd(Nerd nerd) {
  if (nerd.IsSelected)
    DrawEllipseAroundNerd(nerd.Position, nerd.Radius);
  if (nerd.Image != null)
    DrawImageOfNerd(nerd.Image, nerd.Position, nerd.Heading);
  if (nerd is IHasBelt) // a rare occurrence
    DrawBelt(((IHasBelt)nerd).Belt);
  // Etc.
}

Ce qui ne va pas ici ? Eh bien, vous devrez modifier cette méthode chaque fois qu'un client a besoin de nouvelles choses affichées — et ils ont toujours besoin de nouvelles choses affichées. Presque chaque nouvelle fonctionnalité du logiciel nécessite une sorte d'élément d'interface utilisateur. Après tout, c'était le manque de quelque chose dans l'interface existante qui a incité la nouvelle demande de fonctionnalité. Le comportement de cette méthode est une bonne idée, mais vous pouvez déplacer ceux si des déclarations dans les méthodes qu'ils garde et il ne fera pas le problème disparaissent.

Vous avez besoin d'un meilleur plan, mais comment ? Quoi il ressemblera ? Eh bien, vous avez un code qui sait comment tirer certaines choses. Pas de problème. Vous avez juste besoin d'un mode opératoire général pour faire correspondre ces choses avec le code pour les attirer. Il se ramène essentiellement à un schéma comme ceci :

readonly IList<IRenderer> _renderers = new List<IRenderer>();
void Draw(Nerd nerd)
{
  foreach (var renderer in _renderers)
    renderer.DrawIfPossible(_context, nerd);
}

Il existe d'autres moyens d'ajouter à la liste des moteurs de rendu. Le point du code, cependant, est d'écrire le dessin de classes (ou classes sur les classes de dessin) qui implémentent une interface connue. Le moteur de rendu doit avoir l'intelligence afin de déterminer si elle peut ou doit tirer quoi que ce soit basé sur son entrée. Par exemple, le code de la ceinture-dessin peut déplacer son propre convertisseur de « ceinture » qui vérifie l'interface et le produit si nécessaire.

Vous pourriez avoir besoin séparer le CanDraw de la méthode Draw, mais qui ne sera pas violer le principe ouvert fermé ou OCP. Le code qui utilise les moteurs de rendu ne devrait pas avoir à changer si vous ajoutez un nouveau moteur de rendu. C'est aussi simple que cela. Aussi, vous devriez être en mesure d'ajouter le nouveau moteur de rendu dans le bon ordre. Alors que j'utilise rendu à titre d'exemple, cela s'applique également aux gère les entrées, le traitement des données et stockage des données. Ce principe a de nombreuses applications par le biais de tous les types de logiciels. Le modèle est plus difficile à imiter dans Windows Presentation Foundation (WPF), mais il est possible. Voir Figure 1 pour une option possible.

Figure 1 exemple de fusion Windows Presentation Foundation convertisseurs dans une seule Source

public abstract class RenderDefinition : ViewModelBase
{
  public abstract DataTemplate Template { get; }
  public abstract Style TemplateStyle { get; }
  public abstract bool SourceContains(object o); // For selectors
  public abstract IEnumerable Source { get; }
}
public void LoadItemsControlFromRenderers(
    ItemsControl control,
    IEnumerable<RenderDefinition> defs) {
  control.ItemTemplateSelector = new DefTemplateSelector(defs);
  control.ItemContainerStyleSelector = new DefStyleSelector(defs);
  var compositeCollection = new CompositeCollection();
  foreach (var renderDefinition in defs)
  {
    var container = new CollectionContainer
    {
      Collection = renderDefinition.Source
    };
    compositeCollection.Add(container);
  }
  control.ItemsSource = compositeCollection;
}

Voici un autre exemple de faute :

class Nerd
{
  public void WriteName(string name)
  {
    var pocketProtector = new PocketProtector();
    WriteNameOnPaper(pocketProtector.Pen, name);
  }
  private void WriteNameOnPaper(Pen pen, string text)
  {
    ...
}
}

Ce qui ne va pas ici ? Les problèmes avec ce code sont vastes et divers. La question principale, que je tiens à souligner est qu'il n'y a aucun moyen de substituer la création de l'instance de PocketProtector. Code comme celui-ci, il est difficile d'écrire à l'attention des héritiers. Vous avez quelques options pour faire face à ce scénario. Vous pouvez modifier le code pour :

  • La méthode WriteName faire virtuelle. Cela nécessiterait également que vous faire WriteNameOnPaper protégé pour atteindre l'objectif de l'instanciation d'un protecteur de poche mis à jour le.
  • Rendre la méthode de WriteNameOnPaper public, mais qui maintiendra la méthode WriteName cassée sur vos héritiers. Ce n'est pas une bonne option à moins que vous vous débarrasser de WriteName, dans lequel cas l'option dévolue en passant une instance de PocketProtector dans la méthode.
  • Ajoutez une méthode virtuelle protégée dont le seul but est de construire la PocketProtector.
  • Affectez à la classe un type générique T est un type de PocketProtector et de construire avec une sorte de fabrique d'objet. Ensuite, vous aurez le même besoin d'injecter de la fabrique d'objet.
  • Passez une instance de PocketProtector à cette classe dans son constructeur ou via une propriété publique, au lieu de construire il au sein de la classe.

La dernière option répertoriée est généralement le meilleur plan, en supposant que vous pouvez réutiliser les PocketProtector. La méthode de création virtuelle est également une option facile et bonne.

Vous devez envisager les méthodes à faire virtuel pour tenir compte de l'OCP. Cette décision est souvent laissée jusqu'à la dernière minute : « Je vais faire les méthodes virtual quand je dois appeler à partir d'un héritier, que je n'ai pas pour le moment. » D'autres peuvent choisir de faire toutes les méthodes virtuelles, dans l'espoir qui permettra aux extenseurs de la capacité de contourner un oubli dans le code initial.

Les deux approches sont trompent. Ils illustrent l'incapacité de s'engager à une interface ouverte. Ayant trop de méthodes virtuelles limite votre capacité de changer le code plus tard. Un manque de méthodes que vous pouvez substituer limite l'extensibilité et la réutilisabilité du code. Qui limite son utilité et la durée de vie.

Voici un autre exemple courant de violations de l'OCP :

class Nerd
{
  public void DanceTheDisco()
  {
    if (this is ChildOfNerd)
            throw new CoordinationException("Can't");
    ...
}
}
class ChildOfNerd : Nerd { ...
}

Ce qui ne va pas ici ? Le Nerd a une référence difficile à son type d'enfant. C'est pénible à voir et une erreur malheureusement courante pour développeurs juniors. Vous pouvez le voir qu'elle viole l'OCP. Vous devrez modifier plusieurs classes pour améliorer ou Refactoriser ChildOfNerd.

Classes de base ne devraient jamais directement faire référence à leurs héritiers. Fonctionnalité de l'héritier n'est alors plus cohérente entre les héritiers. Une excellente façon d'éviter ce conflit est de mettre l'attention des héritiers d'une classe dans des projets séparés. De cette façon la structure de l'arborescence du projet référence empêche ce scénario malheureux.

Cette question n'est pas limitée aux relations parent-enfant. Elle existe avec classes homologues aussi bien. Supposons que vous ayez quelque chose comme ceci :

class NerdsInAnArc
{
  public bool Intersects(NerdsInAnLine line)
  {
    ...
}
  ...
}

Les arcs et les lignes sont généralement des pairs dans la hiérarchie d'objets. Ils ne devraient pas savent tout non héritées des détails intimes sur l'autre, car ces informations sont souvent utilisés pour des algorithmes intersection optimal. Restez libres de modifier l'un sans avoir à changer l'autre. Ceci ouvre à nouveau une infraction de responsabilité unique. Vous stockez des arcs ou leur analyse ? Mettre les opérations d'analyse dans leur propre classe utilitaire.

Si vous avez besoin de cette capacité particulière de la Croix-pairs, alors vous aurez besoin d'introduire une interface appropriée. Suivez cette règle afin d'éviter la confusion de la Croix-entité : Vous devez utiliser le mot clé « is » avec une abstraction au lieu d'une classe concrète. Vous pourriez potentiellement créer une interface IIntersectable ou INerdsInAPattern pour l'exemple, même si vous reporterait probablement encore à certains autre classe utilitaire d'intersection pour analyser les données exposées sur cette interface.

Le principe de Substitution de Liskov

Le principe de Substitution de Liskov définit des lignes directrices pour le maintien de substitution de l'héritier. Passant l'héritier d'un objet à la place de la classe de base ne doit pas briser toutes les fonctionnalités existantes dans la méthode appelée. Vous devriez être capable de remplacer toutes les implémentations d'une interface donnée avec l'autre.

C# ne permet pas de modifier les types de retour ou des types de paramètres en substituant les méthodes (même si le type de retour est un héritier de type de retour dans la classe de base). Donc, il ne sera pas mal avec les violations de substitution les plus fréquentes : contravariance de méthode arguments (butoirs doivent avoir des types de méthodes de parent mêmes ou base) et la covariance des types de retour (des types de retour dans la substitution de méthodes doivent être le même ou un héritier des types de retour dans la classe de base). Cependant, il est courant pour essayer de contourner cette limitation :

class Nerd : Mammal {
  public double Diopter { get; protected set; }
  public Nerd(int vertebrae, double diopter)
    : base(vertebrae) { Diopter = diopter; }
  protected Nerd(Nerd toBeCloned)
    : base (toBeCloned) { Diopter = toBeCloned.Diopter; }
  // Would prefer to return Nerd instead:
  // public override Mammal Clone() { return new Nerd(this); }
  public new Nerd Clone() { return new Nerd(this); }
}

Ce qui ne va pas ici ? Le comportement de l'objet devient lorsqu'elle est appelée avec une référence d'abstraction. La méthode clone nouveau n'est pas virtuelle et donc n'est pas exécutée lorsque vous utilisez une référence de mammifère. Le mot clé new dans le contexte de déclaration de méthode est censé être une caractéristique. Si vous ne contrôlez pas la classe de base, cependant, comment pouvez-vous garantir la bonne exécution ?

C# a quelques solutions de rechange réalisables, même si elles sont encore un peu de mauvais goût. Vous pouvez utiliser une interface générique (quelque chose comme IComparable < T >) pour implémenter explicitement à chaque héritier. Cependant, vous aurez toujours besoin d'une méthode virtuelle qui effectue l'opération de clonage. Vous avez besoin de cela alors votre clone correspond au type dérivé. C# prend également en charge la norme de Liskov sur contravariance des types de retour et de la covariance des arguments de méthode lorsque vous utilisez des événements, mais qui ne vous aidera à changer l'interface exposée via l'héritage de classe.

À en juger par ce code, vous pourriez penser que c# comprend le type de retour dans l'empreinte de la méthode qu'est d'utiliser le résolveur de méthode de classe. C'est faux, vous ne pouvez pas les remplacements multiples avec différents types de retour, mais les mêmes noms et les types d'entrées. Aussi, les contraintes de la méthode sont ignorées pour la résolution de méthode. La figure 2 montre un exemple de code syntaxiquement correct qui ne sera pas compilé en raison de l'ambiguïté de la méthode.

Figure 2 méthode ambigu empreinte

interface INerd {
  public int Smartness { get; set; }
}
static class Program
{
  public static string RecallSomeDigitsOfPi<T>(
    this IList<T> nerdSmartnesses) where T : int
  {
    var smartest = nerdSmartnesses.Max();
    return Math.PI.ToString("F" + Math.Min(14, smartest));
  }
  public static string RecallSomeDigitsOfPi<T>(
    this IList<T> nerds) where T : INerd
  {
    var smartest = nerds.OrderByDescending(n => n.Smartness).First();
    return Math.PI.ToString("F" + Math.Min(14, smartest.Smartness));
  }
  static void Main(string[] args)
  {
    IList<int> list = new List<int> { 2, 3, 4 };
    var digits = list.RecallSomeDigitsOfPi();
    Console.WriteLine("Digits: " + digits);
  }
}

Le code dans Figure 3 montre comment la capacité de remplacer peut-être être rompue. Examiner vos héritiers. L'un d'eux pourrait modifier le champ d'isMoonWalking au hasard. Si cela se produisait, la classe de base court le risque de rater une section critique de nettoyage. Le domaine de l'isMoonWalking doit être privé. Si l'attention des héritiers ont besoin de savoir, il faudrait une propriété d'accesseur Get protégées qui fournit l'accès, mais pas de modification.

Figure 3 un exemple de comment la capacité de remplacement peut-être être rompue

class GrooveControl: Control {
  protected bool isMoonWalking;
  protected override void OnMouseDown(MouseButtonEventArgs e) {
    isMoonWalking = CaptureMouse();
    base.OnMouseDown(e);
  }
  protected override void OnMouseUp(MouseButtonEventArgs e) {
    base.OnMouseUp(e);
    if (isMoonWalking) {
      ReleaseMouseCapture();
      isMoonWalking = false;
    }
  }
}

Sage et parfois pédants programmeurs prendra cela une étape supplémentaires. Sceller les gestionnaires de la souris (ou toute autre méthode qui repose sur ou modifie l'État privé) et laissez l'attention des héritiers Utilisez événements ou autres méthodes virtuelles qui ne sont pas des méthodes must-appel. Le patron de nécessitant un base appel est recevable, mais n'est pas idéal. Nous avons tous oublié d'appeler des méthodes de base prévus à l'occasion. Don' t laisser l'attention des héritiers briser l'état encapsulé.

Substitution de Liskov nécessite également l'attention des héritiers de ne pas jeter de nouveaux types d'exceptions (bien que l'attention des héritiers d'exceptions déjà levées dans la classe de base sont très bien). C# n'a aucun moyen pour faire appliquer ceci.

Le principe de ségrégation Interface

Chaque interface devrait avoir un but spécifique. Vous ne devriez pas forcé d'implémenter une interface lorsque votre objet ne partage pas cet effet. Par extrapolation, l'interface de la plus grande, plus il est probable il comprend les méthodes que pas tous les implémenteurs peuvent atteindre. C'est l'essence du principe de ségrégation d'Interface. Envisager une paire de vieux et common interface de Microsoft .NET Framework :

public interface ICollection<T> : IEnumerable<T> {
  void Add(T item);
  void Clear();
  bool Contains(T item);
  void CopyTo(T[] array, int arrayIndex);
  bool Remove(T item);
}
public interface IList<T> : ICollection<T> {
  T this[int index] { get; set; }
  int IndexOf(T item);
  void Insert(int index, T item);
  void RemoveAt(int index);
}

Les interfaces sont toujours un peu utiles, mais il y a l'hypothèse implicite que si vous utilisez ces interfaces, vous souhaitez modifier les collections. Souvent, celui qui crée ces collectes de données veut empêcher quiconque de modifier les données. Il est effectivement très utile séparer les interfaces sur les sources et les consommateurs.

Beaucoup de magasins de données voudrais partager une interface non inscriptible commune et indexable. Examiner l'analyse de données ou de données, la recherche de logiciels. Ils lisent généralement dans une table de base de données ou de fichier de grosse bûche pour analyse. Modification des données n'a jamais fait partie de l'ordre du jour.

Certes, l'interface IEnumerable devait être l'interface minimale, en lecture seule. Avec l'ajout de méthodes d'extension LINQ , il a commencé à accomplir ce destin. Microsoft a également reconnu l'écart dans les interfaces de collection indexable. La compagnie a abordé cela dans la version 4.5 du .NET Framework avec l'ajout de IReadOnlyList < T >, désormais mis en œuvre par nombreuses collections de cadre.

Vous vous souvenez de ces beautés dans l'ancienne interface ICollection :

public interface ICollection : IEnumerable {
  ...
object SyncRoot { get; }
  bool IsSynchronized { get; }
  ...
}

En d'autres termes, avant que vous pouvez parcourir la collection, vous devez tout d'abord potentiellement verrouiller sur sa SyncRoot. Un certain nombre des héritiers même mis en place ces éléments particuliers explicitement juste pour aider à cacher leur honte à avoir à mettre en œuvre. L'attente dans les scénarios multithreads est devenu que vous verrouillez la collection partout vous utilisez il (plutôt qu'à l'aide de SyncRoot).

La plupart d'entre vous souhaitez encapsuler vos collections afin qu'ils soient accessibles de façon thread-safe. Au lieu d'utiliser foreach, vous devez encapsuler le magasin de données multi-thread et seulement exposer une méthode ForEach qui utilise un délégué à la place. Heureusement, les nouvelles classes de collection telles que les collections simultanées dans le .NET Framework 4 ou les collections immuables maintenant disponibles pour le .NET Framework 4.5 (via NuGet) ont éliminé une grande partie de cette folie.

L'abstraction .NET Stream partage les mêmes défauts d'être beaucoup trop grandes, y compris les éléments de lecture et en écriture et drapeaux de synchronisation. Cependant, il n'inclut pas les propriétés pour déterminer l'accessibilité en écriture : CanRead, CanSeek, CanWrite et ainsi de suite. Comparer si (flux de données.CanWrite) à si (flux est IWritableStream). Pour ceux d'entre vous créez des flux de données qui ne sont pas accessible en écriture, ce dernier est certainement apprécié.

Maintenant, Regardez le code dans Figure 4.

Figure 4 exemple d'inutiles d'initialisation et de nettoyage

// Up a level in the project hierarchy
public interface INerdService {
  Type[] Dependencies { get; }
  void Initialize(IEnumerable<INerdService> dependencies);
  void Cleanup();
}
public class SocialIntroductionsService: INerdService
{
  public Type[] Dependencies { get { return Type.EmptyTypes; } }
  public void Initialize(IEnumerable<INerdService> dependencies)
  { ...
}
  public void Cleanup() { ...
}
  ...
}

Quel est le problème ici ? Votre l'initialisation du service et le nettoyage devraient venir à travers l'un de l'inversion fantastique de conteneurs de contrôle (IoC) couramment disponibles pour le .NET Framework, au lieu d'être réinventé. Pour l'amour de l'exemple, personne ne se soucie d'initialisation et de nettoyage autres que le gestionnaire de service /­conteneur/boostrapper — quel que soit le code montre comment charger vers le haut de ces services. C'est le code qui s'en soucie. Vous ne voulez pas tout le monde l'appel nettoyage prématurément. C# dispose d'un mécanisme appelé implémentation explicite pour aider avec ceci. Vous pouvez implémenter le service plus proprement, comme ceci :

public class SocialIntroductionsService: INerdService
{
  Type[] INerdService.Dependencies { 
    get { return Type.EmptyTypes; } }
  void INerdService.Initialize(IEnumerable<INerdService> dependencies)
  { ...
}
  void INerdService.Cleanup() {       ...
}
  ...
}

En règle générale, vous souhaitez concevoir vos interfaces avec un but autre que de la pure abstraction d'une même classe de béton. Cela vous donne les moyens d'organiser et de s'étendre. Cependant, il y a au moins deux exceptions notables.

Tout d'abord, les interfaces ont tendance à changer moins souvent que leurs implémentations concrètes. Vous pouvez utiliser ceci à votre avantage. Mettre les interfaces dans un assembly séparé. Laisser les consommateurs référencer uniquement l'assembly de l'interface. Il contribue à la vitesse de compilation. Il vous aide à éviter de mettre les propriétés sur l'interface qui n'appartiennent pas (parce que les types de propriétés inappropriés ne sont pas disponibles avec une hiérarchie de projet appropriée). Si les interfaces et les abstractions correspondantes sont dans le même fichier, quelque chose a mal tourné. Interfaces monter dans la hiérarchie du projet tant que les parents de leurs implémentations et pairs des services (ou les abstractions des services) qui les utilisent.

Deuxièmement, par définition, interfaces n'ont pas toutes les dépendances. Par conséquent, ils se prêtent à test au moyen de cadres moqueur/proxy objet d'unité simple. Cela m'amène au prochain et dernier principe.

Le principe d'Inversion de dépendance

Inversion de dépendance signifie à dépendre des abstractions au lieu de types concrets. Il y a beaucoup de chevauchement entre ce principe et les autres déjà discuté. La plupart des exemples précédents incluent un échec à dépendre des abstractions.

Dans son livre, "Domain Driven Design" (Addison-Wesley Professional, 2003), Eric Evans décrit certaines classifications d'objet qui sont utiles lors de l'examen d'Inversion de dépendance. Pour résumer le livre, il est utile de classer votre objet dans l'une de ces trois groupes : valeurs, des entités ou des services.

Valeurs se réfèrent à des objets sans dépendances qui sont généralement transitoires et immuable. Ils ne sont généralement pas abstrait et vous pouvez les créer à volonté. Cependant, il y rien de mal à faire abstraction, surtout si vous pouvez obtenir tous les avantages d'abstractions. Certaines valeurs peuvent se transformer en entités au fil du temps. Entités sont vos modèles d'affaires et les ViewModels. Elles sont construites à partir des types valeur et les autres entités. Il est utile d'avoir des abstractions pour ces éléments, surtout si vous avez un ViewModel qui représente plusieurs variantes différentes d'un modèle ou vice versa. Les services sont les classes qui contiennent, d'organisent, de service et d'utilisent les entités.

Avec ce classement à l'esprit, Inversion de dépendance porte essentiellement sur les services et les objets qui en ont besoin. Méthodes spécifiques au service devraient toujours être saisies dans une interface. Chaque fois que vous avez besoin d'accéder à ce service, vous y accéder via l'interface. Don' t utiliser un type de service concrète dans votre code n'importe où autre que lorsque le service est construit.

Services dépendent généralement d'autres services. Certains ViewModels dépendent des services, surtout le conteneur et le services d'usine-type. Par conséquent, les services sont généralement difficiles à instancier pour tester car vous avez besoin de l'arborescence complète des services. Abstract, leur essence par une interface. Puis toutes les références aux services devraient être faits par le biais de cette interface, donc ils peuvent être facilement raillés pour des fins de test.

Vous pouvez créer des abstractions n'importe quel niveau dans le code. Lorsque vous trouvez penser, "Wow, il va être douloureux pour A prendre en charge interface de B et B à l'interface de soutien A", c'est le moment idéal pour introduire une nouvelle abstraction au milieu. Rendre des interfaces utilisables et compter sur eux.

Les patrons de l'adaptateur et le médiateur peuvent vous aider à être conforme à l'interface privilégiée. On dirait que les abstractions supplémentaires apportent un code supplémentaire, mais en général ce n'est pas vrai. Prend des mesures partielles vers l'interopérabilité vous aide à organiser le code qui serait ont dû exister pour A et B à parler entre eux de toute façon.

Ans, j'ai lu qu'un développeur doit « toujours réutiliser du code. » À l'époque, il semblait trop simple. Je ne pouvais pas croire que tel un mantra simple pourrait pénétrer les spaghettis sur tout mon écran. Au fil du temps, cependant, j'ai appris. Examinez le code ici :

private readonly IRamenContainer _ramenContainer; // A dependency
public bool Recharge()
{
  if (_ramenContainer != null)
  {
    var toBeConsumed = _ramenContainer.Prepare();
    return Consume(toBeConsumed);
  }
  return false;
}

Ne voyez-vous pas n'importe quel code répétée ? Il y a le double lire sur _ramenContainer. D'un point de vue technique, le compilateur Ceci éliminera avec une optimisation appelée « élimination de la sous-expression commune. » Pour discussion, supposons que vous exécutiez une situation multi-thread et le compilateur a répété en fait classe champ lit dans la méthode. Vous courrait le risque que votre variable de classe est changé à null, avant qu'il est encore utilisé.

Comment réparer cela ? Introduire une référence locale au-dessus de la si instruction. Ce réarrangement nécessite que vous ajoutez un nouvel élément égale ou supérieure à la portée externe. Le principe est le même dans l'organisation de votre projet ! Lorsque vous réutilisez le code ou les abstractions, vous arrivez finalement à une portée utile dans votre hiérarchie de projet. Laissez les dépendances à conduire la hiérarchie de référence entre projets.

Maintenant, regardez ce code :

public IList<Nerd> RestoreNerds(string filename)
{
  if (File.Exists(filename))
  {
    var serializer = new XmlSerializer(typeof(List<Nerd>));
    using (var reader = new XmlTextReader(filename))
      return (List<Nerd>)serializer.Deserialize(reader);
  }
  return null;
}

Est-ce selon les abstractions ?

Non, ce n'est pas. Elle commence par une référence statique au système de fichiers. Il utilise un désérialiseur codé en dur avec des références de type codé en dur. Elle prévoit que la gestion des exceptions pour se produire en dehors de la classe. Ce code est impossible de tester sans le code de stockage associé.

En règle générale, vous cela déplacerait dans deux abstractions : un pour le format de stockage et un pour le support de stockage. Quelques exemples de formats de stockage des données XML, JSON et Protobuf binaires. Supports de stockage incluent les fichiers directes sur un disque et bases de données. Une troisième abstraction est également typique dans ce type de système : une sorte de changement rarement memento qui représente l'objet à stocker.

Examinons cet exemple :

class MonsterCardCollection
{
  private readonly IMsSqlDatabase _storage;
  public MonsterCardCollection(IMsSqlDatabase storage)
  {
    _storage = storage;
  }
  ...
}

Vous voyez quelque chose de mal avec ces dépendances ? L'indice est dans le nom de dépendance. Il est spécifique à la plateforme. Le service n'est pas spécifique à la plate-forme (ou au moins, elle tente d'éviter une dépendance de plate-forme à l'aide d'un moteur de stockage externe). Il s'agit d'une situation où vous avez besoin d'utiliser le modèle d'adaptateur.

Lorsque les dépendances sont spécifiques à la plateforme, les dépendants finira avec leur propre code spécifique à la plateforme. Vous pouvez éviter cela avec une couche supplémentaire. La couche supplémentaire vous aidera à organiser les projets de telle sorte que la mise en oeuvre de la plate-forme spécifique existe dans son propre projet spécial (avec toutes ses références spécifiques à la plateforme). Vous devrez simplement référencer le projet contenant tout le code spécifique à la plate-forme par le projet d'application de démarrage. Les wrappers de plate-forme sont plutôt de grosses ; ne pas dupliquer plus que nécessaire.

Inversion de dépendance regroupe l'ensemble des principes discutés dans cet article. Il utilise des abstractions propres, tenace, que vous pouvez remplir avec des implémentations concrètes qui ne cassent pas l'état de service sous-jacent. Voilà l'objectif.

En effet, les principes solides sont généralement qui se chevauchent dans leurs effets sur le code informatique durable. Le vaste monde de code intermédiaire (c'est-à-dire facilement décompilé) est fantastique dans sa capacité à révéler toute l'étendue à laquelle vous pouvez prolonger de n'importe quel objet. Un certain nombre de projets de bibliothèques .NET s'estomper avec le temps. C'est pas parce que l'idée était défectueuse ; ils ne pouvaient pas étendre juste en toute sécurité des besoins imprévus et variables de l'avenir. Fiers de votre code. Appliquer les principes solides et vous verrez la durée de vie de votre code augmenter.

B. Brannon King a travaillé comme développeur à temps plein pendant 12 ans, dont huit ont été dépensés en c# et le .NET Framework. Son œuvre la plus récente a été avec autonome Solutions Inc. (ASI) près de Logan, Utah (asirobots.com). ASI est unique dans sa capacité à favoriser un amour contagieux de c# ; l'équipage à ASI prend passion pleinement utilisant la langue et en poussant le .NET Framework à ses limites. Le contacter au countprimes@gmail.com.

Je remercie les experts techniques suivants d'avoir relu cet article : Max Barfuss (ASI) et Brian Pepin (Microsoft)
Brian Pepin a travaillé comme ingénieur logiciel chez Microsoft Corporation depuis 1994, en se concentrant principalement sur les outils et les API de développement. Il a travaillé sur la Visual Basic, Java, .NET Framework, Windows Forms, WPF, Silverlight et le concepteur de Windows 8 XAML dans Visual Studio. Actuellement, il travaille sur l'équipe Xbox mettant l'accent sur les composants de système d'exploitation de Xbox et aime passer du temps libre dans la région de Seattle avec son épouse Danna et fils Cole.
Max Barfuss est un artisan de logiciel dédié à la croyance que les bonnes habitudes de codage, la conception et la communication sont les choses qui distinguent les grands ingénieurs du reste. Il a seize ans d'expérience de développement de logiciels, dont onze ans dans les terres de .NET.