软件编程基础规约

1. 概述

业界、指导编程的、经典书籍很多,如《重构》、《代码大全》、《设计模式》;更进一步的则有《敏捷软件开发》、《领域驱动设计》、《计算机程序的构造与解释》等等。这些书籍由浅入深的,有助于开发人员形成一整套的编程体系。

阅读这些书籍需要花费大量的精力,特别是《设计模式》、《领域驱动设计》、《计算机程序的构造与解释》。对于初学者来说有如天书,即便对于有经验的开发人员,想要看懂也并非易事。

另一方面,整个行业对软件开发人员的需求不断增加,人员能力参差不齐,软件质量无法保证。

本文尝试补齐开发人员能力和行业诉求之间的GAP。如果你是一名开发人员,尚未形成自己的编程体系。如果你正在开发产品功能需求,希望快速审视代码质量是否达到产品级标准。如果希望能更聚焦业务逻辑,而非基础编码的问题。那么,OK。

本文只此一篇,不是系列文章,也不会太长。本文将以最简短的方式,描述软件开发最基础、核心的六条编程规约。同时,文末还附赠了一条架构规约,当然这不是本文的重点。

2. 编程规约

  • 规约1:分层设计
  • 规约2:面向接口编程
  • 规约3:业务数据建模
  • 规约4:单一职责原则
  • 规约5:精简原则
  • 规约6: 生命周期最小化

2.1 规约1:代码分层设计

分层设计是编程的基础,首先要有一个合理的分层,才可能写出好的代码。那么什么才是好的分层设计?典型的代码分层又是什么样的呢?

好的分层设计应该包括如下几个特点:

  • 上层只允许调用直接的下层,不允许跨层调用。
  • 下层不允许调用上层,允许通过回调方式解耦。
  • 平层之间调用不违反设计规则,但应尽量避免。

典型的代码分层设计如下:


典型分层设计
  • App:应用层。App层代表了对外的表现形态,一个模块可能有多个App层。例如模块对外提供了restful接口、rpc接口,那么它应该有两个App抽象。App层应尽可能的做薄,不做任何业务逻辑处理,只做入参校验、App层到Server层之间的模型转换。

  • Server:业务编排层。Server层负责核心业务的编排,它可以引用一个或者多个下层业务单元,即上图所示的Mgr类。如果业务足够简单,也可以将Server、Mgr层合并。此时Server层将变为业务处理层,而非业务编排层。

  • Mgr:业务单元层。对于复杂业务,需要采用分治方式,拆分为多个业务单元。这些业务单元有各种明确的职责,由Server层做统一编排。Server层和Mgr层存在一定的模糊边界,对于简单的逻辑Server层可以代劳,Server层的代码超过一定代码量(如300行),需要考虑抽象新的Mgr对象。

  • Repository:数据仓储层。Repository层在Dao层之上,这一层抽象更多源于《领域驱动设计》。在早期的分层设计中,Dao层承载了数据访问抽象,Mgr层直接引用Dao层做数据存储,这样做使得Mgr层关注了一部分存储细节。引入Repository层之后,按业务数据模型构造Repository,通过Repository做数据存储访问,Mgr层集中关注业务逻辑处理。需要注意,Repository和Entity不是一一对应的,Root Entity及其附属的Entity可以共用一个Repository。Root Entity可以理解为核心业务实体对象,附属Entity可以理解为非核心的引用对象。关于这部分的详细描述,请参照《领域驱动设计》中“第5章 软件中所表示的模型”一文。

  • Dao:数据访问层。Dao层用于屏蔽部分数据库访问细节,而非全部。如使用MySql数据库访问的Dao层与使用MongoDB数据库访问的Dao层设计是不同的。Dao层只能尽量做到屏蔽底层细节,在复杂场景下完全屏蔽了底层细节将使得Dao层非常厚重。Repository层负责对上屏蔽全部数据存储细节。

  • Entity:业务数据模型。核心的业务数据建模,系统围绕数据模型编码业务。Entity的设计是整个业务的关键,关于这部分的详细说明参见“规约3:业务数据建模”。

  • Util:工具层。供模块或者产品使用的工具层,由多个类组成。这些工具类一般不依赖其他部件,或者只依赖轻量级部件。通常实现一些通用的逻辑处理,如IP地址校验、日期转换等。Util可以被任何层级调用,不属于分层结构中的任何一层。

  • Proxy:代理层。代理层用于代理其他模块提供的业务接口,该层抽象是按需构建的。如调用其他业务的restful接口需要构建参数,发起restful请求,判定网络异常等;可以提供一个代理层隐藏restful调用细节,使得外部看起来和本地调用没有任何区别。如果对于简单的api接口调用可省略Proxy封装。

