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

64

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 selon 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 diviser peut changer selon le pays. Le remplacement de l'accentuation 'e'peut uniquement être requis 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 dans le monde, cela devient très lourd. Et peu importe, les ifinstructions 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 en quelque sorte 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 beaucoup de classes (dans des exemples plus complexes que celui-ci) dans le style GbrPunctuationHandler, UsaPunctuationHandleretc ... ce qui signifie que vous devez regarder plusieurs classes différentes pour comprendre toutes les actions possibles qui pourraient se produire pendant la ponctuation manipulation. Évidemment, je ne veux pas d'une classe géante avec un milliard d' ifinstructions, mais 20 classes avec une logique légèrement différente me 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 avec ce type de processus?

John Darvill
la source
On dirait que vous avez une PreProcessfonctionnalité, qui pourraient être mises en œuvre différemment selon certains pays, DetermineSeparatorpeuvent être là pour tous, et un PostProcess. Tous peuvent être protected virtual voidavec une implémentation par défaut, et vous pouvez alors avoir un Processorspays spécifique
Icepickle
Votre travail consiste à faire dans un délai donné quelque chose qui fonctionne et qui peut être maintenu dans un avenir prévisible, par vous ou quelqu'un d'autre. Si plusieurs options peuvent satisfaire les deux conditions, vous êtes libre de choisir l'une d'entre elles, selon vos goûts.
Dialecticus
2
Une option viable pour vous est d'avoir une configuration. Donc, dans votre code, vous ne vérifiez pas pour un pays spécifique, mais pour une option de configuration spécifique. Mais chaque pays aura un ensemble spécifique de ces options de configuration. Par exemple, au lieu de if (country == "DEU")vous vérifier if (config.ShouldRemovePunctuation).
Dialecticus
11
Si les pays ont différentes options, pourquoi countryune chaîne plutôt qu'une instance d'une classe qui modélise ces options?
Damien_The_Unbeliever
@Damien_The_Unbeliever - pourriez-vous nous en dire un peu plus à ce sujet? La réponse de Robert Brautigam ci-dessous est-elle conforme à ce que vous proposez? - ah peut voir votre réponse maintenant, merci!
John Darvill

Réponses:

53

Je suggère d'encapsuler toutes les options dans une seule classe:

public class ProcessOptions
{
  public bool Capitalise { get; set; }
  public bool RemovePunctuation { get; set; }
  public bool Replace { get; set; }
  public char ReplaceChar { get; set; }
  public char ReplacementChar { get; set; }
  public char JoinChar { get; set; }
  public char SplitChar { get; set; }
}

et passez-le dans la Processméthode:

public string Process(ProcessOptions options, string text)
{
  if(options.Capitalise)
    text.Capitalise();

  if(options.RemovePunctuation)
    text.RemovePunctuation();

  if(options.Replace)
    text.Replace(options.ReplaceChar, options.ReplacementChar);

  var split = text.Split(options.SplitChar);

  string.Join(options.JoinChar, split);
}
Michał Turczyn
la source
4
Je ne sais pas pourquoi quelque chose comme ça n'a pas été essayé avant de sauter à CountrySpecificHandlerFactory... o_0
Mateen Ulhaq
Tant qu'il n'y a pas d'options trop spécialisées, j'irais certainement dans ce sens. Si les options sont sérialisées en fichier texte, cela permet également aux non-programmeurs de définir de nouvelles variantes / de mettre à jour les variantes existantes sans avoir besoin de passer à l'application.
Tom
4
Cela public class ProcessOptionsdevrait vraiment être juste [Flags] enum class ProcessOptions : int { ... }...
Drunken Code Monkey
Et je suppose que s'ils en ont besoin, ils pourraient avoir une carte des pays à ProcessOptions. Très pratique.
theonlygusti
24

Lorsque le framework .NET a tenté de gérer ce type de problèmes, il n'a pas tout modélisé comme string. Vous avez donc, par exemple, la CultureInfoclasse :

Fournit des informations sur une culture spécifique (appelée locale pour le développement de code non managé). Les informations comprennent les noms de la culture, le système d'écriture, le calendrier utilisé, l'ordre de tri des chaînes et le formatage des dates et des nombres.

Maintenant, cette classe peut ne pas contenir les fonctionnalités spécifiques dont vous avez besoin, mais vous pouvez évidemment créer quelque chose d'analogue. Et puis vous changez de Processméthode:

public string Process(CountryInfo country, string text)

Votre CountryInfoclasse peut alors avoir une bool RequiresCapitalizationpropriété, etc., qui aide votre Processméthode à diriger son traitement de manière appropriée.

Damien_The_Unbeliever
la source
13

Peut-être pourriez-vous en avoir un Processorpar pays?

public class FrProcessor : Processor {
    protected override string Separator => ".";

