In a world of Domain Driven Design it's a common approach to have business entities represented as classes. So we create them plain at the beginning, with the only necessary set of fields. Like in this, a classic example of address entity.
class Address
{
string Line1;
string Line2;
string City;
string State
string ZipCode;
string Country;
}
Looks simple and neat.
Business evolves and requires new features, our app evolves and becomes more and more complicated. In response, we start adding new fields, properties, and methods. For example, we need to print address tags in a fancy manner and we end up adding something like this.
string ToString()
{
return $"{Line1} {Line2}\n{City}, {State} {ZipCode}";
}
Which is totally aligned with best practices of Object-Oriented Paradigm. ToString
method belongs to this data structure logically and we encapsulate it.
Overall, the intention of those practices is to reduce complexity. But, as feature set grows, we add more and more fields, properties, and methods to our classes, increasing it instead!
Let me give you a real example on that.
For whatever business reason, we need to check if the address is located in California. The initial approach is simple and we just check if Address.State
equals "California". But then we find out that our application is integrated with multiple vendors and not all of them implemented strict validation rules, so Address.State
can be equal to "CA" and maybe even something like "Cali".
We end up checking all possible combinations - don't forget upper and lower cases! - and maybe even switching to ZipCode lookup against the table from postal service.
Obviously, we don't want to have this logic duplicated all over the place and, following object-oriented principles, we encapsulate it in address class by creating another property or method.
Awesome.
Actually, not. The very next day we are asked about some specific logic for state Arizona and we create yet another property or method!
Weird, we have 50 states! Address class quickly becomes bloated.
The solution would be to split such feature related sets of properties and methods into separate classes so that we keep all of them clean and neat.
And we don't even break object-oriented principles here. We're just adding another dimension based on Single Responsibility Principle. Class Address
keeps business entity itself and other classes represent feature specific logic, state related checks in our case.
Now it's awesome.
How to do this? As usual, there are several ways.
Using inheritance. Like this.
class StateAwareAddress : Address { private static HashSet<string> californiaZipCodes = new HashSet<string>(); bool IsCalifornia { return californiaZipCodes.ContainsKey(this.ZipCode); } }
Inheritance creates additional coupling, but this is the only way to gain access to private data of
Address
. If there is no need for this, I'd prefer other options.
There is another downside in changes to the legacy code to make sure we instantiate proper addresses when necessary. If there is no access to some codebase, we're out of luck without deep copy.Using composition instead of inheritance
We're losing access to private data of a class but instantiation becomes explicit and clear, just create appropriate state checker and pass address object as a reference.class StateChecker { private Address address; private static HashSet<string> californiaZipCodes = new HashSet<string>(); StateChecker(Address address) { this.address = address; } bool IsCalifornia { return californiaZipCodes.ContainsKey(address.ZipCode); } }
Using extension methods
Some languages like C#, Java, Ruby allows to extend already instantiated objects with new functionality.static class StateCheckers { private static HashSet<string> californiaZipCodes = new HashSet<string>(); static bool IsCalifornia(this Address address) { return californiaZipCodes.ContainsKey(address.ZipCode); } }
Looks very similar to composition, except it's much easier and shorter. Less code is better!
Just keep in mind such classes and methods could bestatic
only. Which is not a problem in our case as we keepcaliforniaZipCodes
static anyway, as soon as this is dictionary data shared across instances.
Extra!
What if we decide to go international? Read the next post!