Hi,
I'm trying to figure out how to design a small application more elegantly, and make it more resistant to change.
Basically it is a sort of project price calculator, and the problem is that there are many parameters that can affect the pricing. I'm trying to avoid cluttering the code with a lot of if-clauses for each parameter, but still I have e.g. if-clauses in two places checking for the value of the size parameter.
I have the Head First Design Patterns book, and have tried to find ideas there, but the closest I got was the decorator pattern, which has an example where starbuzz coffee sets prices depending first on condiments added, and then later in an exercise by adding a size parameter (Tall, Grande, Venti). But that didn't seem to help, because adding that parameter still seemed to add if-clause complexity in a lot of places (and this being an exercise they didn't explain that further).
What I am trying to avoid is having to change several classes if a parameter were to change or a new parameter added, or at least change in as few places as possible (there's some fancy design principle word for this that I don't rememeber :-)).
Here below is the code. Basically it calculates the price for a project that has the tasks "Writing" and "Analysis" with a size parameter and different pricing models. There will be other parameters coming in later too, like "How new is the product?" (New, 1-5 years old, 6-10 years old), etc. Any advice on the best design would be greatly appreciated, whether a "design pattern" or just good object oriented principles that would make it resistant to change (e.g. adding another size, or changing one of the size values, and only have to change in one place rather than in several if-clauses):
public class Project
{
private readonly int _numberOfProducts;
protected Size _size;
public Task Analysis { get; set; }
public Task Writing { get; set; }
public Project(int numberOfProducts)
{
_numberOfProducts = numberOfProducts;
_size = GetSize();
Analysis = new AnalysisTask(numberOfProducts, _size);
Writing = new WritingTask(numberOfProducts, _size);
}
private Size GetSize()
{
if (_numberOfProducts <= 2)
return Size.small;
if (_numberOfProducts <= 8)
return Size.medium;
return Size.large;
}
public double GetPrice()
{
return Analysis.GetPrice() + Writing.GetPrice();
}
}
public abstract class Task
{
protected readonly int _numberOfProducts;
protected Size _size;
protected double _pricePerHour;
protected Dictionary<Size, int> _hours;
public abstract int TotalHours { get; }
public double Price { get; set; }
protected Task(int numberOfProducts, Size size)
{
_numberOfProducts = numberOfProducts;
_size = size;
}
public double GetPrice()
{
return _pricePerHour * TotalHours;
}
}
public class AnalysisTask : Task
{
public AnalysisTask(int numberOfProducts, Size size)
: base(numberOfProducts, size)
{
_pricePerHour = 850;
_hours = new Dictionary<Size, int>() { { Size.small, 56 }, { Size.medium, 104 }, { Size.large, 200 } };
}
public override int TotalHours
{
get { return _hours[_size]; }
}
}
public class WritingTask : Task
{
public WritingTask(int numberOfProducts, Size size)
: base(numberOfProducts, size)
{
_pricePerHour = 650;
_hours = new Dictionary<Size, int>() { { Size.small, 125 }, { Size.medium, 100 }, { Size.large, 60 } };
}
public override int TotalHours
{
get
{
if (_size == Size.small)
return _hours[_size] * _numberOfProducts;
if (_size == Size.medium)
return (_hours[Size.small] * 2) + (_hours[Size.medium] * (_numberOfProducts - 2));
return (_hours[Size.small] * 2) + (_hours[Size.medium] * (8 - 2)) + (_hours[Size.large] * (_numberOfProducts - 8));
}
}
}
public enum Size
{
small, medium, 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)
{
Project project = new Project((int)comboBoxNumberOfProducts.SelectedItem);
labelPrice.Text = project.GetPrice().ToString();
labelWriterHours.Text = project.Writing.TotalHours.ToString();
labelAnalysisHours.Text = project.Analysis.TotalHours.ToString();
}
}
At the end is a simple current calling code in the change event for a combobox that set size... (BTW, I don't like the fact that I have to use several dots to get to the TotalHours at the end here either, as far as I can recall, that violates the "principle of least knowledge" or "the law of demeter", so input on that would be appreciated too, but it's not the main point of the question)
Regards,
Anders