Software design principles

Software design has always been the most important phase in the development cycle, the more time you put on designing a resilient and flexible architecture, the more saving you will get in the future when changes arise. Requirements always change, software will become legacy if no features are added or maintained on regular basis, and the cost of these changes are determined based on the structure and architecture of the system. In this article, we’ll discuss the key design principles which help in creating an easily maintainable and extendable software.

1. A practical scenario

Suppose that your boss asks you to create an application which converts word documents to PDF. The task looks simple and all you have to do is to just lookup a reliable library which converts word documents to PDF and plug it in inside your application. After doing some research you ended up using Aspose.words framework and created the following class:

Life is easy and everything is going pretty well !!

Requirements change as always

After few months, some client asks to support excel documents as well, so you did some research and decided to use Aspose.cells. Then you go back to your class and added a new field called documentType and modified your method like the following:

This code will work perfectly for the new client and will still work as expected for the existing clients, however some bad design smells start to appear in the code which denotes that we’re not doing it the perfect way and we will not be able to modify our class easily when a new document type is requested.

  1. Code repetition: As you see, similar code is being repeated inside if/else block and if we managed someday to support different extensions, then we will have a lot of repetitions. Also if we decided later on for example to return a file instead of byte[] then we have to do the same change in all the blocks.
  2. Rigidity: All the conversion algorithms are being coupled inside the same method, so there is a possibility if you change some algorithm, others will be affected.
  3. Immobility: The above method depends directly on documentType field, some clients would forget to set the field before calling convertToPDF() so they will not get the expected result, also we’re not able to reuse the method in any other project because of its dependency on the field.
  4. Coupling between high level module and the frameworks: If we decide later on for some purpose to replace Aspose framework with a more reliable one, we will end up modifying the whole PDFConverter class and many clients will be affected.

Doing it the right way

Normally, not all developers are able to expect the future changes , so most of them would implement the application exactly as we implemented it the first time, however after the first change the picture becomes clear that similar future changes will arise. So instead of hacking it with if/else block, good developers will manage to do it the right way in order to minimize the cost of future changes. So we create an abstract layer between our exposed tool (PDFConverter) and the low level conversion algorithms, and we move every algorithm into a separate class as the following:

We force the client to decide which conversion algorithm to use when calling convertToPDF().

2. What are the advantages of doing it this way !!

  1. Separation of concerns (high cohesion/low coupling)PDFConverter class now knows nothing about the conversion algorithms used in the application, its main concern is to serve the clients with the various conversion features regardless how the conversion is being done. Now that we’re able anytime to replace our low level conversion framework and no one would even know as long as we’re returning the expected result.
  2. Single responsibility: After creating an abstract layer and moving each dynamic behavior to a separate class , we actually removed the multiple responsibility that convertToPDF() method previously had in the initial design, now it just has a single responsibility which is delegating client requests to abstract conversion layer. Also each concrete class of Converter interface has now a single responsibility related to converting some document type to PDF. As a result, each component has one reason to be modified, hence no regressions.
  3. Open/Closed application: Our application is now opened for extension and closed for modification, whenever we want in future to add support for some document type, we just create a new concrete class from Converter interface and the new type become supported without the need to modify PDFConverter tool since our tool now depends on abstraction.

3. Design principles learned from this article

Following are some best design practices to follow when building an architecture for application:

  1. Divide your application into several modules and add an abstract layer at the top of each module.
  2. Favor abstraction over implementation: always make sure to depend on abstraction layer, this will make your application open for future extensions, the abstraction should be applied on the dynamic parts of the application (which most likely to be changed regularly) and not necessarily on every part since it complicates your code in case of overuse.
  3. Identify the aspects of your application that vary and separate them from what stays the same.
  4. Don’t repeat yourself: always put the duplicate functionalities in some utility class and make it accessible through the whole application, this will make your modification a lot easier.
  5. Hide low level implementation through abstract layer: low level modules have a very high possibility to be changed regularly, so separate them from high level modules.
  6. Each class/method/module should have one reason to be changed, so always give a single responsibility for each of them in order to minimize regressions.
  7. Seperation of concerns: each module knows what other module does, but it should never know how to does it.


0 0 votes
Article Rating

Hussein Terek

Owner of, I have a passion for software engineering and everything related to Java environment.

Newest Most Voted
Inline Feedbacks
View all comments
3 years ago

great article

3 years ago

While you may have a point, I would have liked it more if you didn’t use really bad ‘straw man code’ to demonstrate the design principles. For example, one could argue that a utility method should be declared static in the first place. And, more importantly, most good programmers would either create two methods (excelToPdf and wordToPdf) or use an enum as second argument (convertToPdf(bytes[] …, FileType.WORD), instead of setting a field to a default value (as a public field, no less, which is against most Java design principles that I know of) or reduce the code duplication that is… Read more »