【问题标题】:Replace conditional with polymorphism refactoring or similar?用多态重构或类似方法替换条件?
【发布时间】:2010-05-11 19:33:10
【问题描述】:

我之前尝试过问这个问题的变体。我得到了一些有用的答案,但仍然没有什么让我觉得很合适。在我看来,这不应该真的很难破解,但我无法找到一个优雅简单的解决方案。 (这是我之前的帖子,但请先尝试将此处所述的问题视为程序代码,以免受到之前似乎导致非常复杂的解决方案的解释的影响:Design pattern for cost calculator app?

基本上,问题在于创建一个计算器,用于计算包含许多服务的项目所需的小时数。在这种情况下,“写作”和“分析”。不同服务的工时计算方式不同:写作是通过将“每个产品”的工时率乘以产品的数量来计算的,项目中包含的产品越多,工时率越低,但总数量小时数是逐步累积的(即,对于中型项目,您同时采用小范围定价,然后将中等范围定价添加到实际产品的数量)。而对于分析来说,它要简单得多,它只是每个尺寸范围的批量费率。

您如何能够将其重构为一个优雅且最好是简单的面向对象版本(请注意,我永远不会以纯粹的程序方式编写它,这只是为了以另一种方式简洁地展示问题) .

我一直在考虑工厂、策略和装饰器模式,但无法让任何一个很好地工作。 (我不久前阅读了 Head First Design Patterns ,所描述的装饰器和工厂模式都与这个问题有一些相似之处,但我很难将它们视为那里所述的良好解决方案。装饰器示例在那里似乎非常复杂,仅用于添加调味品,但也许在这里可以更好地工作,我不知道。至少小时计算逐渐累积的事实让我想到了装饰者模式......以及披萨工厂书中的工厂模式示例...... .好吧,它似乎创造了如此荒谬的类爆炸,至少在他们的例子中。我以前发现工厂模式很好用,但我看不出如果没有一组非常复杂的类,我怎么能在这里使用它)

如果我要添加一个新参数(比如另一个大小,如 XSMALL,和/或另一个服务,如“管理”),主要目标是只需要在一个地方进行更改(松散耦合等)。这是程序代码示例:

public class Conditional
{
    private int _numberOfManuals;
    private string _serviceType;
    private const int SMALL = 2;
    private const int MEDIUM = 8;

    public int GetHours()
    {
        if (_numberOfManuals <= SMALL)
        {
            if (_serviceType == "writing")
                return 30 * _numberOfManuals;
            if (_serviceType == "analysis")
                return 10;
        }
        else if (_numberOfManuals <= MEDIUM)
        {
            if (_serviceType == "writing")
                return (SMALL * 30) + (20 * _numberOfManuals - SMALL);
            if (_serviceType == "analysis")
                return 20;
        }
        else //i.e. LARGE
        {
            if (_serviceType == "writing")
                return (SMALL * 30) + (20 * (MEDIUM - SMALL)) + (10 * _numberOfManuals - MEDIUM);
            if (_serviceType == "analysis")
                return 30;
        }
        return 0; //Just a default fallback for this contrived example
    }
}

感谢所有回复! (但正如我在之前的帖子中所说,我会欣赏实际的代码示例,而不仅仅是“尝试这种模式”,因为正如我所提到的,这就是我遇到的麻烦......)我希望有人有一个非常优雅的解决方案这个问题其实我一开始就觉得很简单……

================================================ =========

新增:

到目前为止,我很欣赏所有的答案,但我仍然没有看到一个真正简单灵活的解决方案(我一开始认为不会很复杂,但显然是)。也可能是我还没有完全正确理解每个答案。但我想我会发布我目前的解决方法(在阅读此处答案的所有不同角度的帮助下)。请告诉我我是否在正确的轨道上。但至少现在感觉它开始变得更加灵活......我可以很容易地添加新参数而无需在很多地方进行更改(我认为!),并且条件逻辑都在一个地方。我在xml中有一部分是用来获取基础数据的,这简化了一部分问题,一部分是在策略类型的解决方案上的尝试。

代码如下:

 public class Service
{
    protected HourCalculatingStrategy _calculatingStrategy;
    public int NumberOfProducts { get; set; }
    public const int SMALL = 3;
    public const int MEDIUM = 9;
    public const int LARGE = 20;
    protected string _serviceType;
    protected Dictionary<string, decimal> _reuseLevels;

    protected Service(int numberOfProducts)
    {
        NumberOfProducts = numberOfProducts;
    }

    public virtual decimal GetHours()
    {
        decimal hours = _calculatingStrategy.GetHours(NumberOfProducts, _serviceType);
        return hours;
    }
}

public class WritingService : Service
{
    public WritingService(int numberOfProducts)
        : base(numberOfProducts)
    {
        _calculatingStrategy = new VariableCalculatingStrategy();
        _serviceType = "writing";
    }
}

class AnalysisService : Service
{
    public AnalysisService(int numberOfProducts)
        : base(numberOfProducts)
    {
        _calculatingStrategy = new FixedCalculatingStrategy();
        _serviceType = "analysis";
    }
}

public abstract class HourCalculatingStrategy
{
    public abstract int GetHours(int numberOfProducts, string serviceType);

    protected int GetHourRate(string serviceType, Size size)
    {
        XmlDocument doc = new XmlDocument();
        doc.Load("calculatorData.xml");
        string result = doc.SelectSingleNode(string.Format("//*[@type='{0}']/{1}", serviceType, size)).InnerText;
        return int.Parse(result);
    }
    protected Size GetSize(int index)
    {
        if (index < Service.SMALL)
            return Size.small;
        if (index < Service.MEDIUM)
            return Size.medium;
        if (index < Service.LARGE)
            return Size.large;
        return Size.xlarge;
    }
}

public class VariableCalculatingStrategy : HourCalculatingStrategy
{
    public override int GetHours(int numberOfProducts, string serviceType)
    {
        int hours = 0;
        for (int i = 0; i < numberOfProducts; i++)
        {
            hours += GetHourRate(serviceType, GetSize(i + 1));
        }
        return hours;
    }
}

public class FixedCalculatingStrategy : HourCalculatingStrategy
{
    public override int GetHours(int numberOfProducts, string serviceType)
    {
        return GetHourRate(serviceType, GetSize(numberOfProducts));
    }
}

还有一个调用它的简单示例表单(我想我也可以有一个包装器项目类,其中包含一个包含服务对象的字典,但我还没有做到这一点):

    public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        List<int> quantities = new List<int>();

        for (int i = 0; i < 100; i++)
        {
            quantities.Add(i);
        }
        comboBoxNumberOfProducts.DataSource = quantities;
    }


    private void CreateProject()
    {
        int numberOfProducts = (int)comboBoxNumberOfProducts.SelectedItem;
        Service writing = new WritingService(numberOfProducts);
        Service analysis = new AnalysisService(numberOfProducts);

        labelWriterHours.Text = writing.GetHours().ToString();
        labelAnalysisHours.Text = analysis.GetHours().ToString();
    }
    private void comboBoxNumberOfProducts_SelectedIndexChanged(object sender, EventArgs e)
    {
        CreateProject();
    }

}

