Objektorientierte Programmierung - So vermeiden Sie Doppelarbeit in Prozessen, die sich je nach Variable geringfügig unterscheiden

64

Was in meiner aktuellen Arbeit ziemlich häufig vorkommt, ist, dass ein verallgemeinerter Prozess stattfinden muss, aber dann muss der seltsame Teil dieses Prozesses je nach Wert einer bestimmten Variablen etwas anders ablaufen, und das bin ich nicht Ich bin mir ziemlich sicher, was der eleganteste Weg ist, damit umzugehen.

Ich werde das Beispiel verwenden, das wir normalerweise haben und das die Dinge je nach Land, mit dem wir es zu tun haben, etwas anders macht.

Also habe ich eine Klasse, nennen wir es 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);
    }
}

Abgesehen davon, dass nur einige dieser Maßnahmen für bestimmte Länder durchgeführt werden müssen. Beispielsweise benötigen nur 6 Länder den Kapitalisierungsschritt. Der zu teilende Charakter kann sich je nach Land ändern. Akzent ersetzen'e' möglicherweise nur je nach Land erforderlich.

Offensichtlich könnten Sie es lösen, indem Sie so etwas tun:

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);
}

Aber wenn Sie mit allen möglichen Ländern der Welt zu tun haben, wird das sehr umständlich. Und trotzdem dieif erschweren Anweisungen das Lesen der Logik (zumindest, wenn Sie sich eine komplexere Methode als das Beispiel vorstellen), und die zyklomatische Komplexität steigt ziemlich schnell an.

Im Moment mache ich so etwas:

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);
    }
}

Handler:

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; }
}

Was ich auch nicht wirklich sicher bin, ob ich es mag. Die Logik wird durch die gesamte Factory-Erstellung immer noch etwas verdeckt, und Sie können nicht einfach die ursprüngliche Methode betrachten und sehen, was passiert, wenn beispielsweise ein "GBR" -Prozess ausgeführt wird. Sie landen auch viele Klassen zu schaffen (in komplexeren Beispielen als diese) im Stil GbrPunctuationHandler, UsaPunctuationHandleretc ... was bedeutet , dass Sie aus allen möglichen Aktionen an mehreren verschiedenen Klassen Abbildung aussehen müssen , die während der Zeichensetzung passieren könnte Handhabung. Offensichtlich will ich keine Riesenklasse mit einer Milliardeif Aussagen, aber ebenso 20 Klassen mit leicht unterschiedlicher Logik fühlen sich auch klobig an.

Grundsätzlich denke ich, dass ich mich in eine Art OOP-Knoten verwickelt habe und keine gute Möglichkeit kenne, ihn zu entwirren. Ich habe mich gefragt, ob es da draußen ein Muster gibt, das bei dieser Art von Prozess helfen würde.

John Darvill
quelle
Sieht so aus, als hätten Sie eine PreProcessFunktionalität, die je nach Land unterschiedlich implementiert werden könnte, DetermineSeparatorfür alle verfügbar sein kann, und a PostProcess. Alle von ihnen können protected virtual voidmit einer Standardimplementierung sein, und dann können Sie spezifische Processorspro Land haben
Icepickle
Ihre Aufgabe ist es, in einem bestimmten Zeitrahmen etwas zu schaffen, das funktioniert und in absehbarer Zukunft von Ihnen oder einer anderen Person gewartet werden kann. Wenn mehrere Optionen beide Bedingungen erfüllen können, können Sie eine nach Ihren Wünschen auswählen.
Dialecticus
2
Eine praktikable Option für Sie ist eine Konfiguration. In Ihrem Code suchen Sie also nicht nach einem bestimmten Land, sondern nach einer bestimmten Konfigurationsoption. Jedes Land verfügt jedoch über einen bestimmten Satz dieser Konfigurationsoptionen. Zum Beispiel anstatt if (country == "DEU")Sie zu überprüfen if (config.ShouldRemovePunctuation).
Dialecticus
11
Wenn Länder verschiedene Optionen haben, warum ist countryeine Zeichenfolge eher eine Instanz einer Klasse, die diese Optionen modelliert?
Damien_The_Unbeliever
@Damien_The_Unbeliever - könnten Sie etwas näher darauf eingehen? Entspricht die Antwort von Robert Brautigam unten Ihrem Vorschlag? - Ah kann deine Antwort jetzt sehen, danke!
John Darvill

