Trying to avoid too much knowledge about a class in a different class

124 views Asked by At

I need help with the design of an application that I am am writing. In the application employees can book their work within a project (the so called booking object). The booking objects optionally can have budgets. When the budget of a booking object is exhausted then employees must not be able to book on that booking object.

The project leader should set the budget's amount in one of two ways:

  1. setting a decimal number which represents some monetary amount
  2. setting the amount through man-days

For example it is legal to say that the booking object X has a budget of 10,000$. It also legal to say that the budget of X consists of 2.5 man-days for a senior, 3 man-days for a junior, and 5.5 man-days for a support. Internally the man-day amount is calculated to a monetary amount (2.5 times price of a senior + 3 times price of junior + 5.5 times price of support). The man-day amount is somehow a calculator but it should be persisted, so not an UI thing only.

The question is how to inject the price(s) into the man-day budget without the man-day budget knowing too much of the booking object. Each booking object is allowed to have differernt prices for the levels ("senior", "junior", "support", etc.).

I came up with following classes.

// could be designed as an interface, too
public abstract class Budget
{
  public abstract decimal Amount { get; }
}

Two special classes for the monetary amount and man-day amount.

public class MonetaryBudget : Budget
{
  private decimal amount;
  public MonetaryBudget(decimal amount)
  {
    this.amount = amount;
  }
}

public class ManDayBudget : Budget
{
  private decimal amount;
  public ManDayBudget(IEnumerable<ManDay> manDays)
  {
    this.amount = manDays.Sum(md => md.ManDays * PriceOf(md.Level));
  }
}

The consumer of the classes can now write something like this:

var bo_x = new BookingObject();
bo_x.Budget = new MonetaryBudget(10000);
bo_x.Budget = new ManDayBudget(new List<ManDay>
{
  new ManDay { ManDays = 2.5, Level = "senior" },
  new ManDay { ManDays = 3.0, Level = "junior" },
  new ManDay { ManDays = 5.5, Level = "support" }
});

var bo_y = new BookingObject();
bo_y.Budget = new ManDayBudget(new List<ManDay>
{
  new ManDay { ManDays = 2.5, Level = "senior" },
  new ManDay { ManDays = 3.0, Level = "junior" },
  new ManDay { ManDays = 5.5, Level = "support" }
});

// bo_x.Budget.Amount == bo_y.Budget.Amount is not guaranteed to evaluate to true

For cenvenience I have let out the implementation of the Budget.Amount property in the concrete classes and also the definition of ManDay.

There will be requirement that other objcets in the application will have a budget, too. It's not crystal clear yet. How should I design my classes so that ManDayBudget does not know too much about the price finding logic, better does not know anything about booking objects. I have the feeling that I am missing a class which does the calcualtion.

EDIT:

To be clear what the consumer should and can do:

var bo_x = new BookingObject();
bo_x.Budget = new ManDayBudget(new List<ManDay>
{
  new ManDay { ManDays = 2.5, Level = "senior" },
  new ManDay { ManDays = 3.0, Level = "junior" },
  new ManDay { ManDays = 5.5, Level = "support" }
});

var bo_y = new BookingObject();
bo_y.Budget = new ManDayBudget(new List<ManDay>
{
  new ManDay { ManDays = 2.5, Level = "senior" },
  new ManDay { ManDays = 3.0, Level = "junior" },
  new ManDay { ManDays = 5.5, Level = "support" }
});

The monetary amount of the booking object X (bo_x.Budget.Amount) can be 7.600 and the one of Y can be 9.200 because for each booking object different prices are defined. The consumer says this much man-day budget and nothing more. But the monetary amount must be calculated somehow without too much knowledge about BookingObject in order to reuse the class later.

EDIT 2:

X could have a price set for senior to 100, for junior to 80, etc. and Y could have set price for senior to 125, for junior to 100, etc. Therefore even the man-day amount is set to the same amount of days the monatary amount of the two booking objects differ.

2

There are 2 answers

4
Fendy On

Your question is a bit unclear.

If I understand that you only need to inject the base budget price, looks like the solution can be a bit simple. Please note, this is considered as a decorator pattern though.

