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

软件设计原则

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

前言

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

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

一个实际场景

假设你的老板让你创建一个将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. 分离关注点:每个模块知道另一个模块做什么,但无需知道它们怎么做。

猫头鹰的深夜翻译:软件设计原则--更健壮的代码
想要了解更多开发技术,面试教程以及互联网公司内推,欢迎关注我的微信公众号!将会不定期的发放福利哦~

相关推荐