面向对象的编程-如何避免在因变量而略有不同的过程中重复


64

在我目前的工作中,经常发生的事情是有一个通用的过程需要发生,但是该过程的奇数部分需要根据某个变量的值而稍有不同,所以我不是非常确定处理此问题的最优雅方法是什么。

我将使用我们通常使用的示例,该示例的处理方式会根据所处理的国家/地区而略有不同。

所以我有一堂课,我们称之为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的纠结中,并且还不太了解解决纠缠的好方法。我想知道是否有某种模式可以帮助此类流程?


看起来您具有某个PreProcess功能,可以根据某些国家/地区以不同的方式实施,DetermineSeparator可以针对所有国家/地区使用,还有一个PostProcess。所有这些都可以protected virtual void使用默认实现,然后您可以针对Processors每个国家/地区
指定

您的工作是在给定的时间范围内,使您或其他人可以在可以预见的将来进行某些可行的维护。如果有多个选项可以同时满足这两个条件,那么您可以根据自己的喜好随意选择其中一个。
Dialecticus

2
一个可行的选择是进行配置。因此,在您的代码中,您不必检查特定的国家/地区,而是检查特定的配置选项。但是每个国家/地区都会有一组特定的配置选项。例如,而不是if (country == "DEU")您检查if (config.ShouldRemovePunctuation)
Dialecticus

11
如果国家有多种选择,为什么要country使用字符串而不是模拟这些选择的类的实例?
Damien_The_Unbeliever

@Damien_The_Unbeliever-您能详细说明一下吗?下面是罗伯特·布劳蒂甘(Robert Brautigam)的答案,与您的建议相符吗?-啊,现在可以看到您的答案,谢谢!
约翰·达维尔

Answers:


53

我建议将所有选项封装在一个类中:

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

并将其传递给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);
}

4
不知道为什么在跳到CountrySpecificHandlerFactory... 之前没有尝试过这样的事情... o_0
Mateen Ulhaq '19

只要没有太专业的选择,我肯定会走这条路。如果将选项序列化为文本文件,则还允许非程序员定义新的变体/更新现有的变体,而无需更改应用程序。
汤姆(Tom),

4
public class ProcessOptions真的应该只是[Flags] enum class ProcessOptions : int { ... }……
醉酒的代码猴

我想如果他们需要的话,他们可以提供一张国家地图ProcessOptions。很方便。
theonlygusti

24

当.NET框架着手处理这类问题时,它并没有将所有模型都建模为string。例如,您拥有CultureInfo

提供有关特定区域性的信息(称为非托管代码开发的区域设置)。该信息包括区域性的名称,书写系统,使用的日历,字符串的排序顺序以及日期和数字的格式。

现在,此类可能不包含您需要的特定功能,但是您显然可以创建类似的功能。然后更改Process方法:

public string Process(CountryInfo country, string text)

CountryInfo然后,您的类可以具有bool RequiresCapitalization属性等,以帮助您的Process方法适当地指导其处理。


13

也许Processor每个国家可以有一个?

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

一个基类来处理处理的常见部分:

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

另外,您应该重新处理返回类型,因为它在编写它们时不会编译-有时某个string方法不会返回任何内容。


我想我正在尝试遵循继承原则。但是,是的,这绝对是一个选择,谢谢您的答复。
John Darvill

很公平。我认为在某些情况下继承是合理的,但实际上取决于最终打算如何加载/存储/调用/更改方法和处理。
科伦丁·潘(Corentin Pane)

3
有时候,继承是完成这项工作的正确工具。如果您的流程在几种不同情况下的行为几乎相同,但是在不同情况下又有几个部分的行为会有所不同,那么这就是您应该考虑使用继承的好兆头。
Tanner Swett

5

您可以使用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;

为了使静态方法能够创建国家/地区类的实例。


与没有反射的代码相比,反射是否不显着慢?在这种情况下值得吗?
jlvaquero

@jlvaquero不,反射一点也不慢。当然,在设计时指定类型会影响性能,但这实际上可以忽略不计,只有当您过度使用它时才会注意到。我已经实现了围绕通用对象处理构建的大型消息传递系统,我们根本没有理由质疑性能,而这具有巨大的吞吐量。在性能上没有明显区别的情况下,我将始终像这样简单地维护代码。
恢复莫妮卡·切里奥(Monica Cellio)

如果您正在反思,您是否不想从每次对的调用中删除国家/地区字符串Process,而是只使用一次以获得正确的IProcessor?通常,您会根据同一国家/地区的规则处理大量文本。
戴维斯洛

@Davislor这正是这段代码的作用。调用时,Process("GBR", "text");它将执行创建GBR处理器实例的静态方法,并在该方法上执行Process方法。对于该特定国家/地区类型,它仅在一个实例上执行。
恢复莫妮卡·切里奥(Monica Cellio)

@Archer对,因此在典型情况下,您要根据同一国家/地区的规则处理多个字符串,创建一次实例或在哈希表/字典中查找常量实例并返回将更为有效。对此的参考。然后,您可以在同一实例上调用文本转换。为每个调用创建一个新实例然后丢弃它,而不是为每个调用重新使用它是浪费的。
戴维斯洛

3

几个版本之前,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而不是字符串进行匹配,但这只是次要的细节。


2

我也许会(取决于用例的细节)选择Country一个“真实的”对象而不是一个字符串。关键字是“多态”。

所以基本上它看起来像这样:

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

然后,您可以为所需的国家创建专门的国家。注意:您不必Country为所有国家/地区创建对象,也可以具有LatinlikeCountry甚至GenericCountry。您可以在那里收集应该做的事情,甚至可以重复使用其他内容,例如:

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

或类似。Country可能实际上是Language,我不确定用例,但是我明白了。

同样,方法当然不Process()应该是您实际需要做的事情。喜欢Words()或其他。


1
我写的东西比较冗长,但是我认为这基本上是我最喜欢的东西。如果用例需要根据国家/地区字符串查找这些对象,则可以使用Christopher的解决方案。接口的实现甚至可以是一个类,其实例设置诸如Michal的回答中所述的特征以优化空间而不是时间。
戴维斯洛

1

您想委托(点头责任链)一些了解其自身文化的东西。因此,请使用或制作一个Country或CultureInfo类型的构造,如上面其他答案所述。

但是总的来说,从根本上讲,您的问题是您正在采用“处理器”之类的程序结构并将其应用于OO。面向对象是从软件的业务或问题领域表示现实世界的概念。除了软件本身之外,处理器不会转换为现实世界中的任何内容。每当您有上课者如处理器,管理器或总督时,警钟就会响起。


0

我想知道是否有某种模式可以帮助此类流程

责任链是您可能正在寻找的那种东西,但是在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)成本进行检索。


0

很抱歉,我很久以前就为该主题创造了“对象”一词,因为它使许多人专注于较小的想法。一个伟大的想法是消息传递

〜艾伦·凯(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.
}

0

我认为有关国家的信息应保存在数据中,而不是代码中。因此,您可以拥有一个包含每个国家的记录和每个处理步骤的字段的数据库,而不是CountryInfo类或CapitalisationApplicableCountries词典,然后处理可以遍历给定国家的字段并进行相应处理。然后,维护主要在数据库中,仅在需要新步骤时才需要新代码,并且数据可以在数据库中被人类读取。假设步骤是独立的,并且不会互相干扰;如果不是这样,那么事情就很复杂。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.