Things to trigger a change in your software design
Software design is iterative, the only case this is not going to happen is if you don’t need to add more features and there are no bugs. I…
Software design is iterative, the only case this is not going to happen is if you don’t need to add more features and there are no bugs. I haven’t seen this situation yet.
I like to do evolutionary design instead of up-front. Furthermore, I try to follow YAGNI principle, so I like to think in the future of my code, but I don’t write anything that I don’t need until I have a proof that I need it. Majority of times I discover a very different approach that my first one, even I realize my first approach had no sense at all.
This can sound strange, but I really think it helps a lot to avoid overengineering. So basically I prefer to create a small problem in the code and solve it when it is still small (refactoring). We cannot wait too much because if the issue is too big, the cost of fixing it is going to be very high and difficult to justify, but we need to be sure what to do. You can understand it playing with the code and use code smells to select your tradeoffs.
I think this is one of the reasons people think it’s better to design up-front, just because you could anticipate the issue before it’s created. This is a fantasy, none is able to predict the future.
Small problems
The code changes based on the history of the things done (tech debt, architecture, culture in the team) but also based on the new features we want to introduce, so a design done one year ago is probably not good today.
In my opinion, trying to write the perfect code the first time is usually one of the biggest issues of development today, because this is not usually revisited by the team. Design is iterative, you cannot design the perfect thing for the next five years, no one can.
The key is having small problems in the code not big problems, so now we can change the game, we just need to fix them when we see them to grow.
In some sense, I think this democratizes the software design. No need to be in the company years and years to decide about how to shape things. You just need to read your code and learn how to identify small problems, after that you need tools to fix those problems.
Depending on how good are you on identifying these issues, you can take more risk and trying to apply solutions earlier. This can be done when you have a lot of experience, if you are not, just wait until the issue in the code is clear and find a way to solve it. Learning how to fix that issue is a majority of times a question of memorizing some predefined steps (Refactoring book), others it’s a question of testing different approaches and select your tradeoffs.
Unit tests
Tests are for me a great source of issues in my design. Unit tests are clients of parts of my code, they will need to play with the production code. I prefer to not avoid the issues my design has inside tests, hiding them, this is one of the reasons I don’t like builders.
It’s like a way to play the role of the developer that is going to use my code, designing the experience of using my code is also software design, probably is one of the biggest parts of software design.
Some examples of feedback from my tests:
To create a scenario for a test, I have to create several complicated classes and interactions between them. Perhaps this setup needs to be done inside the class/module under test, perhaps I can simplify that experience.
Multiple asserts in my test, this usually means for me too many responsibilities in the behavior tested. Perhaps we have to split all the things done by the behavior tested.
Big DataClumps with a lot of nulls to be created. This means for me that we are not honoring the Interface Segregation principle. Perhaps that data clump is very coupled to a lot of other behaviors, perhaps we are not following Demeter Law.
Explosion of tests, if you are testing a method or a function, and you need a lot of tests, some of them watching different aspects of the method, then your method probably is not honoring the Single Responsibility Principle. Try to create a collaborator, test it independently and mock in the first one that will solve the explosion of tests.
Nulls in the constructor, if you need to create a class with nulls because some collaborators are not used perhaps your class has low cohesion, another symptom of the need to create a new class. This is why I prefer in test to use nulls as dummy values, for things not used (to mark the real data we require for the test).
There are more things, but I think these are for me the more commons.
Production code
First thing for me when I have to add a behavior from the point of view of the design is where do I have to put the code to solve the problem. Not writing the code, but focusing on where is a good place to do it. Identifying the part where your code should work is a good exercise of investigation.
I love GOF patterns I think they are great solutions, but I think it’s better to start by GRASP patterns (responsibility patterns). Trying to understand who is responsible for doing what. They can give you some why's, instead of how's:
Information Expert — The behavior goes to the object with the data that the behavior manage
Creator — Who is the responsible for creating an object?. Let’s select it minimizing coupling and maximizing cohesion
Controller — The controller pattern assigns the responsibility of dealing with system events to a non-UI class that represents the overall system or a use case scenario.
Indirection — The fundamental theorem of software engineering (FTSE) “We can solve any problem by introducing an extra level of indirection.”
Low Coupling — How much a unit of our system is related to other units of our system. A class is coupled with another class if the first one uses the second one, for example.
High cohesion — How much a unit, module or class is related to the data is using. For example in a class with three attributes if the majority of methods use just two and just one use the other perhaps that info and that method is not related to the class, this is an example of low cohesion.
Polimorphism — Move the logic to those children in your class hierarchy that decide the behavior.
Protected Variations — ifs in the code can be translated to behaviors under an interface
Pure fabrication — if I don’t know where to put a method or a class, let’s put it in a class not related to the domain problem but solving the issue.
Refactoring
Refactoring is a great way to design, if you use refactoring to solve small issues in your code and not making them too big you will be able to design in each change of your codebase. The effect of not refactoring is slowness trying to hack the current design. Each time it will be more difficult to add a new feature.
Refactoring book from Martin Fowler introduce also another great tool:
A lot of small refactors can create a big one. A lot of small steps make a big change easy to do.
In that book, there are a lot of issues you can identify to improve the design and the steps to solve them in a secure and testable way.
I’m not saying this is the best way to design, I think it is the cheaper one, but I don’t have data to justify it. What I’m saying is that you will need to learn how to improve your code anyway, so why not trying also to design using these refactoring techniques?.