Design pattern for cost calculator app?

霸气de小男生 提交于 2019-12-05 19:54:49

In your calculation there is a tight coupling between service type, service size and the number of products, it's very hard as it stands to separate them out into modular chunks to apply the strategy pattern.

If the calculation system is fixed, then it seems that the strategy pattern is not appropriate. If it isn't... Well, why not simplify the system?

For example, pull the base number of hours from the service size, and apply various discounts or increases depending on your other settings.

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

I'd move the logic for choosing which value to calculate into the Service base class and delegate the actual calculations to each subclass:

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

Then if any calculation is particularly complicated you could extract it into a strategy object and use it just for that specific subclass.

EDIT: If you want to support an arbitrary number of calculations, you could create a class to manage the mappings between hours 'categories' and a calculation for each one. Then each subclass (or some factory) can provide the relevant calculations for each category:

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

You could combine the factory and the strategy pattern. Your factory would then create a concrete service and pass it a strategy to handle the different sizes (small, medium or large).

This would give you 8 classes: Service, Analysis, Writing, MediumStrategy, SmallStrategy, LargeStrategy and ServiceFactory + the interface for the strategies.

The ServiceFactory would then contain the code to decide which strategy would be used. Something like:

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

In this case you only save very little code though. As an exercise this doesn't matter of course, but I don't think I would waste my time refactoring this in practice.

EDIT: On second thought, assuming that the rules are likely to change, it seems to me that a control table is probably more appropriate than the OOP patterns.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!