Software Design Principle
This article was based and inspired on Software Design principle by Ahmed Adel and Go SOLID design by Dave Cheney. I will combine all those in this article, hopefully this article can be useful.
What are the principles that we should follow while we are building our software, those principles are there to help us make robust, maintainable, flexible, and scale-able software. Those principles will guide you through building your software.
1- Divide and Conquer : Divide your system into small, reusable, high cohesive parts
Trying to deal with big bunch of code is surely harder than dealing with small chunks, Dividing the code in small chunks will give us some benefits :
· The developers can be able to work on different parts in the system in parallel
· Smaller chunks will be easier to understand, easier to test, and easier to re-use
· Refactoring or changing a small chunk of code is way more easier and safer than doing so in a big class that is used in multiple parts of the system
· The smaller your code unit becomes, the more cohesive it is, and this respects the “Single Responsibility Principle”
· Functional programming is driven mainly by this design principle, and this can be done in Object Oriented Programming as well, for example :
In Java
// a big class that should be divided :
// dividing the class into smaller classes based on there responsibility :class LocationUtils {// one responsibility is to check for location availability public boolean isGpsLocationEnabled(){
// …
} public boolean isNetworkLocationEnabled(){
// …
} // another responsibility is to get locations : public Location retrieveOneLocation(){
// …
} public void retrieveMultipleLocations(Consumer<Location> listener){
// …
listener.accept(newLocation);
}
}class OneLocationRetriever implements Callable<Location> {
@Override
public Location call() {
// poll one location
}
}class MultipleLocationsRetriever implements
Consumer<Consumer<Location>> {
@Override
public void accept(Consumer<Location> locationListener) {
// listen on location
locationListener.accept(newLocation);
}
}
Callable.java and Consumer.java are Java functional interfaces, notice that after breaking down our class into separate functions (functional classes), there is no limitation on moving or re-using any of the functions, if we want to move one function to another package, nothing will be affected, further more, since all the functions implement Java functional interfaces, the users of those functions do not need to know the implementer of the java interface, like in this function :
class MultipleLocationsRetriever implements
Consumer<Consumer<Location>> {
@Override
public void accept(Consumer<Location> locationListener) {
// listen on location
locationListener.accept(newLocation);
}
}
The expected “locationListener” is a class of type the Consumer.java as well, so if the implementer of this interface changed or moved or even deleted, this class will not be affected.
How we do in Go
type LocationUtils struct{}// dividing into smaller function based on there responsibility so it will :
// 1. Satisfied Single Responsibility Principle
// 2. Smaller chunk will get easier to understand, to test the code and to re — use
// 3. Refactoring small chunk will be easier and safer// note : public access defined by first functions letters, Uppercase defined as public while Lowercase defined as private// one responsibility is to check for location availabilityfunc (l *LocationUtils) IsGpsLocationEnabled() bool{
// …
}func (l *LocationUtils) IsNetworkLocationEnabled() bool{
// …
}// another responsibility is to get locations :func (l *LocationUtils) RetrieveOneLocation() Location{
// …
}// in Go also has a void method which not return any values :func (l *LocationUtils) retrieveMultipleLocations(listener Consumer){
listener.accept(newLocation)
}
As interface in Java, Go has also interface that can be used to help us achieve loosely coupled program design. Coupling refers to the direct knowledge that an element of a system has of another. Loose Coupling means interconnecting components without assumptions bleeding through our system.
With earlier example and interface to achieve loosely coupled program design.
// create interface of LocationUtils so it will achieve loosely
// coupled program designtype LocationHelper interface{// just register function name, input and return value. Leave the
// pointer receiver IsGpsLocationEnabled() bool
IsNetworkLocationEnabled() bool
RetrieveOneLocation() Location
retrieveMultipleLocations(listener Consumer)
}
In Go, any value that implements those two functions satisfies the interface implicitly, i.e. there’s no implements required or anything of the like
2- Cohesion : Increase Cohesion where possible
We can measure the organization of the software by it’s cohesion.
3- Coupling : Reduce Coupling where possible
A big software will hold many relations between it’s components, and that’s where coupling comes to play, tightly coupled system components makes it harder to re-use, maintain, and scale.
4- Abstraction : Keep the level of abstraction as high as possible
Make sure that our code makes it easy to hide as much details as possible. The code in previous snippet is using interface, which makes it very easy to change the implementer of those interface at any point, as long as we are dealing with interfaces, the system will be very flexible.
5- Re-usability : Increase re-usability where possible
Design our code so that it can be re-used in multiple contexts, we will need to follow the previous principles to be able to increase the re-usability of the code as well
Put in mind that re-usability comes at the cost of having more complexity, so we must manage the trade-off between making our code re-usable or making it simple, but if we can make it re-usable in a simple way this will be the best to do
One way to gain re-usability and reduce complexity is to divide our code into small functions and implement the known functional interfaces where possible, like the code snippets in the earlier.
6- Re-use Existing : Re-use existing design and code where possible
Building upon the previous principle, re-using the existing code and design benefits from the investments of the others, but put in mind that “Cloning” or “Copy/Paste” is NOT considered re-usability, you should never clone or copy/paste code, always respect the “DRY principle” (Don’t Repeat Yourself)
7- Flexibility : Increase the flexibility of your system
Design your code to be prepared for future changes, we can achieve this through :
· Reduce Coupling and increase Cohesion
· Create and work with Abstractions (Interfaces)
· Never Hard-Code any thing
· Leave all options open, do not put limitations that hinders modifying the system in the future.
· Use Re-usable code, and the new code re-usable as well
8- Anticipate Obsolescence / Deprecation : prepare for changes
The more we use external code, the more often you will get hit with deprecated parts and soon will need to change your code to deal with the new code
· Avoid using early releases
· Avoid using libraries that are specific for particular environments.
· Avoid using undocumented libraries
· Avoid using Software from companies that will not provide long-term support
· Use standard languages and technologies that are supported by multiple vendors
“Uncle Bob” mentioned in his “Clean Architecture” that any external dependency should be outside the core of the application, they should be isolated from the business rules of the application
Also we can use some Coupling and Cohesion techniques to separate between our code and the external code, like putting the external code in a separate Layer, or putting interfaces between your code and the external code.
9- Portability : Design your software to be portable
Always Design our code to be portable, avoid depending on a certain OS while you are building your software. Any depending on specific configuration path will make our software less portable.
10- Test-ability : Design your code to be tested by another code
Unit Testing is one of the major reasons for software stability, if ew have fast and efficient test suit, a suit that wetrust with our life, we can guarantee the stability of our software, the more we divide our code, and deal with abstracts (interfaces), the more easily we can unit test every packages and supply it with fake Objects implementing the Abstract packages that it uses to do it’s Job, respecting the past rules will make unit testing our code easy to achieve
Go has come along with testing package which can be used to test our code. But keep in mind that before we test our code, the code itself must be respect SRP “Single Responsibility Principle”.
package test// Go come along with testing library that we can use to test our
// code.
import "testing" // testing on get location //
func TestGetLocation(t *testing.T){
// we do our test case here
}
11- Design Defensively : Never trust how others will try to use your code
Design oue code in a way that makes sure that no one will use it the wrong way, like for example, if your function is public, you should expect that some one may pass “null” to it, if it needs an object that should meet a certain criteria, you can make this function take a certain interface instead of any type, and so on
But put in mind that excessive defensive design may result in a very bad code, so you should make things as simple and clear as possible