Every software developer with little experience understands the value of keeping things simple and stupid (KISS). You don't want to repeat yourself once you've learned how to use classes and functions, so keep things DRY. The goal of all of these principles is to reduce mental complexity in order to make software easier to maintain.
- Don’t repeat yourself (DRY)
The DRY principle is at the heart of software development. Our code is organized into packages and modules. We eliminate functions. We try to make the code reusable so that we can hopefully maintain it more easily.
Benefits: Reduced complexity. The more code you have, the more maintenance you'll have to do. DRY usually results in less code. This means that for typical changes, you only need to make one adjustment.
Risk: When you do it too often, the code tends to become more complex.
Tooling support: There are programmes that can detect duplicated code. There is one for Python.
pylint --disable=all --enable=similarities src
- You Ain’t Gonna Need It (YAGNI)
The realization that too much abstraction actually harms maintainability is referred to as YAGNI. I'm looking at you, Java developers!
Benefit: Reduced complexity. The removal of abstractions clarifies how the code works.
Risk: You will have difficulty extending your software if you use YAGNI too much and thus make too few abstractions. Furthermore, junior developers may tamper with the code in an unfavourable way.
Tooling support: None
- Keep it Simple and Stupid (KISS)
KISS can be applied to a variety of situations. Although some solutions are smart and solve the problem at hand, the dumber solution may be preferable because it has less of a chance of introducing problems. This may occasionally be less DRY.
- Principle of Least Surprise
Design your systems so that the location of feature implementation, as well as the behaviour and side-effects of a component, are as unsurprising as possible. Keep your coworkers informed.
Benefit: Reduced complexity. You ensure that the system's mental model corresponds to what people naturally assume.
Risk: You may need to break DRY in order to complete this task.
Tooling support: None. However, there are some indications that this was not followed:
You're explaining the same quirks of your system to new colleagues over and over.
You'll have to look up the same topic several times.
You feel compelled to document a topic that is not inherently difficult.
- Separation of Concerns (SoC)
Every package, module, class, or function should be concerned with only one issue. When you try to do too many things, you end up doing none of them well. In practice, it is most visible in the separation of a data storage layer, a presentation layer, and a layer containing the business logic. Other types of concerns could include input validation, data synchronization, authentication, and so on.
Benefit: Reduced complexity: It's usually easier to see where changes need to be made.
There should be fewer unfavourable side effects to consider.
People can work in parallel without encountering a slew of merge conflicts.
Risk: If you go overboard on SoC, you will almost certainly violate KISS or YAGNI.
Tooling support: Cohesion can be measured by counting how many classes/functions from other packages are used. A large number of externally imported functions may indicate SoC violations. A large number of merge conflicts may also indicate a problem.
- Fail early, fail loud
As developers, we must deal with a wide range of errors. And it's unclear how to deal with them, especially for beginners.
To fail early is a pattern that has helped me a lot in the past. That is, the error should be recognized very close to the location where it can occur. User input, in particular, should be validated directly in the input layer. However, network interactions are another common scenario in which error cases must be handled.
The other pattern is to fail loudly, which means to throw an exception and log a message. Don't simply return None or NULL. Exceptions should be made. Depending on the type of Exception, you may also want to notify the user.
Benefit: Easier to maintain because it is clear where functionality belongs and how the system should be designed. Errors occur earlier, making debugging easier.
Tooling support: None
- Defensive Programming
The term "defensive programming" is derived from the term "defensive driving." Defensive driving is defined as "driving to save lives, time, and money regardless of the circumstances around you or the actions of others." Defensive programming is the concept of remaining robust and correct in the face of changing environmental conditions and the actions of others. This can mean being resistant to incorrect input, such as when using an IBAN field in a database to ensure that the content stored there contains an IBAN. It may also imply making assertions explicit and raising exceptions if those assertions are violated. It may imply making API calls idempotent. It may imply having a high level of test coverage in order to be defensive against future breaking changes.
Three fundamental rules of defensive programming
- Until proven otherwise, all data is relevant.
- Unless proven otherwise, all data is tainted.
- Until proven otherwise, all code is insecure.
"Shit in shit out" is an alternative to defensive programming.
Benefit: Higher robustness
Risk: Increased maintenance as a result of a more complex/lengthy code base
Tooling support: Check your coverage to see how much of your unit tests are covered. Try mutation testing if you want to go crazy. There is chaos engineering for infrastructure. Load testing is done.
The SOLID principles provide guidance in the areas of coupling and cohesion. They were designed with object-oriented programming (OOP) in mind, but they can also be applied to abstraction levels other than classes, such as services or functions. Later on, I'll simply refer to those as "components."
Two components can be linked in a variety of ways. For example, one service may require knowledge of how another service operates internally in order to perform its functions. The more component A is dependent on component B, the more A is coupled with B. Please keep in mind that this is an asymmetrical relationship. We don't care about the direction of coupling.
One module's high cohesion indicates that its internal components are tightly linked. They are all about the same thing.
We strive for loose coupling and high cohesion between components.
The principle of single-responsibility
"A class should never change for more than one reason."
The roles of software entities such as services, packages, modules, classes, and functions should be clearly defined. They should typically operate at a single abstraction level and not do too much.
One tool for achieving separation of concerns is single responsibility.
I'm not aware of any automated tools for detecting violations of the principle of single responsibility. You can, however, try to describe the functionality of the components without using the words "and" or "or." If this does not work, you may be breaking the law.
The open-close principle
"Software entities... should be open to extension but not to modification."
If you modify a component on which others rely, you risk breaking their code.
The substitution principle of Liskov
"Functions that use pointers or references to base classes must be able to use derived class objects without being aware of it."
Benefit: This is a fundamental assumption in OOP. Simply follow it.
The principle of interface segregation
"A number of client-specific interfaces are preferable to a single general-purpose interface."
Benefit: It's easier to extend software and reuse interfaces if you have the option to pick and choose. However, if the software is entirely in-house, I would rather create larger interfaces and split as needed.
Risk: Violation of KISS.
The principle of dependency inversion
"Rely on abstractions rather than concretions."
In some cases, you may want to operate on a broader class of inputs than the one you're currently dealing with. WSGI, JDBC, and basically any plugin system come to mind as examples. You want to define an interface on which you will rely. The components must then implement this interface.
Assume you have a programme that requires access to a relational database. All queries for all types of relational databases could now be implemented. Alternatively, you can specify that the function receives a database connector that supports the JDBC interface.
Benefit: In the long run, this makes the software much easier to maintain because it is clear where functionality is located. It also aids in KISS.
Risk: Overdoing it may result in a violation of KISS. A good rule of thumb is that an interface should be implemented by at least two classes before it is created.
- Locality Principle
Things that belong together should not be separated. If two sections of code are frequently edited together, try to keep them as close together as possible. At the very least, they should be in the same package, hopefully in the same directory, and possibly in the same file — and if you're lucky, they should be in the same class or directly below each other within the file.
Benefit: Hopefully, this means fewer merge conflicts. When you try to find that other file, you do not need to switch context. When you refactor that piece, you may recall everything that belongs to it.
Risk: Violating loose coupling or concern separation.
Tooling support: So far, none, but I'm considering making one for Python. Essentially, I would examine the git commits.
If you have any doubt about the above topic. Don’t hesitate to contact us. Airo Global Software will be your digital partner.