2.2 规约2:面向接口编程

面向接口编程的核心诉求是信息隐藏,上层只关心和下层签订的“契约(接口)”。接口定义了需要提供哪些能力,而至于如何提供了这些能力上层不感知、也不关心。

这样做的带来的好处:解耦,下层可以选择任意方式实现“契约”。上层将全部精力关注到业务逻辑,如此方可开发出复杂、稳定的产品。

但落实到实现上,如果所有分层(最上层除外)都严格遵守接口、实现分离,不现实也不合适。每多一层抽象,就增加了代码的复杂度。这里涉及抽象粒度、时机:如何面向接口编程,同时减少不必要的接口抽象?

以我个人的经验来看,处理方式如下:

  • 如果接口只有一个实现,可省略接口定义。以实现类的public方法做为接口定义,其他非接口方法声明为protected或private。
  • 如果接口有多个实现,增加接口抽象。上层创建下层对象时可以通过new、工厂方法、工厂类等方式实现。上层使用下层对象时,使用的是接口对象。至于选择哪种方式,需要根据具体情况而定。

2.3 规约3:业务数据建模

任何一个业务功能都有其业务模型,即核心Entity抽象。大部分人在开发过程中,更关心的是逻辑、代码分支处理,而忽略了业务数据建模。最终,代码可以实现当前的功能诉求,但无法持续演进,新功能开发将导致大量既有代码变更。

因此我们强调,在开发之前,需要先对领域建模。本文讨论的是模块级的代码开发,而非系统级设计。对于一个功能模块来说,核心的数据模型(Entity)可能只有1-5个,一切的业务行为应围绕业务数据模型展开。

比如对于告警管理模块,核心的业务数据模型包括:当前告警Entity、历史告警Entity、事件Entity。告警管理业务围绕三个Enttiy展开,如告警清除,实际上只是从当前告警Entity到历史告警Entity的状态迁移。

典型的业务数据Entity具有如下几个特点:

  • 和业务领域中的一个或多个术语对应。
  • 具有唯一标识,可以是流水号、UUID、身份证号等各种形式。
  • 模块或领域内唯一。对于大型系统,系统级统一建模会很困难,不同的开发团队必然会独自演进各自的业务模型。按《领域驱动设计》一书中的建议,我们需要做到Bounded Context内的统一模型,Bound Context通常指的就是一个业务模块。
  • 业务数据Entity必须做到字段的不可精简。Entity中的每一个字段代表了一个业务属性,需要审视每一个字段新增、删除是否影响Entity的业务表达。保证业务数据模型的稳定性,避免频繁变更。

除Entity外,还有一种业务数据模型称之为Value Object。Value Object没有唯一标识,通常附属在一个或者多个Entity之中。如Address附属在个人信息中时,属于Value Object。关于Entity、Value Object可参看《领域驱动设计》。

落到实现上,还有一个问题需要解决:代码是分层解耦、面向接口编程的,但Entity需要统一建模、跨层,二者互相矛盾。

这样说可能不够直接,举个例子:

class DemoApp
{
    private DemoServer demoServer = new DemoServer();

    public BusinessDto getBusinessData()
    {
        BusinessEntity entity = demoServer.getBusinessData();
        //类型转换
        return convertToDto(entity);
    }
}

