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 h
A good start would be to extract the conditional statement into a method(although only a small method) and give it a really explicit name. Then extract the logic within the if statement into their own methods - again with really explicit names. (Don't worry if the method names are long - as long as they do what they're called)
I would write this out in code but it would be better for you to pick names.
I would then move onto more complicated refactoring methods and patterns. Its only when your looking at a series of method calls will it seem appropriate to start applying patterns etc..
Make your first goal to write clean, easy to read and comprehend code. It is easy to get excited about patterns (speaking from experience) but they are very hard to apply if you can't describe your existing code in abstractions.
EDIT: So to clarify - you should aim to get your if statement looking like this
if( isBox() )
{
doBoxAction();
}
else if( isSquirrel() )
{
doSquirrelAction();
}
Once you do this, in my opinion, then it is easier to apply some of the patterns mentioned here. But once you still have calculatios etc... in your if statement, then it is harder to see the wood from the trees as you are at too low of an abstraction.
You don't need the Factory if your subclasses filter themselves on what they want to charge for. That requires a Project class to hold the data, if nothing else:
class Project {
TaskType Type { get; set; }
int? NumberOfHours { get; set; }
}
Since you want to add new calculations easily, you need an interface:
IProjectHours {
public void SetHours(IEnumerable<Project> projects);
}
And, some classes to implement the interface:
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;
}
}
}
}
The calling code can either dynamically load IProjectHours
implementors (or static them) and then just walk the list of Project
s through them:
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);
I would go with a strategy pattern derivative. This adds additional classes, but is more maintainable over the long haul. Also, keep in mind that there are still opporunities for refactoring here:
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";
}
}
I would tend to start with an enumeration ProjectSize {Small, Medium, Large}
and a simple function to return the appropriate enum given a numberOfManuals. From there, I would write different ServiceHourCalculators
, the WritingServiceHourCalculator
and the AnalysisServiceHourCalculator
(because their logic is sufficiently different). Each would take a numberOfManuals, a ProjectSize, and return the number of hours. I'd probably create a map from string to ServiceHourCalculator, so I could say:
ProjectSize projectSize = getProjectSize(_numberOfManuals);
int hours = serviceMap.getService(_serviceType).getHours(projectSize, _numberOfManuals);
This way, when I added a new project size, the compiler would balk at some unhandled cases for each service. It's not all handled in one place, but it is all handled before it will compile again, and that's all I need.
Update I know Java, not C# (very well), so this may not be 100% right, but creating the map would be something like this:
Map<String, ServiceHourCalculator> serviceMap = new HashMap<String, ServiceHourCalculator>();
serviceMap.put("writing", new WritingServiceHourCalculator());
serviceMap.put("analysis", new AnalysisServiceHourCalculator());
this is a common problem, there are a few options that i can think of. There are two design pattern that come to mind, firstly the Strategy Pattern and secondly the Factory Pattern. With the strategy pattern it is possible to encapsulate the calculation into an object, for example you could encapsulate your GetHours method into individual classes, each one would represent a calculation based on size. Once we have defined the different calculation strategies we wrap then in a factory. The factory would be responsible for selecting the strategy to perform the calculation just like your if statement in the GetHours method. Any way have a look at the code below and see what you think
At any point you could create a new strategy to perform a different calculation. The strategy can be shared between different objects allowing the same calculation to be used in multiple places. Also the factory could dynamically work out which strategy to use based on configuration, for example
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);
}
}