Clean Code – System

By | 2019年7月28日

“复杂要人命,它消磨开发者的生命,让产品难以规划、构建和测试” — Ray Ozzi, 微软首席技术官

1. 如何建造一座城市

你能掌握一切细节吗?大概不行。即便是管理一个既存的城市,也是一个人无法做到的。城市能运转,是因为它演化出恰当的抽象等级和模块,好让个人和他们工作的部门即便在不了解全局时也能有效的运转。

软件设计也是这样,要将关注面分离以及对系统抽象分层。

2. 类不要自己构造依赖对象


public class Worker {
  public void doSomething() {
    Service service = new MyServiceImpl(new Component1(), new Component2());
    /* Do something */
  }
}

上面的代码不好,对象的使用和构造应当分离,Work类不应当知道MyServiceImpl怎么创建。主要有两个严重问题。

第一,不便于测试。很难mock MyServiceImpl类,虽然PowerMokito可以做到。

第二,违反了单一职责原则。Work多了一个职责去管理创建Service,还增加了三个不必要的依赖:MyServiceImpl, Component1和Component2。当MyServiceImple的构造方法变化时,Work类也要变化。

2.1 从外面传入Service

如果Service对象只是本方法使用,可以从该方法传入。


public class Worker {
  public void doSomething(Service service) {
    /* Do something */
  }
}

如果Worker类中很多方法都使用Service,可以将Service声明为field,使用set方法传入(当然也可以在构造方法中传入)。


public class Worker {
  private Service service;
    public void doSomething() {
    /* Do something */
  }

  public void setService(Service service) {
    this.service = service;
  }
}

2.2 使用工厂对象

Worker调用ServiceFactory(抽象工厂)生成Service,来解耦和MyServiceImpl的依赖。


public class Worker {
  private ServiceFactory serviceFactory;

  public void doSomething() {
    Service service = serviceFactory.createService();
    /* Do something */
  }
}

2.3 使用依赖注入

有一种强大的机制可以实现分离构造和使用,那就是控制反转(Inversion of Control, IoC)和依赖注入(Dependency Injection, DI)。控制反转和依赖注入是同一个东西,只是看问题角度不一样。一开始被称为控制反转,指对象不自己实例化依赖对象,将这部分职责反转给其它对象或容器。由于控制反转这一概念不够形象,2004年Martin Fowler给它起了个新的名字“依赖注入”。依赖注入,明确表明类使用的依赖是注入进来的,而不类自己创建的。

Spring框架tight了最有名的Java DI容器,从一开始的配置XML,到后来的Annotation,再到后面的Auto Scan(自动扫描),使用起来越来越方便。用户可以设置容器中对象是单例的还是每次生成新的对象,以及是否是Lazy(调用时才创建对象)等。

3. 扩容

城市由城镇而来,城镇由聚居而来。一开始道路狭窄,几乎无人涉足,随后逐渐拓宽。有多少次,你开着车艰难穿过一个“道路整改”工程,心想“它们为什么不一开始就修条够宽的路呢?!”。那样是不现实的,谁敢打包票说在一个小镇修建一条六车道的公路并不浪费呢?谁会想要那么一条公路穿过他们小镇呢?

“一开始就做对系统”纯属神话。反之,我们应该只去实现今天的用户需求,然后重构,明天再扩展系统,实现新的用户需求,这就是迭代和增量敏捷的精髓。要实现这样的系统,我们要将持续关注的切面恰当分离,说白了就是使用切面编程,每个Aspect各自完成自己的功能。

未分离切面设计

EJB1和EJB2架构没有恰当分离切面,本地EJB接口需要继承java.ejb.EJBLocalObject,Entyty Bean需要实现javax.ejb.EntityBean接口。业务逻辑与EJB2容器紧密耦合,面向对象本身也被侵蚀,因为继承了EJB的类就不能再继承其它的类了。

分类切面设计

EJB3通过XML或Annotation分离切面。下面的Bank是Entity Bean,它不需要了解容器,通过Annotation(也可以用XML)EJB3就知道如何将其和DB数据映射。


@Entity
@Table(name = "BANKS")
public class Banker implements java.io.Serializable {
  @ID @GeneratedValue(strategy=GenerationType.AUTO)
  private int id;
    /*Other fields*/
}

下面来看看Java中的三种切面编程方式。

3.1 Java动态代理

JDK的动态代理对象必须实现某个接口,如果代理对象没有实现接口,则需要使用CGLIB等字节码工具。下面是Java动态代理的例子:


interface Plane {
  void fly();
}

interface Animal {
  void run();
}

public static void main(String[] args) {
  Object proxyObj = Proxy.newProxyInstance(
      Tester.class.getClassLoader(),
      new Class[] {Animal.class, Plane.class},
      new InvocationHandler() {
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("run".equals(method.getName())) {
        System.out.println("Animal runs in the field.");
      } else if ("fly".equals(method.getName())) {
        System.out.println("Plane flys in the sky.");
      }
      return null;
    }
  });

  ((Animal) proxyObj).run();
  ((Plane) proxyObj).fly();
}

输出结果:


Animal runs in the field.
Plane flys in the sky.

3.2 Spring AOP编程

Spring AOP支持使用XML或Annotation声明切面,下面是使用@Around的Example:

NewImage


@Aspect
public class Audiences {

  @Pointcut("execution(* com.test.aop.Performance.perform(..))")
  public void perform() {}

  @Around("perform()")
  public void watchPerformance(ProceedingJoinPoint jp) {
    try {
      Object[] args = jp.getArgs();

      System.out.println("Silencing cell phone");
      System.out.println("Taking seats");

      jp.proceed();

      System.out.println("CLAP CLAP CLAP!!!" + " Cool " + args[0]);
    } catch (Throwable e) {
      System.out.println("Demanding a refund.");
    }
  }

}

当执行Performance类的perform方法时,它就会被代理。

3.3 AspectJ 切面编程

在切面编程中,80% – 90%情况下,Spring AOP 足以满足需求。当我们需要代理构造方法时,既类构造前后执行某些操作,Spring AOP就不胜任,需要使用AspectJ。AspectJ有自己的AOP语法,使用专门的编译器生成代理Class,也就是说编译期代理。Spring AOP主要使用CGLIB在运行期进行代理。

4. 小结

使用AOP编程,我们就可以使用POJO编写应用程序的领域逻辑,在代码层面和架构关注面分离,这样才能真正地用测试来驱动架构

没有必要先做大设计(Big Desgn Up Front, BDUF),DBUF是有害的,它阻碍改进,因为心理上会抵制丢弃既成之事,也因为架构上的方案选择影响到后续的设计思路。应当从简单但切分良好的架构开始软件项目,快速交付可以工作的产品,随着规模的增加来添加更多基础架构。

系统也应该是简洁的。侵害性架构会湮灭领域逻辑,冲击敏捷能力。当领域逻辑受到困扰,质量也就堪忧。无论是设计系统或者单独的模块,别忘了使用大概可以工作的最简单方案