class DemoServer
{
    private DemoDao demoDao = new DemoDao();
 
    public BusinessEntity getBusinessData()
    {
        //注:此处省略了repository,直接使用了dao层
        return demoDao.queryBusinessData();
    }
}

@Entity
@Table(name="tbl_sky")
class BusinessEntity
{
    @Id
     private int id;

    @Column(name="OPTLOCK")
    private string data;

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public string getData() { return data; }
    public void setData(string data) { this.data = data; }
}

各位有没有看出上面代码的问题?

  1. BusinessEntity是数据库实体类,Server层直接向上返回,破坏了信息隐藏原则。App层感知了底层依赖的Hibernate,App层被污染。
  2. Server层业务数据模型和数据库存储Entity抽象绑定,二者无法独立演进。底层抽象和存储介质相关,而业务数据模型只应和领域相关。举例来说,如果底层由MySql切换至MongoDB后,数据库Entity抽象变更,但Server层及之上的业务数据建模不应发生任何改变。

业界通常的做法是:一个业务数据按分层做多次建模,以保证分层解耦。如App层不会直接拿到Dao层的Entity,而是经过Server层转换后的另外一个Entity对象。这样就会导致同一个业务数据存在多个Entity对象,代码中充斥着大量的核心业务无关的转换逻辑。这增加了维护成本,提高了出错的可能性。我认为合理的做法应该是下面这样:

class DemoApp
{
    private DemoServer demoServer = new DemoServer();

    public BusinessDto getBusinessData()
    {
        BusinessEntity entity = demoServer.getBusinessData();
        //类型转换
        return convertToDto(entity);
    }
}

class DemoServer
{
    private DemoDao demoDao = new DemoDao();
 
    public IBusinessEntity getBusinessData()
    {
        //注:此处省略了repository,直接使用了dao层
        return demoDao.queryBusinessData();
    }
}

interface IBusinessEntity
{
    public int getId();
    public string getData();
}

@Entity
@Table(name="tbl_sky")
class BusinessEntity implements IBusinessEntity
{
    @Id
     private int id;

    @Column(name="OPTLOCK")
    private string data;

    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public string getData() { return data; }
    public void setData(string data) { this.data = data; }
}

说明如下:

  • 增加业务接口类IBusinessEntity抽象层,Server返回该接口类,而非数据库实体类,做到对上信息隐藏。
  • BusinessEntity继承IBusinessEntity接口,用作数据库实体类。
  • 业务数据接口类和数据库实体类分离后,二者可以独立演进,互不影响。同时,当业务数据接口类内容是数据库实体类的子集时,二者又可以自然的保持一致。
  • 业务数据接口类只提供get方法,使得业务数据天然具备了immutable特性,避免上层修改污染到下层。
  • 如有必要,我们可以为业务数据接口类增加一个Builder,通过Builder构建业务数据接口对象。通常,只有在引入Repository层时才需要,这里不再详述。

2.4 规约4:单一职责原则。

单一职责原则:就一个类而言,应该仅有一个引起它变化的原因;我们把职责定义为“变化的原因”。
--- 《敏捷软件开发》

单一职责原则就是我们常说的“分治”,组成软件系统的每个部分各司其职,协同完成复杂的系统任务。狭义来讲,单一职责原则用于指导分层、接口定义;按单一职责原则审视分层、接口定义的合理性。

而从更广泛的软件系统设计、实现角度来看,单一职责原则可做如下场景审视:

  • 变量含义是否唯一。变量名称和实际使用语义保持一致,不要为了节省变量将一个变量用作其他含义。
  • 函数职责是否唯一。函数职责和函数名称定义是否一致,是否存在超越函数定义的逻辑。典型的错误包括:get方法中修改对象状态;函数中包含了可被进一步抽象的子函数实现,使得函数本身应当实现的业务逻辑不够清晰等。
  • 类职责是否唯一。类抽象本身是否合理,类职责和类名称定义是否一致,是否只有一个引起该类变化的修改点。举个例子,定义了一个常量类LicenseConstant,用于维护License机制中用到的常量定义,整个License机制共享该类:
