Principe de développement DRY (Don’t Repeat Yourself)

DRY pour “Don’t Repeat Yourself” est un principe de programmation visant à exercer les développeurs à reconnaître des duplications puis de trouver le moyen de les supprimer. Ces duplications peuvent se produire évidemment dans le code mais aussi à tout niveau de l’application comme par exemple dans son architecture, dans les tests unitaires ou dans les processus de déploiement. DRY est donc un principe beaucoup général que simplement de la duplication de code.

Andy Hunt et Dave Thomas qui ont été les premiers (dans The Pragmatic Programmer) à énoncer ce principe, partent de l’idée que chaque ligne de code d’une application doit être maintenue. D’autre part, le plupart des interventions sur du code ont pour but d’effectuer de la maintenance plutôt que l’écriture de code nouveau. Ainsi pour corriger une logique répétée, il faut:

  • d’abord, intervenir sur les points de l’application pour chercher où se trouve ces répétitions,
  • ensute, intervenir sur chacune de ces répétitions pour corriger la logique implémentée.

Toutes ces étapes sont d’autant plus couteuses si elles sont réalisées par un développeur qui n’a pas réalisé ces duplications ou qui ne connaît pas assez l’application pour trouver où se trouve ces points de duplications.

Ainsi, d’après Andy Hunt et Dave Thomas, DRY nécessite que:

"Dans un système, toute connaissance doit avoir une représentation unique, 
    non-ambiguë, faisant autorité."

Ne pas dupliquer du code

La première source de duplication de logique provient de la duplication directe de code en copiant-collant des parties de code.

Acquérir certains réflexes permet d’abord d’éviter de dupliquer du code soi-même et ensuite de s’exercer à identifier des duplications quand on lit du code. A part les cas évident, ce n’est pas toujours trivial de remarquer que du code est dupliqué.

Magic string

Les magic strings peuvent être des chaines de caractères qui sont des constantes utilisées directement dans le code:

Par exemple:

