The Software Engineering Tenets
Every class should have only one responsibility. I think a lot of people struggle with this in concept. A simple way to track to this is: “When you explain what the class does to your rubber ducky, do you use the word ‘and’?”
Every class you create should be closed for modification and opened for extension. If you have to keep changing code you’ve already created then tests must be updated, all code which uses this constantly changing class must be adapted to it’s changes. It is far better and keeps the system more stable if you extend the class (extend the functionality) with new code.
Every new feature should be put into a new class, not sprinkled throughout existing classes.
Simply put, a class instance should be able to be replaced with any of it’s sub-classes instances without changing behavior. The only thing that would violate this is if a sub-class has overrode a method of the main class. The reason why this becomes a violation is that the sub-class would have different functionality than the base class. The goal is to avoid overriding methods. If you need to change functionality of a base class method, then that base class method doesn’t really belong in the base class as it doesn’t correctly represent all of the sub-class objects. The method requiring a different behavior for each sub-class should be migrated to all sub-classes where perhaps some of the sub-classes have the same functionality, but not all of them do. Duplicate code can be eliminated by using shared functions or another level of hierarchy.
If you design a module to an interface, then when you change code in a module on either side of the interface, then no other modules must be recompiled. This ensures that you have “independently deploy-able modules” and this ensures that you have independently develop-able modules. Furthermore, the modules can be independently tested at interface boundaries. Oh, and name these interfaces based on their user – not by their implementer.
High level policy should not depend on low level detail, but low level detail should depend on high level policy. We use interfaces to break apart the compile time dependencies so that we don’t have huge portions of a program dependent on other portions. Inserting an interface breaks this apart, because you compile to an interface. All code can be independently compiled to that interface, but we can also use these interfaces to dictate the flow of control.
Think of plugins, we cannot have a plugin system where our application depends on the plugins, right? Otherwise, our application would have to know about every plugin before we release it. How is a plugin architecture created? The plugins are compiled against the application – this is done by the application providing an interface (a contract). All plugins compile to this interface and thus will be compatible with the application. This is the mechanism by which a dependency is inverted. The plugins will depend on the application (i.e. it’s interfaces).
We call it inverted because the dependencies will oppose the flow of control. Where the dependencies are such that the plugin depends on the interface provided by the Application (i.e. the plugin points to the Application), and the flow of control goes from the User, through the Application, and finally to the plugin (i.e. the Application points to the plugin).
When designing a system, all modules (not just the plugins) should depend on the application, and if they don’t then the dependency should be inverted using an interface. This allows the application to define the behavior, versus lower level dependencies defining the behavior.
Do not overload concepts in software design. Have independent representation for every feature, so that they can be controlled independently.
If you read from a file path or write to a file path, then you should request that path from a service or authoritative object. When you hard code the path, you make the system incoherent. If you write to a path from a service and you read that path from a service, then if the service is updated, the system is updated – the system is coherent. It is even better if the writing and reading can be delegated to an object so no other parts of the system know about the path (i.e. tell don’t ask).
Just don’t do it, and singletons are bad mmm k (they are not multi-thread safe, static initialization is difficult to control, they are not test friendly, etc).
Don’t Repeat Yourself (DRY)
I think most people get this – there is no excuse for writing the same code over and over; however, this goes deeper. If you repeat yourself, then you make your system incoherent and when you make one change you have to make changes all over to keep things in sync. If the same action is done in one place, then updating that one place updates the entire system. While I think everyone understands this concept, it takes discipline to do this when you have to cross module boundaries etc. Make a habit of reusing code!
Avoid Getters and Setters
Tell, Don’t Ask
Seem silly? On some classes, it would be. Why? When you request data from an object you make decisions on that information, and that means that the object is not well encapsulated. If the object changes, then other parts of the system have to change and the system is said to have a high amount of coupling. What is better is if the object is the only one that knows about it’s internal workings, and if we need a decision made, then we tell that object to make the decision or do the thing and it takes care of the work. In this way, if the internal workings of the object change all external callers won’t change. A “tell” type of interface is much less likely to change.
Friendship in Only Allowed within the same Component
You do not want to expose internal workings of a component, which friendship would allow. We only allow access into our component using public (and hopefully well designed) interfaces!
To use your new feature, a user should only have to include one component.
Environment setup should always be done in an external script.
This enables you to recreate the environment and then interactively enter commands. If they are coupled in a script this becomes difficult to debug.
No cyclical dependencies.
Physical dependencies versus Logical dependencies:
- Physical Dependencies – Syntactically
- Does an object use on another object in code? If so, then you have a physical dependency
- Logical Dependencies – Semantically
- When you talk about the objects how do you explain them? If you can logically think about the design and visualize the dependency, then you have a logical dependency.
With this understanding, Physical to Logical mapping should be a 1:1 (i.e. if your Physical Dependencies do not correlate with your Logical Dependencies, then you have a problem).
If you use something directly, then you include it directly – you don’t declare it locally! Local declarations can allow hidden linking problems.
No Argument Production Build Steps
What the system does to build a release should not be configurable in anyway. This prevents strange case where something works in one case and not another. If a developer builds a local release build, this guarantees that the final build will be done in as close a way as is possible to the final release.
Group by How it’s Used
… not by what it is. Don’t make classes around Mode View and Controller, but instead about Clients, Businesses, etc.
When building new code a good test to check if the code is well encapsulated and independent is to ask the question “What all do I have to do to remove this new feature?” It should be simple like removing a class, and if it is not, then ask yourself how can I get it there or closer.
Another good check is to ask yourself when I come in here to make changes where do I want to go or where is the correct place to make a code/data file change that will influence the system. Simply ask “Where do I want to go when I need to make a change?” If it is not in the right spot, then move it so you are updating something that is simple and low impact. Updating headers for example to add configuration changes has compilation costs.