The YAGNI Principle
Different design principles are aimed at solving contradictory tasks in software design. One may say that different principles “pull” the design in different directions, and we must find the right vector which is most useful in a specific case. The SRP pushes us in the direction of a simple solution, and the OCP drives us towards the isolation of components, while the DIP is directed to building a correct relationship between types.
Following one principle may lead to violating another principle. For instance, any inheritance can be viewed as a violation of the SRP, since a whole group of classes is now responsible for a single responsibility (drawing figures). The application of the DIP and OCP, which often require inheritance, may lead to additional “seams”, i.e. interfaces/base classes in the system, which once again will lead to a violation of the SRP and/or ISP.
Such a relationship between the principles is not fixed. In a simple case, extraction of hierarchy of figure for drawing is a violation of the SRP, since “drawing” in the first iteration may well be the display of text on the dashboard, and spreading that information across multiple classes would be excessive. But as the system’s complexity grows, an inheritance hierarchy becomes useful in terms of SRP, since the representation of each figure turns out to be so complicated that the notion of “responsibility” would change too. If initially a “single responsibility” was to represent all figures, now one responsibility would be split into a set – “representation of circle”, representation of square”, etc.
YAGNI (You Aren’t Gonna Need It) is a more fundamental principle (“a higher-order principle” or “metaprinciple”), which can help you understand when you should follow principles/patterns/rules and when you should not.
The YAGNI principle is based on the following observations:
- Programmers, like all other people, are not good at forecasting the future.
- Costs incurred now may or may not be justified in the future.
- No flexible solution will ever be flexible enough.
These observations lead to the following conclusions: any attempt to create a flexible solution at early development stages is doomed to creating an overcomplicated solution. This is because at early stages we are not yet aware what changes the system will require and it is unclear where to “build a safety net” for future changes.
Since at early stages we don’t know what flexibility we will need exactly, we may foresee flexibility where it would not be necessary. We may build in a data layer replacement, but then due to “leaky abstractions” we will nevertheless lock the system to a certain database, or will never need such a feature at all. We may build a “framework” for command line arguments parsing to be used by a single application, but the cost of building it into another application would be so high that no one will do that.
A good design means that you get a simple solution where changes in requirements lead to the linear increase of effort.
The easiest way to achieve that goal is evolutionary design: we begin from splitting the system into large components but without adding unnecessary layers of abstraction. We won’t need base classes unless there are at least 2 or 3 descendants. And even if such descendants “might appear in the future”, you should build the type hierarchy just when that future arrives.
There is a simple acid test for the YAGNI principle: unnecessary abstractions layering (or whatever overcomplicating) is only justified if its cost in the future will be much higher than now.
Investments into a thoroughly thought-out library API would be justified, as the cost of making changes is extremely high. The cost of extraction of an interface/base class in the application would actually be the same either today or in a year. But in a year’s time such extraction will be more feasible economically and in practical terms. We have not done unnecessary work before time, and therefore we can focus on something actually important right now. Besides, in a year we will have more information and understanding what the base class or interface should be like, because they will be based on real not imaginary requirements.
Solving problems as they arise will help you focus on tasks that are critical today and avoid work that might prove unnecessary in the future.
Expert in .Net, Ñ++ and Application Architecture