Dimas Yudha Prawira
sarccom
Published in
7 min readSep 25, 2017

--

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 design
type 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

--

--

Dimas Yudha Prawira
sarccom
Writer for

Open Source enthusiast and contributor, @java @gophers @erlanger @embedded, spare-time writer and full-time coder, working as Ordinary Engineer