Antworten:

53

Ich würde vorschlagen, alle Optionen in einer Klasse zusammenzufassen:

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; }
}

und geben Sie es in die ProcessMethode:

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
quelle
4
Ich CountrySpecificHandlerFactorybin mir nicht sicher, warum so etwas nicht versucht wurde, bevor ich zu ... o_0
Mateen Ulhaq
Solange es keine zu speziellen Optionen gibt, würde ich definitiv diesen Weg gehen. Wenn die Optionen in eine Textdatei serialisiert werden, können auch Nicht-Programmierer neue Varianten definieren / vorhandene aktualisieren, ohne zur Anwendung wechseln zu müssen.
Tom
4
Das public class ProcessOptionssollte eigentlich nur sein [Flags] enum class ProcessOptions : int { ... }...
Drunken Code Monkey
Und ich denke, wenn sie es brauchen, könnten sie eine Karte der Länder haben ProcessOptions. Sehr angenehm.
Theonlygusti
24

Als sich das .NET Framework auf diese Art von Problemen einließ, modellierte es nicht alles als string. So haben Sie zum Beispiel die CultureInfoKlasse :

Bietet Informationen zu einer bestimmten Kultur (als Gebietsschema für die Entwicklung von nicht verwaltetem Code bezeichnet). Die Informationen umfassen die Namen für die Kultur, das Schriftsystem, den verwendeten Kalender, die Sortierreihenfolge der Zeichenfolgen und die Formatierung für Daten und Zahlen.

Diese Klasse enthält möglicherweise nicht die spezifischen Funktionen, die Sie benötigen, aber Sie können natürlich etwas Analoges erstellen. Und dann ändern Sie Ihre ProcessMethode:

public string Process(CountryInfo country, string text)

Ihre CountryInfoKlasse kann dann eine bool RequiresCapitalizationEigenschaft usw. haben, die Ihrer ProcessMethode hilft , ihre Verarbeitung entsprechend zu steuern.

Damien_The_Unbeliever
quelle
13

Vielleicht könntest du einen haben Processor pro Land haben?

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();
    }
}

Und eine Basisklasse für allgemeine Teile der Verarbeitung:

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);
    }
}

Außerdem sollten Sie Ihre Rückgabetypen überarbeiten, da sie nicht so kompiliert werden, wie Sie sie geschrieben haben. Manchmal gibt eine stringMethode nichts zurück.

Corentin Pane
quelle
Ich glaube, ich habe versucht, der Komposition über das Vererbungsmantra zu folgen. Aber ja, es ist definitiv eine Option, danke für die Antwort.
John Darvill
Meinetwegen. Ich denke, Vererbung ist in einigen Fällen gerechtfertigt, aber es hängt wirklich davon ab, wie Sie Ihre Methoden und die Verarbeitung am Ende laden / speichern / aufrufen / ändern möchten.
Corentin Pane
3
Manchmal ist die Vererbung das richtige Werkzeug für den Job. Wenn Sie einen Prozess haben, der sich in verschiedenen Situationen größtenteils gleich verhält, aber auch mehrere Teile hat, die sich in verschiedenen Situationen unterschiedlich verhalten, ist dies ein gutes Zeichen, das Sie in Betracht ziehen sollten, die Vererbung zu verwenden.
Tanner Swett
5

Sie können eine gemeinsame Schnittstelle mit einer ProcessMethode erstellen ...

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

Dann implementieren Sie es für jedes Land ...

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)";
        }
    }
}

Sie können dann eine gemeinsame Methode zum Instanziieren und Ausführen jeder länderbezogenen Klasse erstellen ...

// 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);
}

Dann müssen Sie nur noch die Prozessoren wie folgt erstellen und verwenden ...

// 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."));

Hier ist ein funktionierendes Dotnet-Geigen-Beispiel ...

Sie platzieren die gesamte länderspezifische Verarbeitung in jeder Länderklasse. Erstellen Sie eine gemeinsame Klasse (in der Processing-Klasse) für alle tatsächlichen Einzelmethoden, sodass jeder Länderprozessor zu einer Liste anderer allgemeiner Aufrufe wird, anstatt den Code in jede Länderklasse zu kopieren.