class LicenseConstants
{
    private String ROOT_PATH = "/root/lcs";
    private String ESN = "esn";
    ......
}

这是一段违反单一职责原则的经典案例。首先,类抽象本身就很有问题:LicenseConstants无法对应到现实世界、抽象概念中的任意实体,在业务建模阶段,你不可能构建这样一个模型对象。其次,整个License模块都可能是该类的修改点,如增加配置文件、资源项定义等各种场景。

再往上如模块、微服务、服务等也应遵循单一职责原则。其思考维度大同小异。

2.5 规约5:精简原则

一本书的完成,不在它不能加入任何内容的时候,而在不能再删去任何内容的时候。
----伏尔泰

生活中,我们希望在完成一件事时花费最少。其实,编码也是一样,好的程序员要对代码“吝啬”。完成同样的功能,我们希望自己编写的代码最少、最稳定、可读性最高。每一行代码经过精雕细琢后,将会深深的印在你的脑海里。

本节没有示例,只列出几个重要的检查项,开发人员在编码时懂得自省才是最重要的:

  • 是否存在重复代码可被抽象复用。不是所有的重复代码均应被抽象,可抽象的复用单元应具备业务合理性、提升代码可读性、抽象稳定性等特点。
  • 多分支是否可合并处理。如swtich\case中多个case是否可合并,是否有多个if的处理流程可合并等等。
  • 是否引入了不必要的依赖。即低扇出原则,一个类应尽量少的依赖外部类,提升该类的稳定性。
  • 高扇入原则。抽象类应尽可能的被复用,而不是重复造轮子。
  • 代码深度是否可进一步降低。包括代码深度、圈复杂度等。
  • 是否有更简单的实现方式
    示例:找到type为grocery的所有交易,返回按交易值降序排序后的ID集合
//找到type为grocery的所有交易
List<Transaction> groceryTrans = new Arraylist<>();
for(Transaction t: transactions){
    if(t.getType() == Transaction.GROCERY) {        
        groceryTrans.add(t);
    }
}

//按交易值降序排序
Collections.sort(groceryTrans, new Comparator(){
    public int compare(Transaction t1, Transaction t2) {
        return t2.getValue().compareTo(t1.getValue());
    }
});

//获取ID集合
List<Integer> transIds = new ArrayList<>();
for(Transaction t: groceryTrans){
    transIds.add(t.getId());
}

采用Java8的Lambda、Stream的实现如下:

List<Integer> transIds = transactions.
    parallelStream().
    filter(t -> t.getType() == Transaction.GROCERY).    //找到type为grocery的所有交易
    sorted(comparing(Transaction::getValue).reversed()).    //按交易值降序排序
    map(Transaction::getId).            //获取ID集合
    collect(toList());              //以List形式返回

2.6 规约6:生命周期最小化原则

不知道各位有没有这样的体验:在编程阶段,大脑中模拟程序运行,并验证结果是否符合预期,从而调整代码。在定位问题时也是一样:测试描述操作步骤和问题现象,在脑中构想程序运行经过的分支,推测可能出现问题的代码段。如果有过上述的经历,说明对于编程至少已经入门。反过来,对于一切问题定位都依赖日志、调试信息的开发人员,我只想说:换一个行业,希望还来得及。

继续刚才的话题,如果希望模拟结果正确,代码必须精简,同时系统运行状态可控。关于代码精简可参见上一规约,运行状态可控就是本节要讲的生命周期最小化。

系统运行状态管理,说的简单点就是变量生命周期管理。程序中每个变量代表了一种可变状态,后端代表的实体可能是内核锁资源、内存数据、文件、数据库、网络等等。程序在运行过程中,这些数据不断变化,共同组成了系统的运行状态。变量越多、生命周期越长,系统越复杂、越不容易稳定。