(我无法包含 xml,因为它在此页面上自动格式化,但它基本上只是每种服务类型的一堆元素,每种服务类型都包含以小时费率作为值的大小。)

我不确定我是否只是将问题推到 xml 文件中(我仍然必须为每个新服务类型添加新元素,并在每个服务类型中添加任何新大小的元素(如果更改)。 ) 但也许不可能实现我想要做的事情,而不必做至少那种类型的改变。使用数据库而不是 xml,更改就像添加一个字段和一行一样简单:

ServiceType 小 中 大

写作 125 100 60

分析 56 104 200

(这里只是格式化为“表格”,虽然列并没有完全对齐......但我在数据库设计方面并不是最好的,也许它应该以不同的方式完成,但你明白了...... .)

请告诉我你的想法!

【问题讨论】:

  • 这是一个微小的变化,并没有真正回答任何更广泛的模式问题,但是在“写作”的情况下,您可以递归调用该函数:return GetHours(SMALL) + 20 * _numberOfManuals - SMALL); 当您想添加 XSMALL,可以说读起来更干净。
  • +1 我喜欢人们热衷于编写干净的代码

标签: c# design-patterns refactoring conditional


【解决方案1】:

我倾向于从枚举ProjectSize {Small, Medium, Large} 和一个简单的函数开始,以在给定 numberOfManuals 的情况下返回适当的枚举。从那里,我会写不同的ServiceHourCalculatorsWritingServiceHourCalculatorAnalysisServiceHourCalculator(因为它们的逻辑完全不同)。每个都需要一个 numberOfManuals、一个 ProjectSize,并返回小时数。我可能会创建一个从字符串到 ServiceHourCalculator 的映射,所以我可以说:

ProjectSize projectSize = getProjectSize(_numberOfManuals);
int hours = serviceMap.getService(_serviceType).getHours(projectSize, _numberOfManuals);

