资讯专栏INFORMATION COLUMN

猫头鹰的深夜翻译:软件设计原则--更健壮的代码

xiaochao / 3114人阅读

摘要:软件设计原则这篇文章主要讨论如何以健壮的方式应对变化的需求,从而保持良好的编程习惯。前言软件设计是开发周期中最重要的一个环节。识别出系统会发生变化的部分,并将其和不变的部分分开。

软件设计原则

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

前言

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

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

一个实际场景

假设你的老板让你创建一个将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工具类,因为该工具类现在依赖于抽象接口。

本文中学习到的设计原则

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

将应用拆分成几个模块,并且在每个模块之上添加抽象层。

抽象优先于实现:确保总是依赖于抽象层。这会使你的应用对未来的扩展开放。抽象技术应使用于系统的动态部分(即最可能频繁变化的部分)而不必使用于所有部分。滥用它会增加代码的复杂度。

识别出系统会发生变化的部分,并将其和不变的部分分开。

不要重复:将重复的功能放在工具类中,使其在整个应用中都可以访问。这将会使变更更简单一些。

通过抽象机制隐藏低层实现:低层的模块有很大的可能会频繁变更。所以将它们和高层模块分开。

每个类/方法/模块应当只有一个变更的理由,所以只给它们一个职责。

分离关注点:每个模块知道另一个模块做什么,但无需知道它们怎么做。


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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/70983.html

相关文章

  • 头鹰深夜翻译:在JVM上根据合约编程

    摘要:前言这周我准备介绍一个有趣的但是很少使用的方法按照合约编程,又称为合约编程,是一种软件设计的方法。这些规则被称为合约,可以比拟为商业合同中的条件和义务。通过将检查和异常抛出指令包装到方法中,人们可以很容易地实现合约式编程。 前言 这周我准备介绍一个有趣的但是很少使用的方法 按照合约编程,又称为合约编程,是一种软件设计的方法。它规定了软件设计师应该为软件组件定义正式,精确和可验证的接口规...

    whatsns 评论0 收藏0
  • 头鹰深夜翻译:JAVA中异常处理最佳实践

    摘要:无需检查的异常也是的子类。从低层抛出的需检查异常强制要求调用方捕获或是抛出该异常。当前执行的线程将会停止并报告该异常。单元测试允许我在使用中查看异常,并且作为一个可以被执行的文档来使用。不要捕获最高层异常继承的异常同样是的子类。 前言 异常处理的问题之一是知道何时以及如何去使用它。我会讨论一些异常处理的最佳实践,也会总结最近在异常处理上的一些争论。 作为程序员,我们想要写高质量的能够解...

    W_BinaryTree 评论0 收藏0
  • 头鹰深夜翻译:JDK Vs. JRE Vs. JVM之间区别

    摘要:什么是为执行字节码提供一个运行环境。它的实现主要包含三个部分,描述实现规格的文档,具体实现和满足要求的计算机程序以及实例具体执行字节码。该类先被转化为一组字节码并放入文件中。字节码校验器通过字节码校验器检查格式并找出非法代码。 什么是Java Development Kit (JDK)? JDK通常用来开发Java应用和插件。基本上可以认为是一个软件开发环境。JDK包含Java Run...

    blair 评论0 收藏0
  • 头鹰深夜翻译:你需要了解数据库名词

    摘要:读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 前言 很多人都在讨论数据的指数型增长,以及我们将会有比想象的还要大的数据量。但是,很少有人从数据库的角度谈论这个问题。随着数据量的暴涨,数据库也需要随之升级。这也是为什么既要了解如...

    wangym 评论0 收藏0

发表评论

0条评论

xiaochao

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<