Hinweis: Sie müssen hinzufügen ...

using System.Assembly;

Damit die statische Methode eine Instanz der Länderklasse erstellt.

Stellen Sie Monica Cellio wieder her
quelle
Ist die Reflexion nicht bemerkenswert langsam im Vergleich zu keinem reflektierten Code? lohnt es sich für diesen Fall?
Jlvaquero
@jlvaquero Nein, die Reflexion ist überhaupt nicht bemerkenswert langsam. Natürlich gibt es einen Leistungseinbruch bei der Angabe eines Typs zur Entwurfszeit, aber es ist wirklich ein vernachlässigbarer Leistungsunterschied und macht sich nur bemerkbar, wenn Sie ihn überbeanspruchen. Ich habe große Messagingsysteme implementiert, die auf der generischen Objektbehandlung basieren, und wir hatten überhaupt keinen Grund, die Leistung in Frage zu stellen, und das mit einem enormen Durchsatz. Ohne merklichen Leistungsunterschied werde ich immer einfach zu pflegenden Code wie diesen verwenden.
Setzen Sie Monica Cellio
Wenn Sie nachdenken, möchten Sie dann nicht Processdie Länderzeichenfolge aus jedem Aufruf von entfernen und stattdessen einmal verwenden, um den richtigen IP-Prozessor zu erhalten? Normalerweise verarbeiten Sie viel Text gemäß den Regeln desselben Landes.
Davislor
@Davislor Genau das macht dieser Code. Wenn Sie es aufrufen Process("GBR", "text");, wird die statische Methode ausgeführt, die eine Instanz des GBR-Prozessors erstellt, und die Process-Methode wird darauf ausgeführt. Es wird nur in einer Instanz für diesen bestimmten Landestyp ausgeführt.
Stellen Sie Monica Cellio
@Archer Richtig. In dem typischen Fall, in dem Sie mehrere Zeichenfolgen gemäß den Regeln für dasselbe Land verarbeiten, ist es effizienter, die Instanz einmal zu erstellen - oder eine konstante Instanz in einer Hash-Tabelle / einem Wörterbuch nachzuschlagen und zurückzugeben ein Hinweis darauf. Sie können dann die Texttransformation auf derselben Instanz aufrufen. Es ist verschwenderisch, für jeden Anruf eine neue Instanz zu erstellen und diese dann zu verwerfen, anstatt sie für jeden Anruf wiederzuverwenden.
Davislor
3

Vor einigen Versionen erhielt der C # -Swtich die volle Unterstützung für den Mustervergleich . Damit der Fall "mehrere Länder übereinstimmen" leicht gemacht werden kann. Während es noch keine Durchfallfähigkeit gibt, kann eine Eingabe mehrere Fälle mit Musterabgleich abgleichen. Es könnte das If-Spam vielleicht etwas klarer machen.

Npw ein Schalter kann normalerweise durch eine Sammlung ersetzt werden. Sie müssen Delegaten und ein Wörterbuch verwenden. Prozess kann durch ersetzt werden.

public delegate string ProcessDelegate(string text);

Dann könnten Sie ein Wörterbuch erstellen:

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

Ich habe functionNames verwendet, um den Delegaten abzugeben. Sie können jedoch die Lambda-Syntax verwenden, um den gesamten Code dort bereitzustellen. Auf diese Weise können Sie die gesamte Sammlung wie jede andere große Sammlung ausblenden. Und der Code wird zu einer einfachen Suche:

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

Das sind so ziemlich die beiden Optionen. Möglicherweise möchten Sie für die Zuordnung Aufzählungen anstelle von Zeichenfolgen verwenden, dies ist jedoch ein kleines Detail.

Christopher
quelle
2

Ich würde vielleicht (abhängig von den Details Ihres Anwendungsfalls) das Country"echte" Objekt anstelle einer Zeichenfolge verwenden. Das Schlüsselwort lautet "Polymorphismus".

Im Grunde würde es so aussehen:

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

