web-dev-qa-db-fra.com

Programmation orientée objet - comment éviter la duplication dans des processus qui diffèrent légèrement en fonction d'une variable

Quelque chose qui revient beaucoup dans mon travail actuel est qu'il y a un processus généralisé qui doit se produire, mais ensuite la partie étrange de ce processus doit se produire légèrement différemment en fonction de la valeur d'une certaine variable, et je ne suis pas Je suis sûr que c'est la façon la plus élégante de gérer cela.

Je vais utiliser l'exemple que nous avons habituellement, qui fait les choses légèrement différemment selon le pays avec lequel nous traitons.

J'ai donc une classe, appelons-la Processor:

public class Processor
{
    public string Process(string country, string text)
    {
        text.Capitalise();

        text.RemovePunctuation();

        text.Replace("é", "e");

        var split = text.Split(",");

        string.Join("|", split);
    }
}

Sauf que seules certaines de ces actions doivent se produire pour certains pays. Par exemple, seuls 6 pays nécessitent l'étape de capitalisation. Le personnage sur lequel se diviser peut changer selon le pays. Remplacement de l'accent 'e' peut être requis uniquement en fonction du pays.

Évidemment, vous pouvez le résoudre en faisant quelque chose comme ceci:

public string Process(string country, string text)
{
    if (country == "USA" || country == "GBR")
    {
        text.Capitalise();
    }

    if (country == "DEU")
    {
        text.RemovePunctuation();
    }

    if (country != "FRA")
    {
        text.Replace("é", "e");
    }

    var separator = DetermineSeparator(country);
    var split = text.Split(separator);

    string.Join("|", split);
}

Mais lorsque vous traitez avec tous les pays possibles du monde, cela devient très lourd. Et malgré tout, les instructions if rendent la logique plus difficile à lire (du moins, si vous imaginez une méthode plus complexe que l'exemple), et la complexité cyclomatique commence à monter assez rapidement.

Donc en ce moment je fais quelque chose comme ça:

public class Processor
{
    CountrySpecificHandlerFactory handlerFactory;

    public Processor(CountrySpecificHandlerFactory handlerFactory)
    {
        this.handlerFactory = handlerFactory;
    }

    public string Process(string country, string text)
    {
        var handlers = this.handlerFactory.CreateHandlers(country);
        handlers.Capitalier.Capitalise(text);

        handlers.PunctuationHandler.RemovePunctuation(text);

        handlers.SpecialCharacterHandler.ReplaceSpecialCharacters(text);

        var separator = handlers.SeparatorHandler.DetermineSeparator();
        var split = text.Split(separator);

        string.Join("|", split);
    }
}

Gestionnaires:

public class CountrySpecificHandlerFactory
{
    private static IDictionary<string, ICapitaliser> capitaliserDictionary
                                    = new Dictionary<string, ICapitaliser>
    {
        { "USA", new Capitaliser() },
        { "GBR", new Capitaliser() },
        { "FRA", new ThingThatDoesNotCapitaliseButImplementsICapitaliser() },
        { "DEU", new ThingThatDoesNotCapitaliseButImplementsICapitaliser() },
    };

    // Imagine the other dictionaries like this...

    public CreateHandlers(string country)
    {
        return new CountrySpecificHandlers
        {
            Capitaliser = capitaliserDictionary[country],
            PunctuationHanlder = punctuationDictionary[country],
            // etc...
        };
    }
}

public class CountrySpecificHandlers
{
    public ICapitaliser Capitaliser { get; private set; }
    public IPunctuationHanlder PunctuationHanlder { get; private set; }
    public ISpecialCharacterHandler SpecialCharacterHandler { get; private set; }
    public ISeparatorHandler SeparatorHandler { get; private set; }
}

Et je ne suis pas vraiment sûr d’aimer. La logique est encore quelque peu obscurcie par toute la création d'usine et vous ne pouvez pas simplement regarder la méthode d'origine et voir ce qui se passe lorsqu'un processus "GBR" est exécuté, par exemple. Vous finissez également par créer de nombreuses classes (dans des exemples plus complexes que celui-ci) dans le style GbrPunctuationHandler, UsaPunctuationHandler, etc ... ce qui signifie que vous devez regarder plusieurs classes différentes pour comprendre toutes les actions possibles qui pourraient se produire lors du traitement de la ponctuation. Évidemment, je ne veux pas d'une classe géante avec un milliard d'instructions if, mais 20 classes avec une logique légèrement différente semblent également maladroites.

Fondamentalement, je pense que je me suis retrouvé dans une sorte de nœud OOP et je ne sais pas très bien comment le démêler. Je me demandais s'il y avait un modèle qui pourrait aider à ce type de processus?

64
John Darvill

Je me demandais s'il y avait un modèle qui pourrait aider avec ce type de processus

Chaîne de responsabilité est le genre de chose que vous recherchez peut-être mais en OOP est un peu lourd ...

Qu'en est-il d'une approche plus fonctionnelle avec C #?

using System;


namespace Kata {

  class Kata {


    static void Main() {

      var text = "     testing this thing for DEU          ";
      Console.WriteLine(Process.For("DEU")(text));

      text = "     testing this thing for USA          ";
      Console.WriteLine(Process.For("USA")(text));

      Console.ReadKey();
    }

    public static class Process {

      public static Func<string, string> For(string country) {

        Func<string, string> baseFnc = (string text) => text;

        var aggregatedFnc = ApplyToUpper(baseFnc, country);
        aggregatedFnc = ApplyTrim(aggregatedFnc, country);

        return aggregatedFnc;

      }

      private static Func<string, string> ApplyToUpper(Func<string, string> currentFnc, string country) {

        string toUpper(string text) => currentFnc(text).ToUpper();

        Func<string, string> fnc = null;

        switch (country) {
          case "USA":
          case "GBR":
          case "DEU":
            fnc = toUpper;
            break;
          default:
            fnc = currentFnc;
            break;
        }
        return fnc;
      }

      private static Func<string, string> ApplyTrim(Func<string, string> currentFnc, string country) {

        string trim(string text) => currentFnc(text).Trim();

        Func<string, string> fnc = null;

        switch (country) {
          case "DEU":
            fnc = trim;
            break;
          default:
            fnc = currentFnc;
            break;
        }
        return fnc;
      }
    }
  }
}

REMARQUE: il ne doit pas nécessairement être entièrement statique. Si la classe Process a besoin d'un état, vous pouvez utiliser une classe instanciée ou une fonction partiellement appliquée;).

Vous pouvez créer le processus pour chaque pays au démarrage, stocker chacun dans une collection indexée et les récupérer si nécessaire avec un coût O(1)).

