I have tried to ask a variant of this question before. I got some helpful answers, but still nothing that felt quite right to me. It seems to me this shouldn't really be that hard a nut to crack, but I'm not able to find an elegant simple solution. (Here's my previous post, but please try to look at the problem stated here as procedural code first so as not to be influenced by the earlier explanation which seemed to lead to very complicated solutions: http://stackoverflow.com/questions/2772858/design-pattern-for-cost-calculator-app )
Basically, the problem is to create a calculator for hours needed for projects that can contain a number of services. In this case "writing" and "analysis". The hours are calculated differently for the different services: writing is calculated by multiplying a "per product" hour rate with the number of products, and the more products are included in the project, the lower the hour rate is, but the total number of hours is accumulated progressively (i.e. for a medium-sized project you take both the small range pricing and then add the medium range pricing up to the number of actual products). Whereas for analysis it's much simpler, it is just a bulk rate for each size range.
How would you be able to refactor this into an elegant and preferably simple object-oriented version (please note that I would never write it like this in a purely procedural manner, this is just to show the problem in another way succinctly).
I have been thinking in terms of factory, strategy and decorator patterns, but can't get any to work well. (I read Head First Design Patterns a while back, and both the decorator and factory patterns described have some similarities to this problem, but I have trouble seeing them as good solutions as stated there. The decorator example seemed very complicated there for just adding condiments, but maybe it could work better here, I don't know. At least the fact that the calculation of hours accumulates progressively made me think of the decorator pattern... And the factory pattern example from the book with the pizza factory...well it just seems to create such a ridiculous explosion of classes, at least in their example. I have found good use for factory patterns before, but I can't see how I could use it here without getting a really complicated set of classes)
The main goal would be to only have to change in one place (loose coupling etc) if I were to add a new parameter (say another size, like XSMALL, and/or another service, like "Administration"). Here's the procedural code example:
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
}
}
All replies are appreciated! (But as I stated in my previous posts I would appreciate actual code examples rather than just "Try this pattern", because as I mentioned, that is what I'm having trouble with...) I hope someone has a really elegant solution to this problem that I actually thought from the beginning would be really simple...
========================================================
NEW ADDITION:
I appreciate all the answers so far, but I'm still not seeing a really simple and flexible solution to the problem (one I thought wouldn't be very complex at first, but apparently is). It may also be that I haven't quite understood each answer correctly yet. But I thought I'd post my current attempt at working it out (with some help from reading all the different angles in answers here). Please tell me if I'm on the right track or not. But at least now it feels like it's starting to get more flexible... I can quite easily add new parameters without having to change in lots of places (I think!), and the conditional logic is all in one place. I have some of it in xml to get the basic data, which simplifies part of the problem, and part of it is an attempt at a strategy type solution.
Here's the code:
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));
}
}
And a simple example form that calls it (I guess I could also have a wrapper Project class with a Dictionary containing the Service objects, but I haven't gotten to that):
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();
}
}
(I wasn't able to include the xml because it got automatically formatted on this page, but it's basically just a bunch of elements with each service type, and each service type containing the sizes with the hour rates as values.)
I'm not sure if I'm just pushing the problem over to the xml file (I'd still have to add new elements for each new servicetype, and add elements for any new size in each servicetype if that changed.) But maybe it's impossible to achieve what I am trying to do and not having to do at least that type of change. Using a database rather than xml the change would be as simple as adding a field and a row:
ServiceType Small Medium Large
Writing 125 100 60
Analysis 56 104 200
(Simply formatted as a "table" here, although the columns aren't quite aligned... I'm not the best at database design though, and maybe it should be done differently, but you get the idea...)
Please tell me what you think!