我们希望尽量控制变量的数量和生命周期,使系统具备“可追踪性”。变量的作用域越大,变量的“可追踪性”越弱。对于变量使用的优先级应该是:无变量 > 局部变量 > 成员变量 > 全局变量(静态变量)

  • 无变量:无需管理变量状态。
  • 局部变量:生命周期控制在函数或者{}内部,如果整个函数只依赖局部变量,那么对于固定的输入,输出也是固定的。只需要在函数(或{})内管理变量。
  • 成员变量:生命周期控制在对象内,对象内的多个函数共享成员变量,并发场景下需要做并发保护。
  • 全局变量(静态变量):全局变量始于该变量的首次使用,直至系统运行结束(或显示卸载);该类型的所有对象均可修改全局变量(静态变量)。

上面的表述直接、简单,但没讲具体怎么做。下面列几个典型的、违反可追踪性的示例加以说明:

  1. 示例:将成员变量降至局部变量
    可降至局部变量的成员变量应局部如下特点:
    1.a) 成员变量本身不是对象的核心属性
    1.b) 成员变量只被一个对外(public)方法使用,其余方法不使用或仅private方法使用。

违规代码:

class   DemoClass
{
    private int xx;

    public void doSomething(int param)
    {
        //do ...
        xx = calcData(param);
        doAnotherThing();
    }

    private void doAnotherThing()
    {
        switch(xx)
        {
            case 0:
                //...
                break;
            case 1:
                //...
                break;
        }
    }
}

修改后:

class   DemoClass
{
    public void doSomething(int param)
    {
        //do ...
        int xx = calcData(param);
        doAnotherThing(xx);
    }

    private void doAnotherThing(int xx)
    {
        switch(xx)
        {
            case 0:
                //...
                break;
            case 1:
                //...
                break;
        }
    }
}
  1. 示例:成员变量生命周期放大

我们说一个变量是成员变量时,强调的一定是它的“成员”属性,也就是说这个变量是类对象的一员,那么它就不应该脱离类对象的管控。一旦将该成员变量做为引用对象对外暴露后,该成员变量的生命周期就被扩大了,具备了“全局”属性。这样做有如下几个坏处:

  1. 成员变量的声明周期大于所属对象的声明周期,所属对象销毁后,其“”成员变量”依旧存活。
  2. 成员变量可不经过所属对象直接做状态变更,状态变更难以跟踪。

违规代码:

class Resource
{
    private int num;
    private String name;

    public void setNum(int num) { this.num = num; }
    public int getNum() { return num; }
    public void setName(String name) { this.name = name; }
    public String getName() { return name; }
}

class ResourceMgr
{
     private List<Resource> resources;

    public List<Resource> getResources()
    {
        return resources;
    }
}

class DemoClass
{
    public void doSomething()
    {
        List<Resource> resources =  ResourceMgr.getInstance().getResources();
        for(Resource resource : resources)
        {
              if (resource.getName().equals("xxx"))
              {
                   //do business with resource
                   return;
              }
        }
    }
}

ResourceMgr管理一组Resource对象,但其却将内部管理的的resources列表直接返回至外部,成员变量生命周期被放大。一种可能的修改方案如下:

class ResourceMgr
{
     private List<Resource> resources;

    public List<Resource> find(String name)
    {
        return resources.stream().filter((resource) -> {
            return resource.getName().equals(name);
        }).findFirst().orElse(null);
    }
}

class DemoClass
{
    public void doSomething()
    {
        Resource resource = ResourceMgr.getInstance().find("xxx");
        if (resource == null)
        {
            return ;
        }

        ///do business with resource
    }
}

该方案提供一个查询接口,返回符合条件的资源对象。看似避免resources对外暴露,但返回的对象依旧为成员变量,本质上并无差别。真正合理的修改方案如下:

class Resource implements Cloneable { //!!!注意这里
    private int num;
    private String name;

    public void setNum(int num) {
        this.num = num;
    }