0
jlvaquero

Je pense que les informations sur les pays devraient être conservées dans les données, pas dans le code. Ainsi, au lieu d'une classe CountryInfo ou d'un dictionnaire CapitalisationApplicableCountries, vous pourriez avoir une base de données avec un enregistrement pour chaque pays et un champ pour chaque étape de traitement, puis le traitement pourrait parcourir les champs d'un pays donné et traiter en conséquence. La maintenance est alors principalement dans la base de données, avec un nouveau code uniquement nécessaire lorsque de nouvelles étapes sont nécessaires, et les données peuvent être lisibles par l'homme dans la base de données. Cela suppose que les étapes sont indépendantes et n'interfèrent pas entre elles; si ce n'est pas le cas, les choses sont compliquées.

0
Steve J

Je suis désolé d'avoir inventé il y a longtemps le terme "objets" pour ce sujet, car il permet à de nombreuses personnes de se concentrer sur l'idée la moins importante. La grande idée est la messagerie .

~ Alan Kay, Sur la messagerie

Je voudrais simplement implémenter les routines Capitalise, RemovePunctuation etc. en tant que sous-processus qui peuvent être envoyés avec les paramètres text et country, et retournerais un texte traité.

Utilisez des dictionnaires pour regrouper les pays qui correspondent à un attribut spécifique (si vous préférez les listes, cela fonctionnerait également avec seulement un faible coût de performance). Par exemple: CapitalisationApplicableCountries et PunctuationRemovalApplicableCountries.

/// Runs like a pipe: passing the text through several stages of subprocesses
public string Process(string country, string text)
{
    text = Capitalise(country, text);
    text = RemovePunctuation(country, text);
    // And so on and so forth...

    return text;
}

private string Capitalise(string country, string text)
{
    if ( ! CapitalisationApplicableCountries.ContainsKey(country) )
    {
        /* skip */
        return text;
    }

    /* do the capitalisation */
    return capitalisedText;
}

private string RemovePunctuation(string country, string text)
{
    if ( ! PunctuationRemovalApplicableCountries.ContainsKey(country) )
    {
        /* skip */
        return text;
    }

    /* do the punctuation removal */
    return punctuationFreeText;
}

private string Replace(string country, string text)
{
    // Implement it following the pattern demonstrated earlier.
}
0
Igwe Kalu