public string GetDivisionName(int floor) 
{ 
    if (floor == 0) 
    { 
        return "Accueil"; 
    } 
    else if (floor == 1) 
    { 
        return "Comptabilité"; 
    } 
    else if (floor == 2) 
    { 
        return "Marketing"; 
    } 
    else if (floor == 3) 
    { 
        return "Conception"; 
    } 
    else if (floor == 4) 
    { 
        return "Direction"; 
    } 
    else 
    { 
        throw new InvalidOperationException(string.Format( 
            "L'étage '{0}' n'existe pas.", floor); 
    } 
} 

Les chaînes de caractères “Accueil”, “Comptabilité”, “Marketing”, “Conception” et “Direction” sont des magic strings car elles correspondent à des constantes. Utiliser ce type de constantes dans du code poussent à la duplication.

Par exemple, si on veut faire la fonction symétrique de celle-ci, on peut écrire la fonction suivante en dupliquant les chaînes de caractères:

public int GetFloor(string divisionName) 
{ 
    if (divisionName == "Accueil") 
    { 
        return 0; 
    } 
    else if (divisionName == "Comptabilité") 
    { 
        return 1; 
    } 
    else if (divisionName == "Marketing") 
    { 
        return 2; 
    } 
    else if (divisionName == "Conception") 
    { 
        return 3; 
    } 
    else if (divisionName == "Direction") 
    { 
        return 4; 
    } 
    else 
    { 
        throw new InvalidOperationException(string.Format( 
        "La service '{0}' ne correspond à aucun étage", service); 
    } 
}

Pour éviter cette duplication, on peut, par exemple, ranger les chaines des variables statiques dédiées dans la même classe, dans une autre classe ou même dans un fichier séparé suivant leur portée:

private const string receptionDivisionName = "Accueil"; 
private const string accountingDivisionName = "Comptabilité"; 
private const string marketingDivisionName = "Marketing"; 
private const string conceptionDivisionName = "Conception"; 
private const string directionDivisionName = "Direction"; 
 
public string GetDivisionName(int floor) 
{ 
    if (floor == 0) 
    { 
        return receptionDivisionName; 
    } 
    else if (floor == 1) 
    { 
        return accountingDivisionName; 
    } 
    else if (floor == 2) 
    { 
        return marketingDivisionName; 
    } 
    else if (floor == 3) 
    { 
        return conceptionDivisionName; 
    } 
    else if (floor == 4) 
    { 
        return directionDivisionName; 
    } 
    else 
    { 
        throw new InvalidOperationException(string.Format( 
            "L'étage '{0}' n'existe pas.", floor); 
    } 
} 
 
public int GetFloor(string divisionName) 
{ 
    if (divisionName == receptionDivisionName) 
    { 
        return 0; 
    } 
    else if (divisionName == accountingDivisionName) 
    { 
        return 1; 
    } 
    else if (divisionName == marketingDivisionName) 
    { 
        return 2; 
    } 
    else if (divisionName == conceptionDivisionName) 
    { 
        return 3; 
    } 
    else if (divisionName == directionDivisionName) 
    { 
        return 4; 
    } 
    else 
    { 
        throw new InvalidOperationException(string.Format( 
            "La service '{0}' ne correspond à aucun étage", service); 
    } 
}

Magic number

Les magic numbers sont des constantes qui ont un sens particulier mais ce sens reste caché parce que le code ne le dévoile pas assez facilement.
Dans les fonctions précédentes, les entiers correspondants aux étages correspondent à des magic numbers. On pourrait de la même façon les ranger dans des variables dédiés.

En utilisant un exemple différent:

public string GetMobilePhonePriceRange(decimal price) 
{ 
    if (price < 50.0m) 
    { 
        return "Bon marché"; 
    } 
    else if (price >= 50.0m && price < 130.0m) 
    { 
        return "Moyen de gamme"; 
    } 
    else if (price >= 130.0m && price < 300.0m) 
    { 
        return "Haut de gamme"; 
    } 
    else if (price >= 300.0m) 
    { 
        return "Très haut de gamme"; 
    } 
}

Cette fonction devient:

private const decimal cheapPriceRangeLimit = 50.0m; 
private const decimal midPriceRangeLimit = 130.0m; 
private const decimal highPriceRangeLimit = 300.0m; 
 
public string GetMobilePhonePriceRange(decimal price) 
{ 
    if (price < cheapPriceRangeLimit) 
    { 
        return "Bon marché"; 
    } 
    else if (price >= cheapPriceRangeLimit && price < midPriceRangeLimit) 
    { 
        return "Moyen de gamme"; 
    } 
    else if (price >= midPriceRangeLimit && price < highPriceRangeLimit) 
    { 
        return "Haut de gamme"; 
    } 
    else if (price >= highPriceRangeLimit) 
    { 
        return "Très haut de gamme"; 
    } 
} 

On peut aller plus loin en considérant des gammes de prix:

private const decimal cheapPriceRangeLimit = 50.0m; 
private const decimal midPriceRangeLimit = 130.0m; 
private const decimal highPriceRangeLimit = 300.0m; 
 
private const string cheapPriceLabel = "Bon marché"; 
private const string midPriceLabel = "Moyen de gamme"; 
private const string highPriceLabel = "Haut de gamme"; 
private const string veryHighPriceLabel = "Très haut de gamme"; 
 
private readonly Dictionary<string, Tuple<decimal?, decimal?>> priceRanges = 
    new Dictionary<string, Tuple<decimal?, decimal?>> 
    { 
        {  cheapPriceLabel, new Tuple<decimal?, decimal?>(null, cheapPriceRangeLimit) }, 
        {  midPriceLabel, new Tuple<decimal?, decimal?>(cheapPriceRangeLimit, midPriceRangeLimit) }, 
        {  highPriceLabel, new Tuple<decimal?, decimal?>(midPriceRangeLimit, highPriceRangeLimit) }, 
        {  veryHighPriceLabel, new Tuple<decimal?, decimal?>(highPriceRangeLimit, null) } 
    }; 
 
public string GetMobilePhonePriceRange(decimal price) 
{ 
  return priceRanges.First(r =>  
    { 
      var lowerLimit = r.Value.Item1; 
      var upperLimit = r.Value.Item2; 
      return (!lowerLimit.HasValue && upperLimit.HasValue && price < upperLimit.Value) ¦¦ 
        (lowerLimit.HasValue && upperLimit.HasValue && price >= lowerLimit.Value && price >= upperLimit.Value) ¦¦ 
        (lowerLimit.HasValue && !upperLimit.HasValue && price >= upperLimit.Value); 
    }) 
    .Key; 
}

Duplication de clauses “if”

D’une façon générale la répétition de if est révélateur d’une implémentation qui pourrait être améliorée.

Dans l’exemple utilisé précédemment:

private const string receptionDivisionName = "Accueil"; 
private const string accountingDivisionName = "Comptabilité"; 
private const string marketingDivisionName = "Marketing"; 
private const string conceptionDivisionName = "Conception"; 
private const string directionDivisionName = "Direction"; 
 
private const int mainFloor = 0; 
private const int firstFloor = 1; 
private const int secondFloor = 2; 
private const int thirdFloor = 3; 
private const int fourthFloor = 4; 
 
public string GetDivisionName(int floor) 
{ 
    if (floor == mainFloor) 
    { 
        return receptionDivisionName; 
    } 
    else if (floor == firstFloor) 
    { 
        return accountingDivisionName; 
    } 
    else if (floor == secondFloor) 
    { 
        return marketingDivisionName; 
    } 
    else if (floor == thirdFloor) 
    { 
        return conceptionDivisionName; 
    } 
    else if (floor == fourthFloor) 
    { 
        return directionDivisionName; 
    } 
    else 
    { 
        throw new InvalidOperationException(string.Format( 
            "L'étage '{0}' n'existe pas.", floor); 
    } 
}

Cette fonction comporte beaucoup de répétitions dues aux clauses if. Il est possible de les supprimer facilement:

private readonly Dictionary<int, string> floors = new Dictionary<int, string> 
  { 
    {  mainFloor, receptionDivisionName },  
    {  firstFloor, accountingDivisionName },  
    {  secondFloor, marketingDivisionName },  
    {  thirdFloor, conceptionDivisionName },  
    {  fourthFloor, directionDivisionName }  
  }; 
 
public string GetDivisionName(int floor) 
{ 
    var foundFloor = floors.FirstOrDefault(f => f.Key.Equals(floor)); 
    if (foundFloor == null) 
    { 
        throw new InvalidOperationException(string.Format( 
            "L'étage '{0}' n'existe pas.", floor); 
    } 
 
    return foundFloor.Value; 
}

Duplication de logique

On peut sans s’en rendre compte dupliquer de la logique dans le code.

Par exemple, si on considère les objets suivants:

public class Building 
{ 
  private string Name { get private set; } 
 
  public Building() 
  {} 
} 
 
public class Floor 
{ 
  public string Name { get private set; } 
  public int FloorNumber { get private set; } 
 
  public Floor() 
  {} 
} 
 
public class Branch 
{ 
  public string Name { get private set; } 
 
  public Branch() 
  {} 
} 
 
public class Division 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
 
  public Division() 
  {} 
 
  public string GetDivisionAddress() 
  { ... } 
}

Pour implémenter la fonction Division.GetDivisionAddress() on peut écrire:

public string GetAddress() 
{ 
  string address = string.Empty; 
  if (this.branch != null) 
  { 
    address += string.Format("Branch: {0}", this.branch.Name); 
  } 
  if (this.building != null) 
  { 
    address += string.Format("Building: {0}", this.building.Name); 
  } 
  if (this.floor != null) 
  { 
    address += string.Format("Floor {0}: {1}", this.floor.FloorNumber, 
      this.floor.Name); 
  } 
  return address; 
}

Si on doit créer un nouvel objet représentant un bureau et qu’on souhaite rajouter une adresse dans cet objet, on devra dupliquer certains éléments du code notamment dans la fonction Desk.GetDeskAddress():

public class Desk 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
  public string area; 
 
  public Desk() 
  {} 
 
  public string GetDeskAddress() 
  { 
    string address = string.Empty; 
    if (this.branch != null) 
    { 
      address += string.Format("Branch: {0}", this.branch.Name); 
    } 
    if (this.building != null) 
    { 
      address += string.Format("Building: {0}", this.building.Name); 
    } 
    if (this.floor != null) 
    { 
      address += string.Format("Floor {0}: {1}", this.floor.FloorNumber,  
        this.floor.Name); 
    } 
    if (!string.IsNullOrEmpty(this.area)) 
    { 
      address += string.Format("Area: {0}", this.area); 
    } 
 
    return address; 
  } 
}

Ranger du code dans des classes statiques

Dans l’exemple précédent, les fonctions Division.GetAddress() et Desk.GetDivision() sont dupliquées. Une méthode pour éviter cette duplication, consiste à ranger le code dupliqué dans des classes statiques Helper. Pour cette exemple, on pourrait isoler le code en créent la fonction statique suivante:

public static class Helper 
{ 
  public static string GetAddress(Branch branch, Building building, Floor floor) 
  { 
    string address = string.Empty; 
    if (branch != null) 
    { 
      address += string.Format("Branch: {0}", branch.Name); 
    } 
    if (building != null) 
    { 
      address += string.Format("Building: {0}", building.Name); 
    } 
    if (floor != null) 
    { 
      address += string.Format("Floor {0}: {1}", floor.FloorNumber, 
        this.floor.Name); 
    } 
 
    return address; 
  } 
}

Les appels se font dans les classes Division et Desk, de cette façon:

public class Division 
{ 
  // ... 
 
 
  public string GetDivisionAddress() 
  { 
    return Helper.GetAddress(this.branch, this.building, this.floor); 
  } 
} 
 
public class Desk 
{ 
  // ... 
 
  public string GetDeskAddress() 
  { 
    string address = Helper.GetAddress(this.branch, this.building, this.floor); 
    if (!string.IsNullOrEmpty(this.area)) 
    { 
      address += string.Format("Area: {0}", this.area); 
    } 
 
    return address; 
  } 
}

Cette méthode présent l’inconvénient de coupler la classe statique Helper aux classes Division et Desk. D’autre part, les objets métier Branch, Building et Floor n’ont pas la faculté de savoir comment ils affichent leur propre emplacement. Cette faculté se trouve dans des objets différents comme Division et Desk. Ainsi cette implémentation a plusieurs inconvénients:

  • Couplage fort avec un autre objet Helper.
  • Elle donne trop de responsabilité aux objets Division et Desk puisqu’ils ont la faculté d’afficher des adresses concernant d’autres objets, ce qui casse le principe “Single Responsibility” de SOLID.
  • En cas de modification des propriétés des objets Branch, Building ou Floor, le code des classes Division et Desk devra être modifié, ce qui casse le principe “ouvert/fermé” de SOLID: ouvert aux extensions et fermé aux modifications.

Rétablir les responsabilités

Pour rétablir les responsabilités concernant l’affichage de l’adresse, on peut charger un autre objet d’avoir cette responsabilité. Ce nouvel objet sera uniquement chargé d’afficher une adresse:

public class AddressDisplayer 
{ 
  public string GetAddress(Branch branch, Building building, Floor floor,  
    string area = "") 
  { 
    string address = string.Empty; 
    if (branch != null) 
    { 
      address += string.Format("Branch: {0}", branch.Name); 
    } 
    if (building != null) 
    { 
      address += string.Format("Building: {0}", building.Name); 
    } 
    if (floor != null) 
    { 
      address += string.Format("Floor {0}: {1}", floor.FloorNumber, 
        this.floor.Name); 
    } 
    if (!string.IsNullOrEmpty(this.area)) 
    { 
      address += string.Format("Area: {0}", this.area); 
    } 
 
    return address; 
  } 
}

En injectant cette classe dans les classes Division et Desk, on supprime une partie de la responsabilité de l’affichage de l’adresse:

public class Division 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
 
  public AddressDisplayer addressDisplayer; 
 
  public Division(AddressDisplayer addressDisplayer, Building building, Floor floor, 
    Branch branch) 
  { 
    this.addressDisplayer = addressDisplayer; 
 
    this.building = building; 
    this.branch = branch; 
    this.floor = floor; 
  } 
 
  public string GetDivisionAddress() 
  { 
    return this.addressDisplayer.GetAddress(this.branch, this.building, this.floor); 
  } 
}

De même pour la classe Desk:

public class Desk 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
  public string area; 
 
  public AddressDisplayer addressDisplayer; 
 
  public Desk(AddressDisplayer addressDisplayer, Building building, Floor floor,
    Branch branch, 
    string area) 
  { 
    this.addressDisplayer = addressDisplayer; 
 
    this.building = building; 
    this.branch = branch; 
    this.floor = floor; 
    this.area = area; 
  } 
 
  public string GetDeskAddress() 
  { 
    return this.addressDisplayer.GetAddress(this.branch, this.building, this.floor,
      this.area); 
  } 
}

Le couplage avec la classe statique Helper est supprimée en effectuant une injection de dépendances dans les classes Division et Desk.

Eviter les couplages forts

Pour diminuer davantage le nouveau couplage avec la classe AddressDisplayer, on peut introduire l’interface IAddressDisplayer. De cette façon la dépendance sera vers une interface plutôt qu’une classe:

public interface IAddressDisplayer 
{ 
  string GetAddress(Branch branch, Building building, Floor floor, string area = ""); 
}

On redéfinit alors AddressDisplayer de cette façon:

public class AddressDisplayer : IAddressDisplayer 
{ 
  public string GetAddress(Branch branch, Building building, Floor floor,
    string area = "") 
  { 
    // ... 
  } 
}

Maintenant il suffit d’injecter l’interface IAddressDisplayer plutôt que la classe AddressDisplayer:

public class Division 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
 
  public IAddressDisplayer addressDisplayer; 
 
  public Division(IAddressDisplayer addressDisplayer, Building building, Floor floor,
    Branch branch) 
  { 
    this.addressDisplayer = addressDisplayer; 
 
    // ... 
  } 
 
  // ... 
}

Et:

public class Desk 
{ 
  public Building building; 
  public Floor floor; 
  public Branch branch; 
  public string area; 
 
  public IAddressDisplayer addressDisplayer; 
 
  public Desk(IAddressDisplayer addressDisplayer, Building building, Floor floor,
    Branch branch, string area) 
  { 
    this.addressDisplayer = addressDisplayer; 
 
    // ... 
  } 
 
  // ... 
}

Pour avoir plus de détails sur l’injection de dépendances: Injection de dépendances en utilisant Unity en 10 min.

L’injection de dépendances facilite les tests unitaires

Le gros intérêt d’introduire un couplage vers une interface plutôt que la classe, est de faciliter les tests unitaires des classes Division et Desk. Il sera ainsi plus facile d’utiliser un “Mock” pour la classe AddressDisplayer (un “Mock” peut être facilement appliqué sur une interface, sur une classe abstraite ou sur une classe dont les fonctions à “moquer” sont virtuelles).

Introduire une abstraction

Les classes Division et Desk restent très couplées aux objets Building, Branch et Floor. On peut encore diminuer ce couplage en introduisant la classe abstraite Location:

public abstract class Location 
{ 
  public string Name { get private set; } 
  public int AddressRank { get private set; } 
 
  public Location(string name, int addressRank) 
  { 
    this.Name = name; 
    this.AddressRank = addressRank; 
  } 
 
  public abstract string GetAddress(); 
}

On redéfinit les classes Building, Branch et Floor:

public class Building : Location 
{ 
  public Building(string name, 2) 
    : base(name) 
  {} 
 
  public override string GetAddress() 
  { 
    if (!string.IsNullOrEmpty(this.Name)) 
    { 
      return string.Format("Building: {0}", this.Name); 
    } 
 
    return string.Empty; 
  } 
} 
 
public class Floor : Location 
{ 
  public int FloorNumber { get private set; } 
 
  public Floor(string name, int floorNumber) 
    : base(name, 3) 
  { 
    this.FloorNumber = floorNumber; 
  } 
 
  public override string GetAddress() 
  { 
    if (!string.IsNullOrEmpty(this.Name)) 
    { 
      return string.Format("Floor {0}: {1}", this.FloorNumber, 
        this.Name); 
    } 
 
    return string.Empty; 
  } 
} 
 
public class Branch : Location 
{ 
  public Branch(string name) 
    : base(name, 1) 
  {} 
 
  if (!string.IsNullOrEmpty(this.Name)) 
  { 
    return string.Format("Branch: {0}", this.Name); 
  } 
}

On peut ensuite facilement introduire la classe Area:

public class Area : Location 
{ 
  public Area(string name, 4) 
    : base(name) 
  {} 
 
  public override string GetAddress() 
  { 
    if (!string.IsNullOrEmpty(this.Name)) 
    { 
      return string.Format("Desk area: {0}", this.Name); 
    } 
 
    return string.Empty; 
  } 
}

L’héritage permet de supprimer les dépendances des classes Division et Desk:

public class Desk 
{ 
  public readonly List<Location> locations; 
  public readonly IAddressDisplayer addressDisplayer; 
 
  public Desk(List<Location> locations, IAddressDisplayer addressDisplayer) 
  { 
    this.locations = locations; 
    this.addressDisplayer = addressDisplayer; 
  } 
 
  public string GetDeskAddress() 
  { 
    return this.addressDisplayer.GetAddress(this.locations); 
  } 
} 
 
public class Division 
{ 
  public readonly List<Location> locations; 
  public readonly IAddressDisplayer addressDisplayer; 
 
  public Division(List<Location> locations, IAddressDisplayer addressDisplayer) 
  { 
    this.locations = locations; 
    this.addressDisplayer = addressDisplayer; 
  } 
 
  public string GetDivisionAddress() 
  { 
    return this.addressDisplayer.GetAddress(this.locations); 
  } 
}

On rédifinit la classe AddressDisplayer et l’interface IAddressDisplayer:

public interface IAddressDisplayer 
{ 
  string GetAddress(IEnumerable<Location> locations); 
} 
 
public class AddressDisplayer : IAddressDisplayer 
{ 
  public string GetAddress(IEnumerable<Location> locations) 
  { 
    string address = string.Empty; 
    foreach (var location in locations.OrderBy(l => l.AddressRank) 
    { 
      string locationAddress = location.GetAddress(); 
      if (!string.IsNullOrEmpty(locationAddress)) 
      { 
        if (!string.IsNullOrEmpty(address)) 
        { 
          address += " "; 
        } 
        address += locationAddress; 
      } 
    } 
 
    return address; 
  } 
}

Le couplage entre les classes est donc fortement réduit. D’autre part cette implémentation permet de mieux respecter les principes “Single responsability” et “Open/Close” de SOLID (http://cdiese.fr/principe-de-developpement-oriente-objet-solid/).

Démarche Software Craftmanship

La démarche Software Craftmanship et la réalisation de Katas permet de s’exercer pour acquérir des réflexes visant à limiter des duplications du code ou à indiquer des méthodes de “refactoring” pour supprimer du code dupliqué.

Utiliser des “design patterns”

L’utilisation de “design patterns” permet aussi d’éviter les duplications de logique dans plusieurs objets.

Par exemple dans l’exemple précédent, si souhaite instancier des objets de type Division et Desk, il faudra écrire le code:

IAddressDisplayer addressDisplayer = new AddressDisplayer(); 
 
var itDivisionLocations = new List<Location>  
{ 
  new Branch("Paris"), new Building("La Défense"), new Floor(3, "IT") 
} 
 
Division division = new Division(itDivisionLocations, addressDisplayer); 
 
var ceoDeskLocations = new List<Location> 
{ 
  new Branch("Paris"), new Building("La Défense"), new Floor(36, "Direction"), 
  new Area("Chief headquarter"), 
} 
 
Desk ceoDesk = new Desk(ceoDeskLocations, addressDisplayer);

Ce qui signifie qu’à chaque instanciation de Division ou de Desk, il faudra aussi instancier certaines classes avec certaines informations. Pour éviter de dupliquer cette logique, on peut s’aider des “design patterns” Factory ou Abstract Factory.

Dans le cas de Factory, il suffit de déporter cette logique dans une classe, par exemple:

public class PlaceFactory 
{ 
  private readonly IAddressDisplayer addressDisplayer; 
 
  public PlaceFactory() 
  { 
    this.addressDisplayer = new AddressDisplayer(); 
  } 
 
  public Division CreateDivision(string branchName, string building,  
    int floorNumber, string floorName) 
  { 
    var divisionLocations = new List<Location>  
    { 
      new Branch(branchName), new Building(building),  
      new Floor(floorNumber, floorName) 
    } 
    return new Division(divisionLocations, this.addressDisplayer); 
  } 
 
  public Desk CreateDesk(string branchName, string building,  
    int floorNumber, string floorName, string deskArea) 
  { 
    var deskLocations = new List<Location>  
    { 
      new Branch(branchName), new Building(building),  
      new Floor(floorNumber, floorName), new Area(deskArea) 
    } 
    return new Desk(deskLocations, this.addressDisplayer); 
  } 
}

De même pour afficher les adresses d’une division ou d’un desk, il faut faire des appels différents suivants le type de classe:

Division division = ... 
string divisionAddress = division.GetDivisionAddress(); 
 
Desk desk = ... 
string deskAddress = desk.GetDeskAddress();

On peut utiliser le pattern Strategy pour obtenir les adresses sans se préoccuper de l’appel de la méthode suivant le type de la classe.

Par exemple, si on définit les classes suivantes:

public interface IPlaceAddressDisplayerStrategy 
{ 
  string GetPlaceAddress(); 
} 
 
public class DivisionAddressDisplayerStrategy : IPlaceAddressDisplayerStrategy 
{ 
  private readonly Division division; 
 
 
  public DivisionAddressDisplayerStrategy(Division division) 
  { 
    this.division = division; 
  } 
 
  public string GetPlaceAddress() 
  { 
    return this.division.GetDivisionAddress(); 
  } 
} 
 
public class DeskAddressDisplayerStrategy : IPlaceAddressDisplayerStrategy 
{ 
  private readonly Desk desk; 
 
 
  public DeskAddressDisplayerStrategy(Desk desk) 
  { 
    this.desk = desk; 
  } 
 
  public string GetPlaceAddress() 
  { 
    return this.desk.GetDeskAddress(); 
  } 
}

Si une classe doit afficher des adresses mais ne doit pas se préoccuper du type des objets qui détiennent une adresse, il suffit qu’elle consomme une classe stratégie, dans l’exemple il s’agira d’une classe satisfaisant IPlaceAddressDisplayer.

Par exemple, si on considère la classe AddressShower:

public class AddressShower 
{ 
  public AddressShower() 
  {} 
 
  public void ShowAddress(IPlaceAddressDisplayerStrategy addressDisplayer) 
  { 
    Console.WriteLine(addressDisplayer.GetPlaceAddress()); 
  } 
}

Cette classe ignore le type des objets Division et Desk et n’a pas besoin de les connaître pour afficher une adresse.
AddressShower peut afficher une adresse de cette façon:

AddressShower addressShower = new AddressShower(); 
 
Division division = ... 
var divisionAddressDisplayerStrategy = new DivisionAddressDisplayerStrategy(division); 
addressShower.ShowAddress(divisionAddressDisplayerStrategy); 
 
Desk desk = ... 
var deskAddressDisplayerStrategy = new DeskAddressDisplayerStrategy(desk); 
addressShower.ShowAddress(deskAddressDisplayerStrategy);

Ainsi le pattern Strategy sert aussi éviter des duplications dans le code puisqu’il va permettre d’implémenter la logique d’appel à la bonne méthode suivant le type de classe dans un seul type d’objets.

Automatiser des processus

Le principe DRY ne vise pas seulement à éviter du dupliquer du code, il concerne aussi des processus au sens large. Ainsi ces processus comprennent le fait d’éviter d’effectuer des tâches répétitives.

Test unitaires

Les tests unitaires participent fortement à réduire les tâches répétitives puisqu’ils vont permettre d’appliquer des tests de façon automatique et éviter de les effectuer manuellement.
Des outils comme MsTest, NUnit, moq ou NSubstitute aident à l’exécution de test unitaires.

Intégration continue

Executer des tests unitaires, des tests d’intégration de façon périodique et notifier les développeurs des éventuelles erreurs liées à ces tests permet aussi de réduire les tâches répétitives.
Des outils comme Jenkins ou Teamcity permet d’apporter une automatisation de toutes ces étapes.

Générateurs de code

On peut aussi utiliser des générateurs de code pour s’éviter de dupliquer des objets à la main. Par exemple on peut utiliser la génération T4 de Visual Studio (T4 pour Text Template Transformation Toolkit).

S’aider d’outils pour éviter les duplications

Enfin il existe des outils pour indiquer quelles sont les parties du code qui sont dupliquées. Ce type d’outils permet de traquer les répétitions dans le code, elles seront moins efficaces pour trouver les répétitions dans la logique.
Des outils comme SonarQube permettent d’analyser le code et de donner des indicateurs quant aux duplications du code.

Références

Leave a Reply