Dann können Sie spezialisierte Länder für diejenigen erstellen, die Sie benötigen. Hinweis: Sie müssen nicht Countryfür alle Länder ein Objekt erstellen , das Sie haben LatinlikeCountrykönnen oder sogar GenericCountry. Dort können Sie sammeln, was getan werden soll, und sogar andere wiederverwenden, wie zum Beispiel:

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

O.ä. CountryVielleicht Languagebin ich mir über den Anwendungsfall nicht sicher, aber ich verstehe , worum es geht.

Außerdem sollte die Methode natürlich nicht die Process()sein, die Sie tatsächlich tun müssen. Wie Words()oder was auch immer.

Robert Bräutigam
quelle
1
Ich habe etwas Worthafteres geschrieben, aber ich denke, das gefällt mir im Grunde am besten. Wenn der Anwendungsfall diese Objekte basierend auf einer Länderzeichenfolge nachschlagen muss, kann er die Lösung von Christopher verwenden. Die Implementierung der Schnittstellen könnte sogar eine Klasse sein, deren Instanzen Merkmale wie in Michals Antwort festlegen, um eher räumlich als zeitlich zu optimieren.
Davislor
1

Sie möchten an etwas delegieren (an die Verantwortungskette nicken), das über seine eigene Kultur Bescheid weiß. Verwenden oder erstellen Sie also ein Konstrukt vom Typ Country oder CultureInfo, wie oben in anderen Antworten erwähnt.

Aber im Allgemeinen und im Grunde genommen besteht Ihr Problem darin, dass Sie prozedurale Konstrukte wie 'Prozessor' nehmen und sie auf OO anwenden. Bei OO geht es darum, reale Konzepte aus einer Geschäfts- oder Problemdomäne in Software darzustellen. Der Prozessor übersetzt in nichts in der realen Welt außer der Software selbst. Wann immer Sie Klassen wie Prozessor oder Manager oder Gouverneur haben, sollten Alarmglocken läuten.

Frank
quelle
0

Ich habe mich gefragt, ob es da draußen ein Muster gibt, das bei dieser Art von Prozess helfen würde

Verantwortungskette ist die Art von Dingen, nach denen Sie vielleicht suchen, aber in OOP ist es etwas umständlich ...

Was ist mit einem funktionaleren Ansatz mit 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;
      }
    }
  }
}

HINWEIS: Es muss natürlich nicht alles statisch sein. Wenn die Prozessklasse den Status benötigt, können Sie eine instanziierte Klasse oder eine teilweise angewendete Funktion verwenden;).

Sie können den Prozess für jedes Land beim Start erstellen, jedes in einer indizierten Sammlung speichern und bei Bedarf mit O (1) -Kosten abrufen.

jlvaquero
quelle
0

Es tut mir leid, dass ich vor langer Zeit den Begriff „Objekte“ für dieses Thema geprägt habe, weil sich viele Menschen auf die geringere Idee konzentrieren. Die große Idee ist Messaging .

~ Alan Kay, über Nachrichten

Ich würde einfach Routinen implementieren Capitalise,RemovePunctuation usw. als Teilprozesse , die mit einem messaged werden kann textundcountry Parameter, und würde einen verarbeiteten Text zurückzukehren.

Verwenden Sie Wörterbücher, um Länder zu gruppieren, die einem bestimmten Attribut entsprechen (wenn Sie Listen bevorzugen, funktioniert dies auch mit nur geringen Leistungskosten). Zum Beispiel: CapitalisationApplicableCountriesund 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
quelle
0

Ich bin der Meinung, dass die Informationen über die Länder in Daten und nicht im Code gespeichert werden sollten. Anstelle einer CountryInfo-Klasse oder eines CapitalizationApplicableCountries-Wörterbuchs könnten Sie also eine Datenbank mit einem Datensatz für jedes Land und einem Feld für jeden Verarbeitungsschritt haben, und dann könnte die Verarbeitung die Felder für ein bestimmtes Land durchlaufen und entsprechend verarbeiten. Die Wartung erfolgt dann hauptsächlich in der Datenbank, wobei neuer Code nur benötigt wird, wenn neue Schritte erforderlich sind, und die Daten in der Datenbank für Menschen lesbar sind. Dies setzt voraus, dass die Schritte unabhängig sind und sich nicht gegenseitig stören. Wenn das nicht so ist, sind die Dinge kompliziert.

Steve J.
quelle