Re: Downcasting - whats the problem?

From: Robert C. Martin (unclebob_at_objectmentor.com)
Date: 05/22/04


Date: Sat, 22 May 2004 10:08:03 -0500

On Sat, 22 May 2004 03:43:01 GMT, "Daniel T."
<postmaster@eathlink.net> wrote:

>Personally I'm surprised that RCM is getting away with it without
>everyone jumping all over his case. :-)

The issue is that there are certain structures in object oriented
designs that cause us to lose static type information through
up-casts. In order to regain that static type information later, we
have to use downcasts.

One case that you and I have argued about before is the Payroll
example to be found in Chapter 19 of "Agile Software Development:
Principles, Patterns, and Practices" (www.objectmentor.com/PPP)

In Figure 19-8 and listing 19-15 we see the canonical case of a
downcast being used to resolve type information that was lost in a
dual-inheritance hierarchy.

 |Employee|------>|PaymentClassification|
                         A
                         |
                 +-------+-------+
                 | | *
 |SalariedClassification| |HourlyClassification|<#>->|TimeCard|

The employee holds an object that describes it's pay classification.
One represents a salaried payment, the other an Hourly payment. The
HourlyClassification holds on to a set of time cards.

However, the Employee does not know which of these two it possesses.
It has lost this information due to an upcast. This is good because
we want to be able to pay the employees through a simple algorithm
that looks like this:

void PaydayTransaction::Execute()
{
  list<int> empIds;
  GpayrollDatabase.GetAllEmployeeIds(empIds);
  
  list<int>::iterator i = empIds.begin();
  for (; i != empIds.end(); i++) {
    int empId = *i;
    if (Employee* e = GpayrollDatabase.GetEmployee(empId)) {
      if (e->IsPayDate(itsPayDate)) {
              Paycheck* pc =
                  new Paycheck(e->GetPayPeriodStartDate(itsPayDate),
                                          itsPayDate);
              itsPaychecks[empId] = pc;
              e->Payday(*pc);
      }
    }
  }
}

void Employee::Payday(Paycheck& pc)
{
  double grossPay = itsClassification->CalculatePay(pc);
  double deductions = itsAffiliation->CalculateDeductions(pc);
  double netPay = grossPay - deductions;
  pc.SetGrossPay(grossPay);
  pc.SetDeductions(deductions);
  pc.SetNetPay(netPay);
  itsPaymentMethod->Pay(pc);
}

This is nice because the Employee object does not depend upon the type
of payment classification. It simply invokes a general algorithm to
pay employees, and lets polymorphism do all the interesting selection
work.

However, we have a problem. There are transactions that need to add
time cards to employees via their id numbers. The database returns
employees, but we need to be sure that they are Hourly. These
transactions form another hierarchy which has similarity to the
PayClassification hierarchy. The AddTimeCardTransaction class
pertains specifically and only to Employees who happen to be holding
the HourlyClassification derivative of PaymentClassification.

           |Transaction|------>|Employee|
                 A
                 |
           |AddTimeCard|------>|HourlyClassification|

What this means is that the Execute method of TimeCardTransaction must
downcast the PaymentClassification it finds in the Employee to an
HourlyClassification.

void TimeCardTransaction::Execute()
{
  Employee* e = GpayrollDatabase.GetEmployee(itsEmpid);
  if (e){
    PaymentClassification* pc = e->GetClassification();
    if (HourlyClassification* hc =
        dynamic_cast<HourlyClassification*>(pc)) {
      hc->AddTimeCard(new TimeCard(itsDate, itsHours));
    } else
      throw("Tried to add timecard to non-hourly employee");
  } else
    throw("No such employee.");
}

How would you eliminate this downcast without adding other undesirable
dependencies to the system?

-----
Robert C. Martin (Uncle Bob)
Object Mentor Inc.
unclebob @ objectmentor . com
800-338-6716

"The aim of science is not to open the door to infinite wisdom,
 but to set a limit to infinite error."
    -- Bertolt Brecht, Life of Galileo



Relevant Pages

  • Re: design problem
    ... Revealing the existence of strategies, both interface and each ... and the fact that employee has only one payment model ... that you need to have many payment models active at the same time for the ... need to move employees from one payment model to another, ...
    (comp.object)
  • Re: A funny thing happened...
    ... one only shows what the payment was for and not who made the purchase on ... reimbursing against a receipt, the "proper way" is to 'look through' who ... For example an employee may buy stamps, ... providing the employee with either earnings or expenses because the employee ...
    (uk.rec.scouting)
  • Sufficient reason to de-normalize a field?
    ... I've got a great database design. ... I have a form where I save payment information and the payee might ... of the payee when it is an employee. ...
    (microsoft.public.vb.database)
  • Re: A funny thing happened...
    ... recording in the books who the payment goes to, ... reimbursing against a receipt, the "proper way" is to 'look through' who ... I record who I actually paid, who the supplier was and what it was that was purchased/ paid for. ... For example an employee may buy stamps, ...
    (uk.rec.scouting)