Software Design Principles
Ahmed Adel Ismail
Engineering Manager @ Yassir | x-SadaPay | x-Swvl | x-Talabat | x-TryCarriage | x-Vodafone | More than a decade of experience in Android development and teams leadership
In this article we will discuss Software Design Principles ... 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 :
// a big class that should be divided :
?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);?
}
}
// dividing the class into smaller classes based on there responsibility :
?class LocationAvailability {
public boolean isGpsLocationEnabled(){
// ...
}
public boolean isNetworkLocationEnabled(){
// ...
}
}
class LocationRetriever {
public Location retrieveOneLocation(){
// ...
}
public void retrieveMultipleLocations(Consumer<Location> listener){
// listen on location
listener.accept(newLocation);?
}
}
// --------------------------------------------------------
// There is still more better way in dividing the class, since all the methods
// are pure-functions (they do not update member variables or change the
// state of the class that they are in) a better way is to
// use functional programming style and ?divide each method
// in a separate functional class that holds only one public method :
// --------------------------------------------------------?
?
class GpsLocationAvailabilityChecker implements Callable<Boolean> {
@Override
public Boolean call() {
// check for Gps Location availability
}
}
class NetworkLocationAvailabilityChecker implements Callable<Boolean> {
@Override
public Boolean call() {
// check for Network Location availability
}
}
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 ... more about this subject in the Coupling and Abstraction sections.
2- Cohesion : Increase Cohesion where possible
We can measure the organization of the software by it's cohesion, more on this subject in this article :
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 ... more about this subject in this link :
4- Abstraction : Keep the level of abstraction as high as possible
Make sure that your code makes it easy to hide as much details as possible, like looking at the following code snippet :
class MessangerView {
private Function<String, Boolean> messageSender;
public void onClickSendButton(String message) throws Exception {
boolean messageSent = messageSender.apply(message);
if (!messageSent) {
// show error
}
}
}
class MessageSender implements Function<String, Boolean> {
private Function<String, String> messageFormatter;
private Function<String, Boolean> messageApi;
@Override
public Boolean apply(String message) throws Exception {
String formattedMessage = messageFormatter.apply(message);
return messageApi.apply(formattedMessage);
}
}
class MessageFormatter implements Function<String, String> {
@Override
public String apply(String originalMessage) throws Exception {
// format the originalMessage and return a formatted String
}
}
class SendMessageApi implements Function<String, Boolean> {
@Override
public Boolean apply(String messageContent) throws Exception {
// send the messageContent to server and return true
// if sent, or false if failed
}
}
As you can see, all the classes depend on Java interfaces, which makes it very easy to change the implementer of those interfaces at any point ... as long as we are dealing with abstractions and interfaces, the system will be very flexible
5- Re-usability : Increase re-usability where possible
Design your code so that it can be re-used in multiple contexts, you 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 you must manage the trade-off between making your code re-usable or making it simple ... but if you 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 your code into small functions and implement the known functional interfaces where possible, like the code snippets in the Abstraction principle ... also notice that if your over all design is simple, this will help re-using it's inner components as well
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
- Never Hard-Code any thing
- Leave all options open, do not put limitations that hinders modifying the system in the future ... like forcing developers to implement or extend certain classes even if they do not need there functionality - favor composition over inheritance
- Use Re-usable code, and the new code re-usable as well
8- Anticipate Obsolescence / Deprecation : prepare for changes
The more you 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 (like in Android for example, a library that targets a certain mobile vendor)
- 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 you can use some Coupling and Cohesion techniques to separate between your code and the external code, like putting the external code in a separate Layer, or putting interfaces between your code and the external code ... some techniques are mentioned in the links related to Coupling and Cohesion
9- Portability : Design your software to be portable
Always Design your code to be portable, avoid depending on a certain OS while you are building your software, in Android for example, avoid depending on code related to a specific Mobile Vendor
10- Test-ability : Design your code to be tested by another code
Unit Testing is one of the major reasons for software stability, if you have fast and efficient test suit, a suit that you trust with your life ... you can guarantee the stability of your software ... the more you divide your code, and deal with abstracts, the more easily you can unit test every class and supply it with fake Objects implementing the Abstract classes that it uses to do it's Job ... respecting the past rules will make unit testing your code easy to achieve
11- Design Defensively : Never trust how others will try to use your code
Design your 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
* This article is based on the following videos :
- https://youtu.be/FMKv8Vozf5c
- https://youtu.be/XQnytAeZrWE
Senior Java Software Developer at Vodafone International Services (VIS)
7 年Great article .. your way of explanation is so simple .. keep going on :)
7+ Year as Senior Mobile Application Developer (Kotlin Multiplatform | Android Jetpack Compose | Flutter)
7 年Nice article
Director Of Information Technology at Time Solution Pvt Ltd.
7 年??? ????? ???? ??? ???
Android Engineer @ Adres ,Ex: MaxAB E-commerce I fintech l Android Jetpack
7 年??? ???? ????? ???? ???? clean architecture