When we start programming professionally for some company it’s too easy to fall in the situation of implementing something with too many just in case’s, or what if’s.
I used to say that we were taught that way when we studied Software Engineering or Programming, but now I’m not that sure about it. I think that we just didn’t get the point. When we were told about writing reusable code we automatically thought: “okay, this is about writing code today so that I can reuse it tomorrow, right?”.
Then we realize that we really don’t know what the requirements will be tomorrow, and we end up with lots of awesome classes and methods and interfaces we wrote that are now completely useless. Now we need to maintain them and they are difficult to adapt, so in the short term they start to smell badly and within a couple of months that code is completely rotten.
So, the solution seems to be easy at first:
First thing first: we software engineers are cool when inventing acronyms; aren’t we?
Okay, but let’s go to the point…
Then you ask some workmate and he suggest an approach that is “pityful” SRP but a “confident 100%” OCP.
Regarding object oriented detailed design: inheritance, interfacing, delegation or just composition?
Which one is better?
Let’s consider an example like the following: you have to implement a system that handles employees. Some employees are paid in a month rate and other ones by hour, some other ones have a basic income and the rest depends on the sales rate they achieve in that month. Finally some other ones can choose how they want to be paid: by hour or in a month rate.
Regarding the database, it’s easy to identify an inheritance in the Entity Relationship diagram. Inheritance is designed in one of 3 ways:
- Implement the parent table and the children tables
- Implement only the parent table
- Implement only the children
Regarding the code, you can go for the Template Method pattern and use inheritance with an abstract Employee class and the actual Employee types inheriting from it or go for a Strategy pattern and use delegation (assuming .NET programming) to calculate the income.
That’s what it comes to my mind as a first approach.
So, which one is better?
It depends on what? We implement for today, don’t we? So there must be a perfect solution to this problem.
But silver bullets never ever exist in software, d’you remember?
The trick here for experienced developers is to anticipate to the change somehow. If I was in such a situation like the one depicted above I would ask my customer something like:
- Is it possible that a customer changes his / her income mode?
- Is it possible that new payment options arise?
- Is all the employees data required to be loaded in every operation?
Going back to the database, we know that if we use only one EMPLOYEE table we will have lots of nulls for those columns that don’t apply for some specific payments. That’s not cool, we are wasting lots of disk space. If we use the “children-only” option then we will have lots of redundancy in the common columns (again, a waste). Finally, if we use the parent table AND the children we will need a JOIN to load all the employee information, which may damage the performance of the overall system.
About the code, we all know that inheritance is hard to maintain because if new children classes appear and they don’t fit in the common parent we start to have several levels of hierarchy and in a couple of years we have a hierarchy too rigid, hard to maintain, hard to test and hard to refactor without breaking anything. On the other hand, if payment options don’t change then the Template Method may be easier to understand and more natural and elegant to use than the Strategy pattern.
So, software designs aren’t often good for every single situation. Solutions are good for some requirements in a certain point in time, and we should base our decisions on that. As long as we have a clean design, if eventually we need to do a change and then we realize that our code is not prepared for that kind of change, we refactor with the support of our tests. But let’s face it: there’s no perfect design. It depends on the requirements. Even the very best piece of software design may fail the SOLID principles under some twisted change of requirements. Let’s embrace refactor, then.
The important thing to get here is that, when we need to give some solution to a certain problem we need to find several solutions so that the right questions arise and therefore those questions will lead us to choose the more appropriate solution according to the answers we get. Then we choose the simplest, cleanest, SOLID-est solution ever possible for today’s requirements, anticipating tomorrow changes if (and only if) we have a strong certainty what those are.