博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
猫头鹰的深夜翻译:软件设计原则--更健壮的代码
阅读量:6721 次
发布时间:2019-06-25

本文共 5257 字,大约阅读时间需要 17 分钟。

软件设计原则

这篇文章主要讨论如何以健壮的方式应对变化的需求,从而保持良好的编程习惯。

前言

软件设计是开发周期中最重要的一个环节。在实现弹性和灵活的设计上花的时间越多,未来在面对需求变更时节约的时间就越多。

需求总是在变化--如果没有定期加入新功能,或是维护现有功能,软件很快就会成为遗弃产物--而这些变化带来的开销是由系统的架构和体系结构决定的。在这篇文章中,我们将会讨论一个关键的设计原则,该设计原则能帮助我们创建易于维护和扩展的软件。

一个实际场景

假设你的老板让你创建一个将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();        }    }}

这段代码对新客户来说是完美的(现有的客户也可以如期使用它),但是代码中开始出现了坏味道。这意味着,我们的修改并不完美。当出现新的文档类型时,我们不能简单的修改这个类。

  1. 代码的重复:如你所见,在if/else块中出现了相似的代码。如果某天我们设法扩展这段代码,我们将会产生大量的重复代码。除此以外,如果我们以后决定,比如,返回一个file而不是byte[],那么我们需要在所有的代码快中进行重复的修改。
  2. 僵硬:所有的转化算法在同一个方法中高度耦合,所以当你改变其中某个算法时,很有可能会影响别的算法。
  3. 固定性:上面的方法直接依赖于documentType变量。一些用户在使用方法converToPDF之前可能会忘记设置该变量,所以他们无法得到预期的结果。而且,因为这个方法依赖于该变量,我们无法在别的项目中重用该方法。
  4. 高层模块额底层框架的耦合:如果我们后面出于某种原因,决定将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()方法时决定转化算法。

这样做的好处?

  1. 关注点分离(高内聚/低耦合)PDFConverter类现在对应用中使用的转化算法一无所知。它只关注于想用户提供各种转化功能,而不去关心转化是如何实现的。现在,只要能够返回预期的结果,我们就能够在没有人注意到的情况话,替换底层的转换框架。
  2. 单一职责:在创建了抽象层,并将每个动态的行为移动到各个类之后,我们能够删除原始设计中convertToPDF()方法持有的多个职责。现在它只有一个职责,就是将客户的请求委托给抽象转化层。除此以外,Converter接口的每个具体实现都只有将某种类型的文档转化为PDF这一个职责。因此,每个组件只可能因为单个原因被修改,不会相互影响。
  3. 开闭原则:我们的应用现在对扩展开放,对更改关闭。无论何时我们想要添加对某种文档的支持,我们只需要创建Converter接口的一个新的具体类,然后这个新的类型就会立刻被支持,而无需修改PDFConverter工具类,因为该工具类现在依赖于抽象接口。

本文中学习到的设计原则

当你创建你自己系统的体系结构时,以下是一些最佳实践:

  1. 将应用拆分成几个模块,并且在每个模块之上添加抽象层。
  2. 抽象优先于实现:确保总是依赖于抽象层。这会使你的应用对未来的扩展开放。抽象技术应使用于系统的动态部分(即最可能频繁变化的部分)而不必使用于所有部分。滥用它会增加代码的复杂度。
  3. 识别出系统会发生变化的部分,并将其和不变的部分分开。
  4. 不要重复:将重复的功能放在工具类中,使其在整个应用中都可以访问。这将会使变更更简单一些。
  5. 通过抽象机制隐藏低层实现:低层的模块有很大的可能会频繁变更。所以将它们和高层模块分开。
  6. 每个类/方法/模块应当只有一个变更的理由,所以只给它们一个职责。
  7. 分离关注点:每个模块知道另一个模块做什么,但无需知道它们怎么做。

clipboard.png

想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~

转载地址:http://bcjmo.baihongyu.com/

你可能感兴趣的文章
网络故障分析 某单位部分网段无法访问网站故障分析
查看>>
MySQL数据库SQL语法参考 - MySQL - IT技术网
查看>>
My Code
查看>>
HyperlinkButton控件的使用
查看>>
[转载] 中华典故故事(孙刚)——25 见怪不怪,其怪自败
查看>>
应用间跳转
查看>>
手机测试用例-多媒体测试用例
查看>>
大型网站技术架构(八)网站的安全架构
查看>>
我的友情链接
查看>>
嵌入式 Linux C语言(十三)——双链表
查看>>
python从数据库取数据后写入excel 使用pandas.ExcelWriter设置单元格格式
查看>>
Java基础学习总结(4)——对象转型
查看>>
rsync +inodify实现文件同步
查看>>
mysql 控制台上传数据库
查看>>
Java8 十大新特性详解
查看>>
Python-练习7
查看>>
MyBatis学习总结(八)——Mybatis3.x与Spring4.x整合
查看>>
程序员面试100题之5
查看>>
有环单链表
查看>>
关于MetaProgramming
查看>>