    public int getNum() {
        return num;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    @Override
    public Resource clone() throws CloneNotSupportedException {
        return (Resource) super.clone();
    }
}

class ResourceMgr {
    private static ResourceMgr instance = new ResourceMgr();
    
    private List<Resource> resources = new ArrayList<>();
    
    private ResourceMgr() {
    }
    
    public static ResourceMgr getInstance() {
        return instance;
    }

    public Resource find(String name) {
        //查找符合条件的数据
        Resource resource = resources.stream().filter((r) -> {
            return r.getName().equals(name);
        }).findFirst().orElse(null);

        if (resource == null) {
            return null;
        }
        
        //!!!注意这里
        //返回cloneable对象
        try {
            return resource.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
}

class DemoClass
{
    public void doSomething()
    {
        Resource resource = ResourceMgr.getInstance().find("xxx");
        if (resource == null)
        {
            return ;
        }

        ///do business with resource
    }
}

为Resource实现Cloneable方法,find返回cloneable对象而非成员变量本身,有效避免成员变量的生命周期被放大。另外一种修改方案:

//!!!注意这里
interface IResource
{
    int getNum();
    String getName();
}

//!!!注意这里
class Resource implements IResource {
    private int num;
    private String name;

    public void setNum(int num) {
        this.num = num;
    }

    public int getNum() {
        return num;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

class ResourceMgr {
    private static ResourceMgr instance = new ResourceMgr();
    
    private List<Resource> resources = new ArrayList<>();
    
    private ResourceMgr() {
    }
    
    public static ResourceMgr getInstance() {
        return instance;
    }

    //!!!注意这里
    public IResource find(String name) {
        //查找符合条件的数据
        return resources.stream().filter((r) -> {
            return r.getName().equals(name);
        }).findFirst().orElse(null);
    }
}

class DemoClass
{
    public void doSomething()
    {
            //注意这里
        IResource resource = ResourceMgr.getInstance().find("xxx");
        if (resource == null)
        {
            return ;
        }

        ///do business with resource
    }
}

抽象一个接口类,find时返回接口IResource而非实现Resource对象。此时,成员变量的生命周期被放大,但对象的状态变更依旧在所属对象的控制之内。二者各有优缺点,到底使用哪种方案要根据具体使用场景而定。

上述两种方案都尝试解决控制成员变量的生命周期和状态变更的“可追踪性”,但如果我们期望的是更新符合find条件的对象,此时上述两种方案均不可取,而应在ResourceMgr中提供相应的接口,如下:

class ResourceMgr {
    private static ResourceMgr instance = new ResourceMgr();

    private List<Resource> resources = new ArrayList<>();

    private ResourceMgr() {
    }

    public static ResourceMgr getInstance() {
        return instance;
    }

    public boolean update(String name, int num) {
        Resource resource = resources.stream().filter((r) -> {
            return r.getName().equals(name);
        }).findFirst().orElse(null);

        if (resource == null) {
            return false;
        }

        resource.setNum(num);
        return true;
    }
}
  1. 示例:控制局部变量的生命周期

成员变量的生命周期控制在函数内部,因为其生命周期本身较短,我们通常疏于管理,使得生命周期放大,可读性降低。

违规代码:

    public void doSomething(List<Resource> resources, List<Device> devices) {
        int resourceChecksum = 0;
        int deviceChecksum = 0;
        int resourceTotal = 0;
        int deviceTotal = 0;
        int resourceAverage = 0;
        int deviceAverage =0;
        StringBuilder resourceStringBuilder = new StringBuilder();
        StringBuilder deviceStringBuilder = new StringBuilder();
        
        //计算resource的average、checksum
        for (Resource resource : resources) {
            resourceStringBuilder.append(resource.getName());
            resourceTotal += resource.getNum();
        }
        resourceChecksum = resourceStringBuilder.toString().hashCode();
        resourceAverage = resourceTotal / resources.size();
        
        //计算device的average、checksum
        for (Device device : devices) {
            deviceStringBuilder.append(device);
            deviceTotal += device.getNum();
        }
        deviceChecksum = deviceStringBuilder.toString().hashCode();
        deviceAverage = deviceTotal / devices.size();
        
        doAnoterThing(resourceChecksum, resourceAverage, deviceChecksum, deviceAverage);
    }

上述代码将所有的变量声明定义在函数顶部,但和device相关的变量只有在计算device时才需要,生命周期被放大。针对这点,可以做如下修改:

    public void doSomething(List<Resource> resources, List<Device> devices) {
        int resourceChecksum = 0;
        int resourceTotal = 0;
        int resourceAverage = 0;
        StringBuilder resourceStringBuilder = new StringBuilder();
        
        //计算resource的average、checksum
        for (Resource resource : resources) {
            resourceStringBuilder.append(resource.getName());
            resourceTotal += resource.getNum();
        }
        resourceChecksum = resourceStringBuilder.toString().hashCode();
        resourceAverage = resourceTotal / resources.size();
        
        int deviceChecksum = 0;
        int deviceTotal = 0;
        int deviceAverage =0;
        StringBuilder deviceStringBuilder = new StringBuilder();
        
        //计算device的average、checksum
        for (Device device : devices) {
            deviceStringBuilder.append(device.getIdentifier());
            deviceTotal += device.getNum();
        }
        deviceChecksum = deviceStringBuilder.toString().hashCode();
        deviceAverage = deviceTotal / devices.size();
        
        doAnoterThing(resourceChecksum, resourceAverage, deviceChecksum, deviceAverage);
    }

device相关变量生命周期缩小,可读性提高,但代码看起来还是不够整洁。仔细分析代码的最后一行,我们只需要resourceChecksum, resourceAverage, deviceChecksum, deviceAverage,但函数的生命周期中却增加了两个stringbuilder和两个total变量。我们期望这些临时变量的生命周期进一步降低,如下:

    public void doSomething(List<Resource> resources, List<Device> devices) {
        //计算resource的average、checksum
        int resourceChecksum = 0;
        int resourceAverage = 0;
        {
            int total = 0;
            StringBuilder stringBuilder = new StringBuilder();
            
            for (Resource resource : resources) {
                stringBuilder.append(resource.getName());
                total += resource.getNum();
            }
            resourceChecksum = stringBuilder.toString().hashCode();
            resourceAverage = total / resources.size();
        }


        //计算device的average、checksum
        int deviceChecksum = 0;
        int deviceAverage =0;
        {
            int total = 0;
            StringBuilder stringBuilder = new StringBuilder();
            
            for (Device device : devices) {
                stringBuilder.append(device.getIdentifier());
                total += device.getNum();
            }
            deviceChecksum = stringBuilder.toString().hashCode();
            deviceAverage = total / devices.size();
        }
        
        doAnoterThing(resourceChecksum, resourceAverage, deviceChecksum, deviceAverage);
    }   

经过二次重构后,我们不但降低了sting builder和total变量的生命周期,也避免了名称污染。不同场景下的string builder和total可以同名,代码可读性进一步提升。关于使用{}控制生命周期的技巧还有很多,不再一一列举。

3. 架构规约

  • 规约7:针对扩展点编程

3.7 规约7:针对扩展点编程

你愚弄了我一次,可耻的是你;但如果你愚弄了我两次,可耻的是我。
----《福尔摩斯:基本演绎法》

首先声明,这是一条附赠规约,不是说它不重要,而是因为以本文的篇幅,不可能讲通。针对扩展点编程是一个很大的话题,大到《设计模式》要花整本书来讲。本文只能提供一些指导建议,希望能有所帮助。

本节,我们讨论两个问题:什么是扩展点?以及如何针对扩展点编程?

  • 什么是扩展点?
    扩展点也称之为变化点,指的是软件中频繁变化的那一部分。系统中哪部分需求最活跃,那这部分就是系统的扩展点的来源。将需求映射到现有的系统业务模型,需要修改的那部分代码就是扩展点实现。
    最理想的效果是:整个系统框架为所有的扩展点都做了预留,后续需求开发只需要在预留的“slot”上完成,系统框架不需要做任何变更。要达到这个效果,我们就需要具备预知能力,预知系统的演进方向、潜在需求等等。
    对未来准确预知是不可能的,所以不存在完美的架构。构造出持续演进的框架是有可能的,但依然非常困难,你需要具备如下能力:

    • 业务建模的能力。首先,你要是一个业务专家。熟悉你所在行业,并能提炼出准确的业务模型。
    • 基于功能、质量属性架构的能力。功能来源于用户需求,质量属性指的是性能、安全、可扩展性、可用性、一致性等等。很多质量属性是相互矛盾的,典型的如CAP理论。如何选择质量属性,在满足功能需求的同时,最大程度匹配系统的演进方向。
    • 对扩展点的把控力。并不是所有的需求都会成为系统的扩展点,好的设计人员有能力评判断哪些需求能成为扩展点,而哪些不能。错失扩展点抽象,将会影响系统的稳定性;增加无谓的扩展点,将增加维护成本,不利于对整个系统的理解。
    • 持续演进的能力。一个基于持续演进架构的、稳定的系统,其需求量和代码量之间不应该呈现出线性关系,而是逐渐收敛的。
      其实,系统的扩展点也大致遵循上述原则。我们强调持续演进,时刻考虑推翻之前的部分架构设计,用更符合当前系统演进的架构替代,保持系统的生命力。
      至于如何挖掘扩展点,建议参考《敏捷软件开发--原则、模式与实践》、《领域驱动设计》。
  • 如何和针对扩展点编程?
    一旦你有能力识别扩展点,那么针对扩展点编程就变得很容易了。
    针对扩展点编程最主要的一个指导原则:开放封闭原则(OCP)。OCP的标准定义为:针对修改封闭,针对扩展开放。用通俗的话来说,如果有一个新的需求,我们期望达到如下效果:现有代码一行不改(或极少改动),在现有代码基础上通过增加新的类或配置的方式实现需求功能。
    关于OCP原则,《敏捷软件开发--原则、模式与实践》的第九章有详细描述,本文不再赘述。另外,GOF的《设计模式》就是从实践中总结出来的、针对扩展点编程的具体方法,业界的经典之作。

4. 总结

本文讲了6条基本编程规约、1条架构指导规约。我尝试以最少的文字描述,帮助一线开发人员提升编码能力,但最后文章的篇幅还是超出了我的预期。

同样,限于本文的篇幅,有太多的细节没有展开,好在有前辈早已帮我们整理成文。如本文最开始说的,如果你真正的想提高自身的编码、架构能力,请仔细翻阅以下书籍(建议按先后顺序阅读):

  1. 《重构--改善既有代码的设计》
  2. 《代码大全》
  3. 《敏捷软件开发--原则、模式与实践》
  4. 《设计模式--可复用面向对象软件的基础》
  5. 《领域驱动设计--软件核心复杂性应对之道》
  6. 《计算机程序的构造和解释》

(完)


【转载请注明】随安居士. 软件编程基础规约. 2018.06.13

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容

  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 11,089评论 1 32
  • 当一个男人同时爱着这两个女人,他会怎么做呢
    容乃公阅读 91评论 0 0
  • 一年一度的端午节又到了。前几天接到家里电话,问要不要给我寄点粽子吃。我拒绝了,因为粽子哪里都能买得到,但是出门在外...
    舌尖八闽阅读 541评论 0 2
  • 最近发现自己在等待中浪费了很多的时间,想把这个照片拍的美一点,再上架,想要把课程大纲设计的更完美一点再公布,想要把...
    芊蓁中医芳疗阅读 613评论 1 9
  • 离开你眼前看到的一切 一段时间系统内的初任培训,虽然课程安排很紧凑,每天都很疲惫,但是又认识了很多优秀的小伙伴。处...
    我乘一叶小舟阅读 131评论 1 1