    protected override string ProcessSpecific(string text) {
        return text.Replace("é", "e");
    }
}

public class UsaProcessor : Processor {
    protected override string Separator => ",";

    protected override string ProcessSpecific(string text) {
        return text.Capitalise().RemovePunctuation();
    }
}

Et une classe de base pour gérer les parties communes du traitement:

public abstract class Processor {
    protected abstract string Separator { get; }

    protected virtual string ProcessSpecific(string text) { }

    private string ProcessCommon(string text) {
        var split = text.Split(Separator);
        return string.Join("|", split);
    }

    public string Process(string text) {
        var s = ProcessSpecific(text);
        return ProcessCommon(s);
    }
}

De plus, vous devez retravailler vos types de retour car ils ne se compileront pas comme vous les avez écrits - parfois une stringméthode ne retourne rien.

Corentin Pane
la source
Je suppose que j'essayais de suivre la composition sur le mantra d'héritage. Mais oui, c'est définitivement une option, merci pour la réponse.
John Darvill
C'est suffisant. Je pense que l'héritage est justifié dans certains cas, mais cela dépend vraiment de la façon dont vous prévoyez de charger / stocker / invoquer / changer vos méthodes et votre traitement à la fin.
Corentin Pane
3
Parfois, l'héritage est le bon outil pour le travail. Si vous avez un processus qui se comportera principalement de la même manière dans plusieurs situations différentes, mais qui comporte également plusieurs parties qui se comporteront différemment dans différentes situations, c'est un bon signe que vous devriez envisager d'utiliser l'héritage.
Tanner Swett
5

Vous pouvez créer une interface commune avec une Processméthode ...

public interface IProcessor
{
    string Process(string text);
}

Ensuite, vous l'implémentez pour chaque pays ...

public class Processors
{
    public class GBR : IProcessor
    {
        public string Process(string text)
        {
            return $"{text} (processed with GBR rules)";
        }
    }

    public class FRA : IProcessor
    {
        public string Process(string text)
        {
            return $"{text} (processed with FRA rules)";
        }
    }
}

Vous pouvez ensuite créer une méthode commune pour instancier et exécuter chaque classe liée au pays ...

// also place these in the Processors class above
public static IProcessor CreateProcessor(string country)
{
    var typeName = $"{typeof(Processors).FullName}+{country}";
    var processor = (IProcessor)Assembly.GetAssembly(typeof(Processors)).CreateInstance(typeName);
    return processor;
}

public static string Process(string country, string text)
{
    var processor = CreateProcessor(country);
    return processor?.Process(text);
}

Ensuite, il vous suffit de créer et d'utiliser les processeurs comme ça ...

// create a processor object for multiple use, if needed...
var processorGbr = Processors.CreateProcessor("GBR");
Console.WriteLine(processorGbr.Process("This is some text."));

// create and use a processor for one-time use
Console.WriteLine(Processors.Process("FRA", "This is some more text."));

Voici un exemple de violon dotnet fonctionnel ...

Vous placez tous les traitements spécifiques au pays dans chaque classe de pays. Créez une classe commune (dans la classe Processing) pour toutes les méthodes individuelles réelles, afin que chaque processeur de pays devienne une liste d'autres appels communs, plutôt que de copier le code dans chaque classe de pays.

Remarque: vous devrez ajouter ...

using System.Assembly;

afin que la méthode statique crée une instance de la classe country.

Rétablir Monica Cellio
la source
La réflexion n'est-elle pas remarquablement lente par rapport à l'absence de code réfléchi? ça vaut le coup pour ce cas?
jlvaquero
@jlvaquero Non, la réflexion n'est pas remarquablement lente du tout. Bien sûr, il y a un impact sur les performances lors de la spécification d'un type au moment de la conception, mais c'est vraiment une différence de performances négligeable et seulement perceptible lorsque vous en abusez. J'ai implémenté de grands systèmes de messagerie construits autour de la gestion d'objets génériques et nous n'avons eu aucune raison de remettre en question les performances, et cela avec un débit énorme. Sans différence notable de performances, je choisirai toujours un code simple à maintenir, comme celui-ci.
Rétablir Monica Cellio
Si vous réfléchissez, ne voudriez-vous pas supprimer la chaîne de pays de chaque appel à Process, et l'utiliser à la place une fois pour obtenir le bon processeur IP? Vous traitez généralement beaucoup de texte selon les règles du même pays.
Davislor
@Davislor C'est exactement ce que fait ce code. Lorsque vous l'appelez, Process("GBR", "text");il exécute la méthode statique qui crée une instance du processeur GBR et exécute la méthode Process sur celle-ci. Il ne l'exécute que sur une seule instance, pour ce type de pays spécifique.
Rétablir Monica Cellio
@Archer Droite, donc dans le cas typique où vous traitez plusieurs chaînes selon les règles pour le même pays, il serait plus efficace de créer l'instance une fois - ou de rechercher une instance constante dans une table de hachage / dictionnaire et de retourner une référence à cela. Vous pouvez ensuite appeler la transformation de texte sur la même instance. Créer une nouvelle instance pour chaque appel, puis la supprimer, plutôt que de la réutiliser pour chaque appel, est un gaspillage.
Davislor
3

