软件设计原理
软件设计一直是开发周期中最重要的阶段。您花更多的时间设计弹性和灵活的体系结构,将来在发生更改时会节省更多的时间。需求总是在变化–如果不定期添加或维护任何功能,则软件将成为传统–并且这些变化的成本取决于系统的结构和体系结构。
软件设计一直是开发周期中最重要的阶段。您花更多的时间设计弹性和灵活的体系结构,将来在发生更改时会节省更多的时间。需求总是在变化–如果不定期添加或维护任何功能,则软件将成为传统–并且这些变化的成本取决于系统的结构和体系结构。在本文中,我们将讨论有助于创建易于维护和扩展的软件的关键设计原则。
实际方案
假设老板要求您创建一个将Word文档转换为PDF的应用程序。该任务看起来很简单-您要做的就是查找一个可靠的库,该库将Word文档转换为PDF,并将其插入应用程序中。经过研究后,说您最终使用了 Aspose.words框架并创建了以下类:
/**
* 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();
}
}
生活轻松,一切顺利!
需求变更一如既往
几个月后,一些客户要求您也支持Excel文档。因此,您进行了一些研究,并决定使用Aspose.cells。然后,返回您的类,添加一个名为documentType的新字段,并按如下所示修改您的方法:
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();
}
}
}
该代码将对新客户端完全有效(并且仍将按预期对现有客户端运行),但是一些不良的设计异味开始出现在代码中。这意味着我们没有采用完美的方法,当请求新的文档类型时,我们将无法轻松地修改类。
代码重复:如您所见,在if / else块中正在重复类似的代码,如果有一天我们设法支持不同的扩展,那么我们将有很多重复。同样,例如,如果稍后我们决定返回文件而不是byte [],那么我们必须在所有块中进行相同的更改。
刚性: 所有转换算法都在同一方法内耦合,因此,如果更改某些算法,则其他算法可能会受到影响。
固定性:上述方法直接取决于 documentType字段。一些客户端会在调用convertToPDF()之前忘记设置该字段,因此他们将无法获得预期的结果。另外,由于该方法依赖于字段,因此我们无法在其他任何项目中重用该方法。
高级模块和框架之间的耦合: 如果我们以后出于某种目的决定用更可靠的框架替换Aspose框架,我们将最终修改整个PDFConverter 类-许多客户端将受到影响。
用正确的方法做
通常,开发人员无法预测未来的变化,因此大多数开发人员将完全按照我们第一次实施的方式实施该应用程序。但是,在进行第一次更改之后,可以清楚地看到将来还会发生类似的更改。因此,优秀的开发人员不会使用if / else块对其进行破解,而是以正确的方式进行操作,以最大程度地减少未来更改的成本。因此,我们在暴露的工具(PDFConverter)和低级转换算法之间创建一个抽象层,并将每种算法移到一个单独的类中,如下所示:
/**
* This interface represents an abstract algorithm for converting
* any type of document to a PDF.
* @author Hussein
*
*/
public interface Converter {
public byte[] convertToPDF(byte[] fileBytes) throws Exception;
}
/**
* This class holds the algorithm for converting Excel
* documents to PDFs.
* @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();
};
}
/**
* This class holds the algorithm for converting Word
* documents to PDFs.
* @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();
}
}
public class PDFConverter {
/**
* This method accepts the document to be converted as an input and
* returns the converted one.
* @param fileBytes
* @throws Exception
*/
public byte[] convertToPDF(Converter converter, byte[] fileBytes) throws Exception {
return converter.convertToPDF(fileBytes);
}
}
我们强制客户端决定在调用convertToPDF()时使用哪种转换算法。
这样做的好处是什么?
关注点分离(高内聚/低耦合):现在, PDFConverter类对应用程序中使用的转换算法一无所知。它的主要关注点是为客户提供各种转换功能,而不管转换是如何进行的。现在我们可以替换低级转换框架了,只要我们返回预期的结果,没人会知道。
单一职责:创建抽象层并将每个动态行为移至一个单独的类之后,我们实际上删除了最初在最初设计中的convertToPDF()方法所承担的多个职责 。现在,它只需承担一项职责,即将客户请求委托给抽象转换层。而且,Converter接口的每个具体类 现在都具有与将某些文档类型转换为PDF有关的单一职责。结果,每个组件都有一个要修改的原因,因此没有回归。
打开/关闭应用程序: 我们的应用程序现已打开以进行扩展,并关闭以进行修改。每当我们想要添加对某些文档类型的支持时,我们只需从Converter接口创建一个新的具体类, 并且由于无需修改PDFConverter工具,新类型便会受到支持 ,因为我们的工具现在依赖于抽象。
从本文中学到的设计原则
以下是构建应用程序体系结构时应遵循的一些最佳设计实践。
将您的应用程序划分为几个模块,并在每个模块的顶部添加一个抽象层。
在实现上偏爱抽象:始终确保依赖抽象层。这将使您的应用程序打开以供将来扩展。应该将抽象应用于应用程序的动态部分(最有可能定期更改),而不必应用于每个部分,因为过度使用会使代码复杂化。
识别应用程序中各个方面,将其与保持不变的方面分开。
不要重复自己:始终将重复的功能放在某个实用程序类中,并使其可在整个应用程序中访问。这将使您的修改容易得多。
通过抽象层隐藏低级实现:低级模块很可能会定期更改,因此请将其与高级模块分开。
每个类/方法/模块都应该有一个更改的原因,因此,为了使回归最小化,请始终对每个类/方法/模块负责。
关注点分离:每个模块都知道另一个模块的功能,但永远不应该知道如何执行。