这样,当我添加新的项目大小时,编译器会阻止每个服务的一些未处理的情况。不是在一个地方全部处理,而是在再次编译之前全部处理完毕,这就是我所需要的。

更新 我知道 Java,而不是 C#(非常好),所以这可能不是 100% 正确,但创建地图是这样的:

Map<String, ServiceHourCalculator> serviceMap = new HashMap<String, ServiceHourCalculator>();
serviceMap.put("writing", new WritingServiceHourCalculator());
serviceMap.put("analysis", new AnalysisServiceHourCalculator());

【讨论】:

  • +1 我喜欢这个 - 朝着重构解决方案的渐进步骤,而不是硬着头皮进入模式
  • 谢谢,yeah,枚举用于实际应用程序(正如我提到的,这是一个人为的程序示例,与我迄今为止尝试的代码完全不同,因为发布这只是让我得到了过于复杂的答案)。但基本上你是说你不会努力只在一个地方得到条件,只要编译器得到它你就会很高兴?我可能误解了你的意思,但我希望能进一步实现将这种逻辑集中在一个地方的目标......我没有明白服务地图的重点,你能详细说明一下吗?
  • 我对枚举舒适。它可能不是最终的、最好的设计,但它很简单,如果它没有出现太多的地方,我可以把它留在那里。无论如何,这是一个很好的临时情况,一旦事情处于这种形式,就可以更容易地看到后续的重构。服务地图只是一种说法,给定一个字符串(“写作”或“分析”),给我相应的计算器。我可能应该称它为“calculatorMap”。 :-)
  • 我在任何更详细的回复中都看不到真正的答案,所以我会接受这个答案,简短而简洁,因为它让我想到了我自己的解决方案:- )
【解决方案2】:

一个好的开始是将条件语句提取到一个方法中(虽然只是一个小方法)并给它一个非常明确的名称。然后将 if 语句中的逻辑提取到它们自己的方法中 - 再次使用真正明确的名称。 (如果方法名称很长,请不要担心 - 只要它们执行它们被调用的操作即可)

我会用代码把它写出来,但你最好选择名字。

然后我会转向更复杂的重构方法和模式。只有当您查看一系列方法调用时,才会开始应用模式等。

您的首要目标是编写干净、易于阅读和理解的代码。很容易对模式感到兴奋(根据经验),但如果您不能抽象地描述现有代码,则很难应用它们。

编辑: 所以澄清一下 - 你的目标应该是让你的 if 语句看起来像这样

if( isBox() )
{
    doBoxAction();
}
else if( isSquirrel() )
{
    doSquirrelAction();
}

在我看来,一旦你这样做了,那么应用这里提到的一些模式就更容易了。但是,一旦您的 if 语句中仍然有计算等...,那么就很难从树上看到木头,因为您的抽象程度太低了。

【讨论】:

  • 好的,我想我有点明白你的意思,但你能在代码中给出一个简短的例子,以免我误解吗?
  • 好的,感谢您的澄清。尽管如此,正如我所提到的,这个例子是为了清楚起见而设计的——以一种我永远不会写它的程序方式来解释它,只是为了获得面向对象解决方案的客观(不是双关语)观点。跨度>
【解决方案3】:

如果您的子类根据他们想要收费的内容过滤自己,则您不需要工厂。这需要一个 Project 类来保存数据,如果没有别的:

class Project {
    TaskType Type { get; set; }
    int? NumberOfHours { get; set; }
}

既然你想轻松地添加新的计算,你需要一个接口:

IProjectHours {
    public void SetHours(IEnumerable<Project> projects);
}

还有,一些实现接口的类:

class AnalysisProjectHours : IProjectHours {
    public void SetHours(IEnumerable<Project> projects) {
       projects.Where(p => p.Type == TaskType.Analysis)
               .Each(p => p.NumberOfHours += 30);
    }
}

// Non-LINQ equivalent
class AnalysisProjectHours : IProjectHours {
    public void SetHours(IEnumerable<Project> projects) {
       foreach (Project p in projects) {
          if (p.Type == TaskType.Analysis) {
             p.NumberOfHours += 30;
          }
       }
    }
}

class WritingProjectHours : IProjectHours {
    public void SetHours(IEnumerable<Project> projects) {
       projects.Where(p => p.Type == TaskType.Writing)
               .Skip(0).Take(2).Each(p => p.NumberOfHours += 30);
       projects.Where(p => p.Type == TaskType.Writing)
               .Skip(2).Take(6).Each(p => p.NumberOfHours += 20);
       projects.Where(p => p.Type == TaskType.Writing)
               .Skip(8).Each(p => p.NumberOfHours += 10);
    }
}

