c# - 面向對象的編程-如何避免在因變量而略有不同的過程中重複




oop (7)

在我當前的工作中,經常發生的事情是有一個通用的過程需要發生,但是該過程的奇數部分需要根據某個變量的值而稍有不同,所以我不是非常確定處理此問題的最優雅方法是什麼。

我將使用我們通常使用的示例,該示例的處理方式會根據所處理的國家/地區而略有不同。

所以我有一個類,我們稱它為 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);
    }
}

除了某些國家只需要採取其中一些行動。 例如,只有6個國家需要資本化步驟。 要分割的字符可能會因國家/地區而異。 僅根據國家/地區,才需要替換帶重音符號的 'e'

顯然,您可以通過執行以下操作來解決此問題:

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

但是,當您與世界上所有可能的國家打交道時,這將變得非常麻煩。 而且無論如何, if 語句使邏輯更難以閱讀(至少,如果您想像的是比示例更複雜的方法),則循環複雜度開始迅速攀升。

所以目前我正在做這樣的事情:

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

處理程序:

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

同樣,我不確定自己是否喜歡。 所有工廠創建過程中的邏輯仍然有些模糊,例如,您不能簡單地查看原始方法並查看執行“ GBR”過程時會發生什麼。 您還最終以 GbrPunctuationHandlerUsaPunctuationHandler 等樣式創建了很多類(在比這更複雜的示例中)……這意味著您必須查看幾個不同的類才能找出可能發生的所有可能動作。在標點符號處理過程中。 顯然,我不希望有一個擁有十億個 if 語句的巨型類,但同樣地,20個邏輯稍有不同的類也感到笨拙。

基本上,我認為我已經陷入某種OOP糾結,並且還不太了解解決糾纏的好方法。 我想知道是否有某種模式可以幫助此類流程?


很抱歉,我很久以前就為該主題創造了“對象”一詞,因為它使許多人專注於較小的想法。 一個偉大的想法是消息傳遞

〜艾倫·凱(Alan Kay), 關於消息傳遞

我將簡單地將例程 CapitaliseRemovePunctuation 等實現為可以使用 textcountry 參數進行消息傳遞的子流程,並將返回經過處理的文本。

使用字典對符合特定屬性的國家/地區進行分組(如果您希望使用列表,則只需少量的性能費用即可使用)。 例如: CapitalisationApplicableCountriesPunctuationRemovalApplicableCountries

/// 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.
}

我想知道是否有某種模式可以幫助此類流程

責任鍊 是您可能正在尋找的那種東西,但是在OOP中有點麻煩。

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

注意:當然,它不必全部都是靜態的。 如果Process類需要狀態,則可以使用實例化類或部分應用的函數;)。

您可以在啟動時為每個國家/地區建立流程,將每個國家/地區存儲在索引集中,並在需要時以O(1)成本進行檢索。


幾個版本之前,C#swtich被 完全支持模式匹配 。 這樣“多個國家匹配”的情況就很容易了。 儘管它仍然沒有穿透能力,但一個輸入可以通過模式匹配來匹配多個案例。 可能會使垃圾郵件更加清晰。

Npw交換機通常可以用Collection代替。 您需要使用代理和字典。 流程可以替換為。

public delegate string ProcessDelegate(string text);

然後,您可以製作字典:

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

我使用functionNames提交了委託。 但是您可以使用Lambda語法在此處提供整個代碼。 這樣,您可以像隱藏任何其他大型集合一樣隱藏整個集合。 代碼變成簡單的查找:

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

這些幾乎是兩個選擇。 您可能需要考慮使用Enumerations而不是字符串進行匹配,但這只是次要的細節。


您可以使用 Process 方法創建公共接口...

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

然後您針對每個國家/地區實施它...

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

然後,您可以創建用於實例化和執行每個國家/地區相關類的通用方法...

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

然後,您只需要像這樣創建和使用處理器即可...

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

這是一個有效的dotnet小提琴示例...

您將所有特定於國家/地區的處理置於每個國家/地區類別中。 為所有實際的單個方法創建一個通用類(在Processing類中),因此每個國家/地區處理器將成為其他通用調用的列表,而不是在每個國家/地區類中復制代碼。

注意: 您需要添加...

using System.Assembly;

為了使靜態方法能夠創建國家/地區類的實例。


我也許會(取決於用例的細節)將 Country 作為“真實”對象而不是字符串。 關鍵字是“多態”。

所以基本上它看起來像這樣:

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

然後,您可以為所需的國家創建專門的國家。 注意:您不必為所有國家 Country 創建 Country 對象,您可以擁有 LatinlikeCountry 甚至 GenericCountry 。 您可以在那裡收集應該做的事情,甚至可以重複使用其他內容,例如:

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

或類似。 Country 實際上可能是 Language ,我不確定用例,但是我明白了。

同樣,方法當然不應該是 Process() 它應該是您實際需要完成的事情。 像 Words()


我建議將所有選項封裝在一個類中:

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

並將其傳遞給 Process 方法:

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

當.NET框架著手處理這類問題時,它並沒有將所有模型都建模為 string 。 因此,例如,您具有 CultureInfo

提供有關特定區域性的信息(稱為非託管代碼開發的區域設置)。 該信息包括區域性的名稱,書寫系統,使用的日曆,字符串的排序順序以及日期和數字的格式。

現在,此類可能不包含您需要的 特定 功能,但是您顯然可以創建類似的功能。 然後您更改您的 Process 方法:

public string Process(CountryInfo country, string text)

然後,您的 CountryInfo 類可以具有 bool RequiresCapitalization 屬性等,以幫助您的 Process 方法適當地指導其處理。







oop