Translator: Crazy Technology House
Source: Programmer Gate
Title: Software design principles
English text: http://programmergate.com/sof...
Software design has always been the most important stage in the development cycle. The more time it takes to design flexible and flexible architectures, the more time it saves when future changes occur. Requirements are always changing. If functions are not added or maintained regularly, software will become a legacy problem, and the cost of change is determined by the structure and architecture of the system. In this article, we will discuss key design principles that help create easy-to-maintain and extensible software.
1. A practical scenario
Suppose your boss asks you to write a program that converts word documents into PDF. This task seems simple, just find a reliable library that can convert word documents into PDF and integrate it into your program. After doing some research, you finally decided to use the Aspose.words framework and create the following classes:
Code: PDFConverter.java
/** * A utility class which converts a word document to PDF * @author Hussein * */ public class PDFConverter { /** * This method accepts as input the document to be converted and * returns the converted one. * @param fileBytes * @throws Exception */ public byte[] convertToPDF(byte[] fileBytes) throws Exception { // We're sure that the input is always a WORD. So we just use //aspose.words framework and do the conversion. InputStream input = new ByteArrayInputStream(fileBytes); com.aspose.words.Document wordDocument = new com.aspose.words.Document(input); ByteArrayOutputStream pdfDocument = new ByteArrayOutputStream(); wordDocument.save(pdfDocument, SaveFormat.PDF); return pdfDocument.toByteArray(); } }
Life is simple, everything goes well!!
Demand is always changing
A few months later, some users asked for support for excel documents, so you did some research and decided to use ascell.cell. Then you find your original class, add a new field named documentType, and modify your method. The code is as follows:
Code: PDFConverter.java
public class PDFConverter { // we didn't mess with the existing functionality, by default // the class will still convert WORD to PDF, unless the client sets // this field to EXCEL. public String documentType = "WORD"; /** * This method accepts as input the document to be converted and * returns the converted one. * @param fileBytes * @throws Exception */ public byte[] convertToPDF(byte[] fileBytes) throws Exception { if(documentType.equalsIgnoreCase("WORD")) { InputStream input = new ByteArrayInputStream(fileBytes); com.aspose.words.Document wordDocument = new com.aspose.words.Document(input); ByteArrayOutputStream pdfDocument = new ByteArrayOutputStream(); wordDocument.save(pdfDocument, SaveFormat.PDF); return pdfDocument.toByteArray(); } else { InputStream input = new ByteArrayInputStream(fileBytes); Workbook workbook = new Workbook(input); PdfSaveOptions saveOptions = new PdfSaveOptions(); saveOptions.setCompliance(PdfCompliance.PDF_A_1_B); ByteArrayOutputStream pdfDocument = new ByteArrayOutputStream(); workbook.save(pdfDocument, saveOptions); return pdfDocument.toByteArray(); } } }
This code can work for new users normally and still work for existing users as expected, but some bad design scents begin to appear in the code, which is not perfect. When a new document type is added, we will not be able to easily modify this class.
Code duplication: As you can see, similar code exists in if/else blocks, and if different extensions are added one day, a lot of duplication will occur. If we decide to return a file instead of a byte [], we have to make the same changes in all blocks.
Rigidity: All conversion algorithms are coupled in the same way, so if you change some algorithms, other algorithms will be affected.
Fixed: The above method relies directly on the documentType field. If some users forget to set this field before calling convertToPDF(), the expected result will not be achieved. We cannot reuse this method in any other project because it depends on the field.
Coupling between advanced modules and frameworks: If we decide to replace the Aspose framework in a more reliable way in the future, the final fix will change the entire PDFConverter class, and many users will be affected.
The Right Way
Usually, not all developers can foresee future changes. Therefore, most of them will implement the program as we did for the first time, but after the first change, it will become obvious that similar changes will take place in the future. Therefore, good developers will use the right way to minimize the cost of future changes, rather than using if / else blocks. So we create an abstraction layer between the exposed tools (PDFConverter) and the low-level transformation algorithms, and move each algorithm into a separate class, as follows:
Code: Converter.java
/** * This interface represents an abstract algorithm for converting * any type of document to PDF. * @author Hussein * */ public interface Converter { public byte[] convertToPDF(byte[] fileBytes) throws Exception; }
Code: Excel PDF Converter. Java
/** * This class holds the algorithm for converting EXCEL * documents to PDF. * @author Hussein * */ public class ExcelPDFConverter implements Converter{ public byte[] convertToPDF(byte[] fileBytes) throws Exception { InputStream input = new ByteArrayInputStream(fileBytes); Workbook workbook = new Workbook(input); PdfSaveOptions saveOptions = new PdfSaveOptions(); saveOptions.setCompliance(PdfCompliance.PDF_A_1_B); ByteArrayOutputStream pdfDocument = new ByteArrayOutputStream(); workbook.save(pdfDocument, saveOptions); return pdfDocument.toByteArray(); }; }
Code: WordPDFConverter.java
/** * This class holds the algorithm for converting WORD * documents to PDF. * @author Hussein * */ public class WordPDFConverter implements Converter { @Override public byte[] convertToPDF(byte[] fileBytes) throws Exception { InputStream input = new ByteArrayInputStream(fileBytes); com.aspose.words.Document wordDocument = new com.aspose.words.Document(input); ByteArrayOutputStream pdfDocument = new ByteArrayOutputStream(); wordDocument.save(pdfDocument, SaveFormat.PDF); return pdfDocument.toByteArray(); } }
Code: PDFConverter.java
public class PDFConverter { /** * This method accepts as input the document to be converted and * returns the converted one. * @param fileBytes * @throws Exception */ public byte[] convertToPDF(Converter converter, byte[] fileBytes) throws Exception { return converter.convertToPDF(fileBytes); } }
When convertToPDF() is called, we force the user to decide which conversion algorithm to use.
2. What are the benefits of doing this?!
Separation of concerns (high cohesion/low coupling): The PDFConverter class now knows nothing about the transformation algorithms used in the program. It focuses on providing users with various transformation features and on how the transformation takes place. Now, we can always replace the underlying transformation framework, as long as we can return the expected results, no one will know.
Single Responsibility: After creating the abstraction layer and moving each dynamic behavior to a separate class, we actually removed the multiple responsibilities of the convertToPDF() method in the previous initial design. Now it has only one responsibility, that is, delegating user requests to the abstract transformation layer. In addition, each implementation class of the converter interface now has a single responsibility to convert certain document types to PDF. Therefore, each component has a reason to be modified, so there is no regression.
Open/Close Programs: Our programs are now open to extensions and closed to modifications. When we want to support some new document types in the future, we just need to create a new implementation class from the Converter interface and do not need to modify the PDF Converter tool, because our tools now rely on abstraction.
3. Design principles learned from this article
The following are the best design practices to follow when building an application architecture:
The program is divided into several modules and an abstraction layer is added at the top of each module.
Favorable for abstraction: Must rely on the abstraction layer, which will facilitate future expansion of the program. Abstraction should be applied to the dynamic part of the program (the most likely part to change frequently), not necessarily in all parts, because your code becomes very complex in the case of overuse.
Identify the different aspects of the program and separate them from the parts that remain unchanged.
Don't repeat yourself: Always put duplicate functions in some tool classes and make them accessible through the whole program, which will make your modifications much easier.
Hide low-level implementations through the abstraction layer: Low-level modules are highly likely to change periodically, so they are separated from high-level modules.
Each class/method/module should have a reason to change, so in order to reduce regression, each class is always given a single responsibility.
Separation of concerns: Each module knows what other modules do, but it does not know what to do.
Author's Profile:
HUSSEINTEREK: The founder of programmergate.com, is passionate about software engineering and everything related to java.
Welcome to scan the two-dimensional code, pay attention to the public number, and push my translated technical articles every day.