As I’ve progressed in my career, I’ve come across more and more projects where “a class design is considered good enough if it works”. With such a belief, programmers often come up with myopic class designs that can create maintenance nightmares.
For the sake of this post, I will provide a simple example, albeit a real-life one. Consider an application whose purpose is to deal with payments. A core domain class in such an application is a Money class. After all processing payments is impossible without Money.
An often seen version of the Money class looks like this:
This class leaks it’s internal state.
When you need to do business operations with this class, the problem manifests itself. Business logic with data-object styled classes such as above tend to be handled through specific Service/Transaction-Script classes.
While this is perfectly fine for extra-small applications, it is an no-no for bigger ones that have complex business logic, or one that is frequently updated with more and more features.
For example, the incident that I had seen involved
- having to know if the final invoice for a customer was above a specific threshold value of 275 EUR, and
- to do something very specific if that was the case.
Do note that this application was available across various European countries, even those that have not adopted the Euro currency.
The class where this was done looked pretty much like this:
In the particular case that I am referring to, this Service that you see above was implemented in two separate modules. Unbeknownst to the developer who had implemented this new feature, the amount of thresholdValue in function parameter was sent as Euros in one module, where as it was the local currency of the market in the other module.
The code nor the semantics made this explicit. It was a disaster waiting to happen.
Making it a bit better
Now, we could take the easy route, and merely rename the function parameter to thresholdValueInEuros to signify intent.
We can tweak it a bit more. In this iteration, we can make the function parameter an instance of the Money class itself. So now the function definition looks like this
Can we do more?
My problem with the above solution is that core Money related logic will be now spread across another two classes, the ConversionService (for conversion) and the BillCalculatingService (is one Money of greater numerical value than the other).
This means all these three classes are coupled.
We have taken a simple example here of a single class, but imagine a scenario where an entire application is written with data-objects like the Money class, and all business logic spread out in various Service classes. As projects grow with more and more features, there is more chances of introducing extremely coupled classes, making changes very cumbersome.
My preferred approach
The crux of my preferred approach is to create encapsulated classes that do not expose their internal state.
A different Money class implementation would look like this
There are two differences here
- both the value and the currency are not exposed through getters
- the value comparison is done through the Money class itself
This removes the need for Money comparison in the BillCalculatingService class, and the conversion logic in the ConversionService is invoked through the Money class too.
But I would take this a bit further. Rather than merely invoking the ConversionService method, I would move that logic elsewhere too.
With this approach, every time a new currency is introduced into the application (for example, a roll-out into a new country), having to specify the conversion factor becomes mandatory, since this will be a compile time check now.
Thinking even more in ‘Objects’
The last change I would think of is to convert the thresholdAmount parameter to a specific ThresholdAmount object.
Now if you think about it ThresholdAmount is-a Money, but the only difference is how it is initialized, the former being done through configuration. (Remember, this value was configurable)
This is why I think there can be a ThresholdAmount object.
Too much upfront design can backfire.
Too little can, too!
Finding the sweet spot is something that we need to learn, and often this happens with experience.
The key is to know when a refactor is needed, and then convincing your team and maybe your manager of the same. Well designed classes are often a serious tool to help you move faster in implementing new feature or avoiding bugs. The next person who maintains your code-base will thank you.