// Non-LINQ equivalent
class WritingProjectHours : IProjectHours {
    public void SetHours(IEnumerable<Project> projects) {
       int writingProjectsCount = 0;
       foreach (Project p in projects) {
          if (p.Type != TaskType.Writing) {
             continue;
          }
          writingProjectsCount++;
          switch (writingProjectsCount) {
              case 1: case 2:
                p.NumberOfHours += 30;
                break;
              case 3: case 4: case 5: case 6: case 7: case 8:
                p.NumberOfHours += 20;
                break;
              default:
                p.NumberOfHours += 10;
                break;
          }
       }
    }
}

class NewProjectHours : IProjectHours {
    public void SetHours(IEnumerable<Project> projects) {
       projects.Where(p => p.Id == null).Each(p => p.NumberOfHours += 5);
    }
}

// Non-LINQ equivalent
class NewProjectHours : IProjectHours {
    public void SetHours(IEnumerable<Project> projects) {
       foreach (Project p in projects) {
          if (p.Id == null) {
            // Add 5 additional hours to each new project
            p.NumberOfHours += 5; 
          }
       }
    }
}    

调用代码可以动态加载IProjectHours 实现者(或静态它们),然后通过它们遍历Projects 的列表:

foreach (var h in AssemblyHelper.GetImplementors<IProjectHours>()) {
   h.SetHours(projects);
}
Console.WriteLine(projects.Sum(p => p.NumberOfHours));
// Non-LINQ equivalent
int totalNumberHours = 0;
foreach (Project p in projects) {
   totalNumberOfHours += p.NumberOfHours;
}
Console.WriteLine(totalNumberOfHours);

【讨论】:

  • 谢谢,虽然我不能说我很理解你的例子,但恐怕。我不太了解 Lambda 和 Linq,无法准确解释正在发生的事情。它确实让我想更多地研究它,然后也许我可以判断这是否会奏效...... :-)(如果你有时间,欢迎你进一步解释)我也会研究访问者模式,这在 HF 书中没有涉及。
  • @Anders - 现在我想起来了,这根本不是访客模式。那好吧。 LINQ 语句只是我试图使您的逻辑更易于阅读的尝试。重要的一点是条件句被替换为IProjectHours,它知道它们适合什么类型的Project。这使过滤器(条件)和操作(小时数)保持在同一位置。
【解决方案4】:

我会选择策略模式衍生品。这增加了额外的类,但从长远来看更易于维护。另外,请记住,这里仍有重构的机会:

public class Conditional
{
    private int _numberOfManuals;
    private string _serviceType;
    public const int SMALL = 2;
    public const int MEDIUM = 8;
    public int NumberOfManuals { get { return _numberOfManuals; } }
    public string ServiceType { get { return _serviceType; } }
    private Dictionary<int, IResult> resultStrategy;

    public Conditional(int numberOfManuals, string serviceType)
    {
        _numberOfManuals = numberOfManuals;
        _serviceType = serviceType;
        resultStrategy = new Dictionary<int, IResult>
        {
              { SMALL, new SmallResult() },
              { MEDIUM, new MediumResult() },
              { MEDIUM + 1, new LargeResult() }
        };
    }

    public int GetHours()
    {
        return resultStrategy.Where(k => _numberOfManuals <= k.Key).First().Value.GetResult(this);
    }
}

public interface IResult
{
    int GetResult(Conditional conditional);
}

public class SmallResult : IResult
{
    public int GetResult(Conditional conditional)
    {
        return conditional.ServiceType.IsWriting() ? WritingResult(conditional) : AnalysisResult(conditional); ;
    }

    private int WritingResult(Conditional conditional)
    {
        return 30 * conditional.NumberOfManuals;
    }

    private int AnalysisResult(Conditional conditional)
    {
        return 10;
    }
}

public class MediumResult : IResult
{
    public int GetResult(Conditional conditional)
    {
        return conditional.ServiceType.IsWriting() ? WritingResult(conditional) : AnalysisResult(conditional); ;
    }

    private int WritingResult(Conditional conditional)
    {
        return (Conditional.SMALL * 30) + (20 * conditional.NumberOfManuals - Conditional.SMALL);

    }

    private int AnalysisResult(Conditional conditional)
    {
        return 20;
    }
}

