【问题标题】:Design pattern for cost calculator app?成本计算器应用程序的设计模式?
【发布时间】:2010-05-05 11:48:48
【问题描述】:

我有一个问题,我之前尝试过寻求帮助,但当时我无法解决,所以我现在尝试简化问题,看看是否可以得到更具体的帮助这是因为它让我发疯......

基本上,我有这个应用程序的工作(更复杂)版本,它是一个项目成本计算器。但是因为我同时试图学习更好地设计我的应用程序,所以我想就如何改进这个设计提供一些意见。基本上,我想要的主要内容是输入(此处)在两个地方重复出现的条件。之前得到的建议是使用策略模式或者工厂模式。我也知道 Martin Fowler 的书,其中包含使用多态性重构条件的建议。我在他更简单的例子中理解了这个原则。但是我怎么能在这里做这些事情(如果有的话)?在我看来,计算取决于几个条件: 1. 它是什么类型的服务,写作还是分析? 2. 项目是小、中还是大? (请注意,可能还有其他参数,同样不同,例如“产品是新的还是以前存在的?”所以应该可以添加这样的参数,但我尽量保持示例简单,只需要两个参数能够得到具体的帮助)

所以用多态性重构意味着创建许多子类,我已经为第一个条件(服务类型)创建了更多子类,我真的应该为第二个条件(大小)创建更多子类吗?那会变成什么,AnalysisSmall、AnalysisMedium、AnalysisLarge、WritingSmall 等等……???不,我知道这不好,我只是不知道如何使用该模式?

对于使用策略模式的建议,我基本上看到了同样的问题(我认为工厂模式只是实现上述多态性的帮手)。因此,如果有人对如何以最佳方式设计这些课程有具体建议,我将不胜感激!还请考虑我是否也正确选择了对象,或者是否需要重新设计它们。 (像“你应该考虑工厂模式”这样的回应显然不会有帮助......我已经走上了这条路,我对这种情况下的确切方式感到困惑)

问候,

安德斯

代码(非常简化,不要介意我使用字符串而不是枚举,不使用配置文件来存储数据等,一旦我掌握了窍门,这将在实际应用程序中根据需要完成这些设计问题):

public abstract class Service
{
    protected Dictionary<string, int> _hours;
    protected const int SMALL = 2;
    protected const int MEDIUM = 8;

    public int NumberOfProducts { get; set; }
    public abstract int GetHours();
}

public class Writing : Service
{
    public Writing(int numberOfProducts)
    {
        NumberOfProducts = numberOfProducts;
        _hours = new Dictionary<string, int> { { "small", 125 }, { "medium", 100 }, { "large", 60 } };
    }

    public override int GetHours()
    {
        if (NumberOfProducts <= SMALL)
            return _hours["small"] * NumberOfProducts;
        if (NumberOfProducts <= MEDIUM)
            return (_hours["small"] * SMALL) + (_hours["medium"] * (NumberOfProducts - SMALL));
        return (_hours["small"] * SMALL) + (_hours["medium"] * (MEDIUM - SMALL))
            + (_hours["large"] * (NumberOfProducts - MEDIUM));
    }
}

public class Analysis : Service
{
    public Analysis(int numberOfProducts)
    {
        NumberOfProducts = numberOfProducts;
        _hours = new Dictionary<string, int> { { "small", 56 }, { "medium", 104 }, { "large", 200 } };
    }

    public override int GetHours()
    {
        if (NumberOfProducts <= SMALL)
            return _hours["small"];
        if (NumberOfProducts <= MEDIUM)
            return _hours["medium"];
        return _hours["large"];
    }
}

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 comboBoxNumberOfProducts_SelectedIndexChanged(object sender, EventArgs e)
    {
        Service writing = new Writing((int) comboBoxNumberOfProducts.SelectedItem);
        Service analysis = new Analysis((int) comboBoxNumberOfProducts.SelectedItem);

        labelWriterHours.Text = writing.GetHours().ToString();
        labelAnalysisHours.Text = analysis.GetHours().ToString();
    }
}