Il y a quelques versions, le swtich C # a été entièrement pris en charge pour la correspondance de modèles . De sorte que le cas "plusieurs pays correspondent" est facile à faire. Bien qu'il n'ait toujours pas de capacité de chute, une entrée peut faire correspondre plusieurs cas avec la correspondance de motifs. Cela pourrait peut-être rendre ce spam si un peu plus clair.

Npw un commutateur peut généralement être remplacé par une collection. Vous devez utiliser des délégués et un dictionnaire. Le processus peut être remplacé par.

public delegate string ProcessDelegate(string text);

Ensuite, vous pouvez créer un dictionnaire:

var Processors = new Dictionary<string, ProcessDelegate>(){
  { "USA", EnglishProcessor },
  { "GBR", EnglishProcessor },
  { "DEU", GermanProcessor }
}

J'ai utilisé functionNames pour remettre le délégué. Mais vous pouvez utiliser la syntaxe Lambda pour y fournir l'intégralité du code. De cette façon, vous pouvez simplement masquer cette collection entière comme vous le feriez pour toute autre grande collection. Et le code devient une simple recherche:

ProcessDelegate currentProcessor = Processors[country];
string processedString = currentProcessor(country);

Ce sont à peu près les deux options. Vous pouvez envisager d'utiliser des énumérations au lieu de chaînes pour la correspondance, mais c'est un détail mineur.

Christophe
la source
2

J'irais peut-être (selon les détails de votre cas d'utilisation) avec l' Countryidée d'être un "vrai" objet au lieu d'une chaîne. Le mot clé est "polymorphisme".

Donc, fondamentalement, cela ressemblerait à ceci:

public interface Country {
   string Process(string text);
}

Ensuite, vous pouvez créer des pays spécialisés pour ceux dont vous avez besoin. Remarque: vous n'avez pas à créer d' Countryobjet pour tous les pays, vous pouvez en avoir LatinlikeCountry, ou même GenericCountry. Là, vous pouvez collecter ce qui doit être fait, même en réutiliser d'autres, comme:

public class France {
   public string Process(string text) {
      return new GenericCountry().process(text)
         .replace('a', 'b');
   }
}

Ou similaire. Countrypeut-être en fait Language, je ne suis pas sûr du cas d'utilisation, mais je vous comprends.

En outre, la méthode ne doit pas être bien sûr, Process()elle devrait être la chose que vous devez réellement faire. Comme Words()ou peu importe.

Robert Bräutigam
la source
1
J'ai écrit quelque chose de plus verbeux, mais je pense que c'est essentiellement ce que j'aime le plus. Si le cas d'utilisation doit rechercher ces objets en fonction d'une chaîne de pays, il peut utiliser la solution de Christopher avec cela. L'implémentation des interfaces pourrait même être une classe dont les instances définissent des traits comme dans la réponse de Michal, pour optimiser l'espace plutôt que le temps.
Davislor
1

Vous voulez déléguer (signe de tête à la chaîne de responsabilité) quelque chose qui connaît sa propre culture. Donc, utilisez ou créez une construction de type Country ou CultureInfo, comme mentionné ci-dessus dans d'autres réponses.

Mais en général et fondamentalement, votre problème est que vous prenez des constructions procédurales comme «processeur» et les appliquez à OO. OO consiste à représenter des concepts du monde réel à partir d'un domaine commercial ou problématique dans un logiciel. Le processeur ne se traduit par rien dans le monde réel à part le logiciel lui-même. Chaque fois que vous avez des cours comme Processor, Manager ou Governor, les sonneries d'alarme devraient sonner.

Franc
la source
0

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

La chaîne de responsabilité est le genre de chose que vous recherchez peut-être, mais dans la POO, c'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 en cas de besoin avec un coût O (1).

jlvaquero
la source
0

Je suis désolé d'avoir inventé il y a longtemps le terme «objets» pour ce sujet, car cela amène beaucoup de gens à se concentrer sur l'idée moindre. La grande idée est la messagerie .

~ Alan Kay, sur la messagerie

Je mettre en œuvre simplement des routines Capitalise, RemovePunctuationetc. comme sous - processus qui peut être messaged avec textet countryparamètres, et je renvoie 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: CapitalisationApplicableCountrieset 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.
}
Igwe Kalu
la source
0

Je pense que les informations sur les pays devraient être conservées dans des données, pas dans du 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.

Steve J
la source