If you do TDD or write unit tests you soon start to feel reluctant about parameters and scalar data types. Why?
Consider the function below. Is it easy to test? Would it be created if TDD were used?
bool result = f(int p1, long p2, Object p3);
Pretend that the function above lives in a business layer and determines whether a customer can get a discount a particular month given a set of conditions embodied in the object
otherConditions. Thus the above could be rewritten as:
bool eligibleForDiscount = canCustomerGetDiscount(int month, long customerNumber, Object otherConditions);
Without thinking about it, we would most likely refactor it by introducing types for month and customer number. This would save us from cluttering the first lines of the function with code related to checking the validity of those parameters. Some people would call this object-oriented programming, others would stretch as far as calling it domain-driven design. Irrespectively of the term chosen, the function above would most likely be refactored into something like:
bool eligibleForDiscount = canCustomerGetDiscount(Month month, CustomerNumber customerNumber, Object otherConditions);
While at it, we would probably discover that this code needs to live in a context, and we would place it in a class that handles business rules related to discounts. That class, whatever its name and function, would assume the responsibility of handling discounts, which would make the
otherConditions parameter unnecessary. Of course this parameter was never actually an
Object, rather another class in the business domain. However, I didn’t want to complicate the example.
= discountManager.canCustomerGetDiscount(Month month, CustomerNumber customerNumber);
At this point we could feel quite happy about the function.
- It has a good name
- Its invocation honors the Single Responsibility Principle (SRP), as the
DiscountManager’s only responsibility seems to be handling discounts, while validation and domain-compliance are handled in
- It doesn’t take too many parameters
- It feels quite testable (based on its signature, at least)
But what did we actually do to make this happen? Is there a way to formalize these refactorings?
It turns out there is, although we have to borrow a definition from the testing domain.
I came across an article that mentions quite an interesting metric: The Domain-to-Range Ratio. It’s defined as the cardinality of the domain of the specification over the cardinality of the range:
DRR = |D| / |R|
Without being too formal about it, we can say that the lower the DRR, the better. For example:
|D| = cardinality of a 32-bit integer type, |R| cardinality of a 1-bit boolean type, which would produce a function like f(int32) → bool
Is that testable? How many unit tests do you need to make sure that the function behaves correctly? One, ten, or 232?
Returning to the discount function, we see that its DRR would be a scalar product of cardinalities in the magnitude of 232, 264, and the combined DRR of all members of
otherConditions that are queried by the function.
While it’s not apparent how complex the condition object actually is (it could be as simple as a boolean flag), we see that introducing a
Month class reduces the cardinality of the first parameter from 232 to 12, and that introducing a
CustomerNumber class at least halves the cardinality of the second, as customer numbers are most likely not negative.
In this light, we see that refactorings like:
Scalar → enum, Scalar → object, Remove parameter, start making sense in a formal sort of way.
It also happens that such refactorings make the code object-oriented…