public class ManDayBudget : Budget
{
  private decimal amount;
  public ManDayBudget(IEnumerable<ManDay> manDays, Budget baseBudget)
  {
    decimal baseAmount = baseBudget.Amount;
    this.amount = manDays.Sum(md => md.ManDays * PriceOf(md.Level));
    // do other things with baseAmount
  }
}

EDIT:

Eventhough it is still unclear, I can grasp some of the requirement. Let's say that this is the current design of BookingObject:

public class BookingObject{
    public decimal Price { get; set; }
    public Budget Budget{ get{ /*return */ }
        set{
            value.BookingObject = this;
            /*set to private*/
        }
    }
}

With this design, the Budget.Amount will obviously relies on BookingObject's Price for its Budget.Amount calculation, making a dependency between each. It is worse, that each derived budget can has different calculation parameter (static monetary, man days, etc), making it harder to implement.

Reconstruct the Budget class

The current Budget class is troublesome. It has the logic of Amount calculation but need the Price from other source. You can re-design the Budget a bit like this, please note you can do the same with interface:

public abstract class Budget{
    public abstract decimal GetAmount(decimal price);
}

You can re-design the input parameter to be another contract like IPrice if needed. Then you can re-design the BookingObject to be as following:

public class BookingObject{
    public decimal Price { get; set; }
    public Budget Budget{ get; set; }
    public decimal Amount{
        get{
            return this.Budget.GetAmount(this.Price);
        }
    }
}

The sample of the new ManDayBudget and MonetaryBudget.

public class ManDayBudget : Budget{
    public ManDayBudget(IEnumerable<ManDay> manDays)
    {
        this.manDays = manDays;
    }
    private readonly IEnumerable<ManDay> manDays;
    public override decimal Amount(decimal price){
        return price * // the calculation here
    }
}

public class MonetaryBudget : Budget{
    public MonetaryBudget(decimal amount)
    {
        this.amount = amount;
    }
    private readonly decimal amount;
    public override decimal Amount(decimal price){
        return amount;
    }
}

This design has flaws though. First, now there are 2 ways to retrieve the budget amount.

  1. decimal amount = bo.Amount
  2. decimal amount = bo.Budget.Amount(bo.Price);

Second, the BookingObject has dependency to budget class. If the Budget class is constructor injected, then it will making the booking object instantiation harder.

3
Jason Evans On

I think you need to look at the Decorator design pattern. I will try to come up with a code example the represents your code; for now, here some links to help get you started:

http://www.codeproject.com/Articles/42042/Decorator-Design-Pattern http://www.dofactory.com/Patterns/PatternDecorator.aspx

EDIT:

Your decorator could look like this:

public class BudgetDecorator : Budget
{
    private readonly IPriceCalculator _calc;

    private Budget budget;

    public BudgetDecorator(Budget budget, IPriceCalculator calc)
    {
        _calc = calc;
        budget = budget;
    }

    public override decimal Amount
    {
        get
        {
            ManDayBudget dayBudget = budget as ManDayBudget;
            
            if (dayBudget != null)
            {
                return dayBudget.ManDays.Sum(md => md.ManDays * _calc.PriceOf(md.Level));
            }

            return budget.Amount;
        }
    }
}

You pass into its ctor an object that does the price calculation. This is how the budgets themselves will never need to know how the price is calculated, another object takes care of that. Then you use the decorator like so:

var b2 = new ManDayBudget(new List<ManDay>
{
    new ManDay { ManDays = 2.5, Level = "senior" },
    new ManDay { ManDays = 3.0, Level = "junior" },
    new ManDay { ManDays = 5.5, Level = "support" }
});

var d = new BudgetDecorator(b2, null /* Replace with object that does calcuation. */);

Console.WriteLine(d.Amount);

I've changed the decorator slightly. also if you change the ManDayBudget a bit:

public class ManDayBudget : Budget
{
    public IEnumerable<ManDay> ManDays { get; set; } // Add this property.

    public ManDayBudget(IEnumerable<ManDay> manDays)
    {
        this.ManDays = manDays;        
    }
    
    // Rest of code.....
}

EDIT:

Forgot to mention, that the decorator example if a geernic one, in that it does not care about the type of Budget being passed into it. Another way to achieve what you want is to change the constructor of the ManDayBudget class to accept a ICalculator object and use it inside the ManDayBudget class, rather then using a decorator. It just depends on how generic you wanted the code to be like.