public class LargeResult : IResult
{
    public int GetResult(Conditional conditional)
    {
        return conditional.ServiceType.IsWriting() ? WritingResult(conditional) : AnalysisResult(conditional); ;
    }

    private int WritingResult(Conditional conditional)
    {
        return (Conditional.SMALL * 30) + (20 * (Conditional.MEDIUM - Conditional.SMALL)) + (10 * conditional.NumberOfManuals - Conditional.MEDIUM);

    }

    private int AnalysisResult(Conditional conditional)
    {
        return 30;
    }
}

public static class ExtensionMethods
{
    public static bool IsWriting(this string value)
    {
        return value == "writing";
    }
}

【讨论】:

  • 好的,谢谢,这更符合我自己的想法,但我相信添加新参数仍然存在问题......我可能已经很好地解释了我的目标,但我意思是确定添加更多大小类可能很容易,但是如果您添加其他服务,那会不会更糟?必须在每个尺寸类别中为新服务添加代码...?
【解决方案5】:

这是一个常见问题,我能想到几个选项。我想到了两种设计模式,首先是Strategy Pattern,其次是Factory Pattern。使用策略模式可以将计算封装到一个对象中,例如,您可以将 GetHours 方法封装到单独的类中,每个类都表示基于大小的计算。一旦我们定义了不同的计算策略,我们就将其包装在工厂中。工厂将负责选择执行计算的策略,就像您在 GetHours 方法中的 if 语句一样。随便看看下面的代码,看看你的想法

您可以随时创建新策略来执行不同的计算。该策略可以在不同对象之间共享,允许在多个地方使用相同的计算。工厂还可以根据配置动态制定使用哪种策略,例如

class Program
{
    static void Main(string[] args)
    {
        var factory = new HourCalculationStrategyFactory();
        var strategy = factory.CreateStrategy(1, "writing");

        Console.WriteLine(strategy.Calculate());
    }
}

public class HourCalculationStrategy
{
    public const int Small = 2;
    public const int Medium = 8;

    private readonly string _serviceType;
    private readonly int _numberOfManuals;

    public HourCalculationStrategy(int numberOfManuals, string serviceType)
    {
        _serviceType = serviceType;
        _numberOfManuals = numberOfManuals;
    }

    public int Calculate()
    {
        return this.CalculateImplementation(_numberOfManuals, _serviceType);
    }

    protected virtual int CalculateImplementation(int numberOfManuals, string serviceType)
    {
        if (serviceType == "writing")
            return (Small * 30) + (20 * (Medium - Small)) + (10 * numberOfManuals - Medium);
        if (serviceType == "analysis")
            return 30;

        return 0;
    }
}

public class SmallHourCalculationStrategy : HourCalculationStrategy
{
    public SmallHourCalculationStrategy(int numberOfManuals, string serviceType) : base(numberOfManuals, serviceType)
    {
    }

    protected override int CalculateImplementation(int numberOfManuals, string serviceType)
    {
        if (serviceType == "writing")
            return 30 * numberOfManuals;
        if (serviceType == "analysis")
            return 10;

        return 0;
    }
}

public class MediumHourCalculationStrategy : HourCalculationStrategy
{
    public MediumHourCalculationStrategy(int numberOfManuals, string serviceType) : base(numberOfManuals, serviceType)
    {
    }

    protected override int CalculateImplementation(int numberOfManuals, string serviceType)
    {
        if (serviceType == "writing")
            return (Small * 30) + (20 * numberOfManuals - Small);
        if (serviceType == "analysis")
            return 20;

        return 0;
    }
}

public class HourCalculationStrategyFactory
{
    public HourCalculationStrategy CreateStrategy(int numberOfManuals, string serviceType)
    {
        if (numberOfManuals <= HourCalculationStrategy.Small)
        {
            return new SmallHourCalculationStrategy(numberOfManuals, serviceType);
        }

        if (numberOfManuals <= HourCalculationStrategy.Medium)
        {
            return new MediumHourCalculationStrategy(numberOfManuals, serviceType);
        }

        return new HourCalculationStrategy(numberOfManuals, serviceType);
    }
}

【讨论】:

  • 谢谢,不过,我觉得在这里添加更多服务可能仍然是个问题,因为您必须在每个大小策略中为新服务添加逻辑。正如我所看到的,这里仍然有很多条件......所有这些都取决于相同的参数,因此没有完成我最初阅读 Fowler 的用多态性重构替换条件......另外,策略不应该是使用它们的另一个类的一部分(但也许你只是忽略了那部分)?
猜你喜欢
  • 1970-01-01
  • 2014-08-22
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-04-10
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多