【问题讨论】:

    标签: c# design-patterns factory-pattern strategy-pattern


    【解决方案1】:

    在您的计算中,服务类型、服务规模和产品数量之间存在紧密耦合,因此很难将它们分成模块化块来应用策略模式。

    如果计算系统是固定的,那么策略模式似乎不合适。如果不是……那么,为什么不简化系统呢?

    例如,从服务规模中提取基本小时数,并根据您的其他设置应用各种折扣或增加。

    public class Service
    {
        public IServiceSize serviceSize { internal get; set; }
        public IServiceBulkRate serviceBulkRate { internal get; set; }
        public IServiceType serviceType { internal get; set; }
        public int numberOfProducts { get; set; }
    
        /// <summary>
        /// Initializes a new instance of the <see cref="Service"/> class with default values
        /// </summary>
        public Service()
        {
            serviceSize = new SmallSize();
            serviceBulkRate = new FlatBulkRate();
            serviceType = new WritingService();
            numberOfProducts = 1;
        }
    
        public decimal CalculateHours()
        {
            decimal hours = serviceSize.GetBaseHours();
            hours = hours * serviceBulkRate.GetMultiplier(numberOfProducts);
            hours = hours * serviceType.GetMultiplier();
    
            return hours;
        }
    }
    
    public interface IServiceSize
    {
        int GetBaseHours();
    }
    
    public class SmallSize : IServiceSize
    {
        public int GetBaseHours()
        {
            return 125;
        }
    }
    
    public interface IServiceBulkRate
    {
        decimal GetMultiplier(int numberOfProducts);
    }
    
    public class FlatBulkRate : IServiceBulkRate
    {
        public decimal GetMultiplier(int numberOfProducts)
        {
            return numberOfProducts;
        }
    }
    
    public class StaggeredBulkRate : IServiceBulkRate
    {
        public decimal GetMultiplier(int numberOfProducts)
        {
            if (numberOfProducts < 2)
                return numberOfProducts;
            else if (numberOfProducts >= 2 & numberOfProducts < 8)
                return numberOfProducts * 0.85m;
            else
                return numberOfProducts * 0.8m;
        }
    }
    
    public interface IServiceType
    {
        decimal GetMultiplier();
    }
    
    public class WritingService : IServiceType
    {
        public decimal GetMultiplier()
        {
            return 1.15m;
        }
    }
    

    【讨论】:

    • 好的,另一个有趣的答案,谢谢。但我有点认为这会解决问题,如果我错了,请纠正我 - 首先,在构造函数中设置 ServiceSize = new SmallSize(),但这就是问题 - 我们不能只在构造函数中设置它,调用代码必须有一个 if 语句(即使在工厂中): if numberOfProducts
    • 我确实喜欢用折扣率等来扭转逻辑的想法,不过,我一直在尝试自己想出这个想法,但一直无法理解周围。我认为问题在于规范详细说明了这样的问题,我陷入了这种想法。欢迎对此提出更多想法!
    • 好吧,您仍然有两个条件,但它们现在在正确的位置,并且它们不会随着新的服务类型被添加到逻辑中而重复。
    【解决方案2】:

    我会将选择计算哪个值的逻辑移到 Service 基类中,并将实际计算委托给每个子类:

    public abstract class Service
    {
        private readonly int numberOfProducts;
        private readonly IDictionary<string, int> hours;
        protected const int SMALL = 2; 
        protected const int MEDIUM = 8;
    
        public Service(int numberOfProducts, IDictionary<string, int> hours)
        {
            this.numberOfProducts = numberOfProducts;
            this.hours = hours;
        }
    
        public int GetHours()
        {
            if(this.numberOfProducts <= SMALL)
                return this.CalculateSmallHours(this.hours["small"], this.numberOfProducts);
            else if(this.numberOfProducts <= MEDIUM)
                return this.CalculateMediumHours(this.hours["medium"], this.numberOfProducts);
            else
                return this.CalculateLargeHours(this.hours["large"], this.numberOfProducts);
        }
    
        protected abstract int CalculateSmallHours(int hours, int numberOfProducts);
        protected abstract int CalculateMediumHours(int hours, int numberOfProducts);
        protected abstract int CalculateLargeHours(int hours, int numberOfProducts);
    }
    

    然后,如果任何计算特别复杂,您可以将其提取到策略对象中并仅用于该特定子类。

    编辑:如果您想支持任意数量的计算,您可以创建一个类来管理小时“类别”和每个计算之间的映射。那么每个子类(或某个工厂)可以为每个类别提供相关的计算:

    public class HoursCalculationStrategyCollection
    {
        private readonly Dictionary<string, int> hours;
    
        private readonly Dictionary<string, Func<int, int, int>> strategies;
    
        public HoursCalculationStrategyCollection(IDictionary<string, int> hours)
        {
            this.hours = hours;
            this.strategies = new Dictionary<string, Func<int, int, int>();
        }
    
        public void AddCalculationStrategy(string hours, Func<int, int, int> strategy)
        {
            this.strategies[hours] = strategy;
        }
    
        public int CalculateHours(int numberOfProducts)
        {
            string hoursKey = null;
    
            if(numberOfProducts <= SMALL)
                hoursKey = small;
            else if(...)
                ...
    
            Func<int, int, int> strategy = this.strategies[hoursKey];
            return strategy(this.hours[hoursKey], numberOfProducts);
        }
    }
    

    【讨论】:

    • 好的,只要我将小时的初始化移回每个子类(基类不能传递,因为每个子类不同),它就可以工作。然而,这是我之前尝试过的设计的一个变体(但相反),我有一个项目基类和 3 个子类:SmallProject、MediumProject 和 LargeProject。然后我有了 GetWritingHours 和 GetAnalysisHours 方法。所以它以同样的方式工作。但问题是,它似乎无法改变。如果我想添加另一个参数值(在您的示例中为另一个大小),例如 Xsmall 和 Xlarge,那么我必须更改(续)
    • (续)基类,以及所有子类来适应这个,对吧?我可能会离开,但如果我误解了,请澄清。如果这实际上是实际上需要重复条件 if 语句的情况,那么任何人都应该随时告诉我 :-) 我觉得这应该可以更优雅地使用模式,但到目前为止它似乎只是使事情复杂化,并且不会消除在规范更改的情况下在多个地方进行更新的需要(如上面提到的添加方法)......
    • 好的...很有趣,我不太了解 Func 部分,是 lambda 还是 Linq 相关?我欢迎解释它是如何工作的,以及我将如何从子类中使用这个类(我猜)?但是哇,这变得越来越复杂了。我真正想要的是通过不必重复检查大小等来简化类。但我猜你是说问题比这更复杂,因此解决方案会很复杂,所以除非我真的有需要一个非常开放的设计来改变这可能是矫枉过正?
    【解决方案3】:

    您可以将工厂模式和策略模式结合起来。然后,您的工厂将创建一个具体的服务并传递一个策略来处理不同的大小(小型、中型或大型)。

    这将为您提供 8 个类:Service、Analysis、Writing、MediumStrategy、SmallStrategy、LargeStrategy 和 ServiceFactory + 策略接口。

    ServiceFactory 将包含决定使用哪种策略的代码。比如:

    Analysis createAnalysis(int numberOfProducts) {
        SizeStrategy strategy;
        if (numberOfProducts <= SMALL) {
            strategy = new SmallStrategy();
        } else if (numberOfProducts <= MEDIUM) {
            strategy = new MediumStrategy();
        } else {
            strategy = new LargeStrategy();
        }
        return new Analysis(numberOfProducts, strategy);
    }
    

    在这种情况下,您只需节省很少的代码。作为一个练习,这当然无关紧要,但我认为我不会浪费时间在实践中重构它。

    编辑: 再想一想,假设规则可能会改变,在我看来control table 可能比 OOP 模式更合适。

    【讨论】:

    • 好的,但是如果你看一下Analysis and Writing中的GetHours方法,你会发现它们是不同的,那我怎么可能只有一个SmallStrategy之类的......?我需要一个 AnalysisSmallStrategy 和一个 WritingSmallStrategy 不是吗?我认为这将使其不可行......另外,考虑我所说的参数将输入“IsNew”,无论产品是否存在。如果 IsNew 然后为 hoursPerProduct... 选择一种配置比例,以此类推,这将使其更加复杂。我同意我很想离开条件而不打扰。但是像福勒这样的大师告诉我不要:-)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-08-23
    • 1970-01-01
    • 1970-01-01
    • 2010-11-09
    • 1970-01-01
    相关资源
    最近更新 更多