猫头鹰的深夜翻译:软件设计原则--更健壮的代码
软件设计原则
这篇文章主要讨论如何以健壮的方式应对变化的需求,从而保持良好的编程习惯。
前言
软件设计是开发周期中最重要的一个环节。在实现弹性和灵活的设计上花的时间越多,未来在面对需求变更时节约的时间就越多。
需求总是在变化--如果没有定期加入新功能,或是维护现有功能,软件很快就会成为遗弃产物--而这些变化带来的开销是由系统的架构和体系结构决定的。在这篇文章中,我们将会讨论一个关键的设计原则,该设计原则能帮助我们创建易于维护和扩展的软件。
一个实际场景
假设你的老板让你创建一个将Word
文件转化为PDF
文件的应用。这个任务看上去很简单--你要做的就是找到一个可靠的将Word转化为PDF的库,并将这个库插入到你的应用中。在一番查找之后,假设你决定使用Aspose.words
插件,并且新建了这样一个类:
/** * A utility class which converts a word document to PDF * @author Hussein * */ public class PDFConverter { /** * 这个方法传入一个待转化的文档作为参数并返回转化后的文档 * @param fileBytes * @throws Exception */ public byte[] convertToPDF(byte[] fileBytes) throws Exception { // 我们确定输入总是一个WORD格式的文件,所以我们直接用aspose.words框架进行转化 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 { // 我们不想影响现有的功能 // 默认情况下,这个类将WORD转化为PDF // 当用户将该变量设为EXCEL时,会将EXCEL转化为PDF /** public String documentType = "WORD"; * 这个方法传入一个待转化的文档作为参数并返回转化后的文档 * @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
块中出现了相似的代码。如果某天我们设法扩展这段代码,我们将会产生大量的重复代码。除此以外,如果我们以后决定,比如,返回一个file
而不是byte[]
,那么我们需要在所有的代码快中进行重复的修改。 - 僵硬:所有的转化算法在同一个方法中高度耦合,所以当你改变其中某个算法时,很有可能会影响别的算法。
- 固定性:上面的方法直接依赖于
documentType
变量。一些用户在使用方法converToPDF
之前可能会忘记设置该变量,所以他们无法得到预期的结果。而且,因为这个方法依赖于该变量,我们无法在别的项目中重用该方法。 - 高层模块额底层框架的耦合:如果我们后面出于某种原因,决定将
Aspose
框架换成另一个更可靠的框架,我们将会需要修改整个PDFConverter
类,很多用户将会受到影响。
正确的方式
通常情况下,开发者无法预见未来的变化,因此初次开发时我们会将其实现成第一个class那样。但是,在第一次变更后,就明确知道了未来可能会出现类似的变更。所以,优秀的开发者会采取正确的实践减少未来变更的开销,而不是用if/else
强行解决。所以,我们在工具层(PDFConverter
)和底层的转化算法之间,添加了一个抽象层,并将所有的算法移动到单独的类中,如下:
/** * 这个接口代表一个抽象算法,用于将任何类型的文档转化为PDF * @author Hussein */ public interface Converter { public byte[] convertToPDF(byte[] fileBytes) throws Exception; }
/** * 这个类包含将Excel文档转化为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(); }; }
/** * 这个类持有将Word文档转化为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(); } }
public class PDFConverter { /** * 这个方法接收待转化文档作为参数并且返回转化后的文档 * @param fileBytes * @throws Exception */ public byte[] convertToPDF(Converter converter, byte[] fileBytes) throws Exception { return converter.convertToPDF(fileBytes); } }
我们强迫用户在调用convertToPDF()
方法时决定转化算法。
这样做的好处?
- 关注点分离(高内聚/低耦合):
PDFConverter
类现在对应用中使用的转化算法一无所知。它只关注于想用户提供各种转化功能,而不去关心转化是如何实现的。现在,只要能够返回预期的结果,我们就能够在没有人注意到的情况话,替换底层的转换框架。 - 单一职责:在创建了抽象层,并将每个动态的行为移动到各个类之后,我们能够删除原始设计中
convertToPDF()
方法持有的多个职责。现在它只有一个职责,就是将客户的请求委托给抽象转化层。除此以外,Converter
接口的每个具体实现都只有将某种类型的文档转化为PDF这一个职责。因此,每个组件只可能因为单个原因被修改,不会相互影响。 - 开闭原则:我们的应用现在对扩展开放,对更改关闭。无论何时我们想要添加对某种文档的支持,我们只需要创建
Converter
接口的一个新的具体类,然后这个新的类型就会立刻被支持,而无需修改PDFConverter
工具类,因为该工具类现在依赖于抽象接口。
本文中学习到的设计原则
当你创建你自己系统的体系结构时,以下是一些最佳实践:
- 将应用拆分成几个模块,并且在每个模块之上添加抽象层。
- 抽象优先于实现:确保总是依赖于抽象层。这会使你的应用对未来的扩展开放。抽象技术应使用于系统的动态部分(即最可能频繁变化的部分)而不必使用于所有部分。滥用它会增加代码的复杂度。
- 识别出系统会发生变化的部分,并将其和不变的部分分开。
- 不要重复:将重复的功能放在工具类中,使其在整个应用中都可以访问。这将会使变更更简单一些。
- 通过抽象机制隐藏低层实现:低层的模块有很大的可能会频繁变更。所以将它们和高层模块分开。
- 每个类/方法/模块应当只有一个变更的理由,所以只给它们一个职责。
- 分离关注点:每个模块知道另一个模块做什么,但无需知道它们怎么做。
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~