用多态重构或类似替换条件?

时间:2010-05-07 22:49:59

标签: c# design-patterns refactoring conditional

之前我试过问过这个问题的变体。我得到了一些有用的答案,但对我来说仍然没有任何感觉。在我看来,这不应该是一个难以破解的坚果,但我无法找到一个优雅的简单解决方案。 (这是我以前的帖子,但请先尝试将此处所述的问题视为程序代码,以免受早期解释的影响,这些解释似乎导致了非常复杂的解决方案: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));
    }
}

一个简单的示例表单调用它(我想我也可以有一个包装器的Project类,其中包含一个包含Service对象的Dictionary,但我还没有这样做):

    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文件中(我仍然需要为每个新的servicetype添加新元素,并且如果更改,则在每个servicetype中添加任何新大小的元素。 )但也许我不可能实现我想做的事情而不必至少做那种改变。使用数据库而不是xml,更改就像添加字段和行一样简单:

ServiceType Small Medium Large

写作125 100 60

分析56 104 200

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

请告诉我你的想法!

5 个答案:

答案 0 :(得分:6)

我倾向于从枚举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 :(得分:2)

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

我会在代码中写出来,但最好选择名字。

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

让您的第一个目标是编写干净,易于阅读和理解的代码。很容易对模式感到兴奋(从经验来讲)但如果你不能在抽象中描述你现有的代码,它们很难应用。

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

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

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

答案 2 :(得分:2)

如果您的子类根据他们想要收取的费用过滤自己,则不需要工厂。这需要一个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个实现者(或静态),然后只需通过它们遍历Project的列表:

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

答案 3 :(得分:1)

这是一个常见问题,我可以想到几个选项。我想到了两种设计模式,首先是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);
    }
}

答案 4 :(得分:1)

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

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