Tuesday, January 12, 2016

正交设计 - orthogonal design



https://codingstyle.cn/topics/75
Design is there to enable you to keep changing the software easily in the long term. -- Kent Beck.

设计是什么

正如Kent Beck所说,软件设计是为了「长期」更加容易地适应未来的变化。正确的软件设计方法是为了长期地、更好更快、更容易地实现软件价值的交付。

软件设计的目标

软件设计就是为了完成如下目标,其可验证性、重要程度依次减低。
  • 实现功能
  • 易于重用
  • 易于理解
  • 没有冗余

实现功能

实现功能的目标压倒一起,这也是软件设计的首要标准。如何判定系统功能的完备性呢?通过所有测试用例。
TDD的角度看,测试用例就是对需求的阐述,是一个闭环的反馈系统,保证其系统的正确性;及其保证设计的合理性,恰如其分,不多不少;当然也是理解系统行为最重要的依据。

易于理解

好的设计应该能让其他人也能容易地理解,包括系统的行为,业务的规则。那么,什么样的设计才算得上易于理解的呢?
  • Clean Code
  • Implement Patterns
  • Idioms

没有冗余

没有冗余的系统是最简单的系统,恰如其分的系统,不做任何过度设计的系统。
  • Dead Code
  • YAGNI: You Ain't Gonna Need It
  • KISS: Keep it Simple, Stupid

易于重用

易于重用的软件结构,使得其应对变化更具弹性;可被容易地修改,具有更加适应变化的能力。
最理想的情况下,所有的软件修改都具有局部性。但现实并非如此,软件设计往往需要花费很大的精力用于依赖的管理,让组件之间的关系变得清晰、一致、漂亮。
那么软件设计的最高准则是什么呢?「高内聚、低耦合」原则是提高可重用性的最高原则。为了实现高内聚,低耦合的软件设计,袁英杰提出了「正交设计」的方法论。

正交设计

「正交」是一个数学概念:所谓正交,就是指两个向量的内积为零。简单的说,就是这两个向量是垂直的。在一个正交系统里,沿着一个方向的变化,其另外一个方向不会发生变化。为此,Bob大叔将「职责」定义为「变化的原因」。
「正交性」,意味着更高的内聚,更低的耦合。为此,正交性可以用于衡量系统的可重用性。那么,如何保证设计的正交性呢?袁英杰提出了「正交设计的四个基本原则」,简明扼要,道破了软件设计的精髓所在。

正交设计原则

  • 消除重复
  • 分离关注点
  • 缩小依赖范围
  • 向稳定的方向依赖

实战

需求1: 存在一个学生的列表,查找一个年龄等于18岁的学生

快速实现

public static Student findByAge(Student[] students) {
  for (int i=0; i<students.length; i++) {
    if (students[i].getAge() == 18) {
        return students[i];
    }
  }
  return null;
}
为了阐述设计的整个思考,及其重构的过程,揭示软件设计过程的真实样貌,很多设计是故意地引入坏味道。例如,上述实现存在很多代码的坏味道:
  • 缺乏弹性参数类型:只支持数组类型,List, Set都被拒之门外;
  • 容易出错:操作数组下标,往往引入不经意的错误;
  • 幻数:硬编码,将算法与配置高度耦合;
  • 返回null:再次给用户打开了犯错的大门;

使用for-each

按照最小依赖的原则,现屏蔽掉数组下标的细节,使用for-each降低错误发生的可能性。
public static Student findByAge(Student[] students) {
  for (Student s : students) {
    if (s.getAge() == 18) {
        return s;
    }
  }
  return null;
}
需求2: 查找一个名字为horance的学生

重复设计

Copy-Paste是最快的实现方法,但会产生重复的设计。
public static Student findByName(Student[] students) {
  for (Student s : students) {
    if (s.getName().equals("horance")) {
        return s;
    }
  }
  return null;
}
为了消除重复,可以将查找的算法与比较的准则这两个关注点进行分离。

抽象准则

首先将比较的准则进行抽象化,让其独立变化。
public interface StudentPredicate {
  boolean test(Student s);
}
将各个「变化的原因」对象化,为此建立了两个简单的算子。
public class AgePredicate implements StudentPredicate {
  private int age;

  public AgePredicate(int age) {
    this.age = age;
  }

  @Override
  public boolean test(Student s) {
    return s.getAge() == age;
  }
}
public class NamePredicate implements StudentPredicate {
  private String name;

  public NamePredicate(String name) {
    this.name = name;
  }

  @Override
  public boolean test(Student s) {
    return s.getName().equals(name);
  }
}
此刻,查找算法的方法名也应该被重命名,使其保持一致的「抽象层次」。
public static Student find(Student[] students, StudentPredicate p) {
  for (Student s : students) {
    if (p.test(s)) {
        return s;
    }
  }
  return null;
}
客户端的调用根据场景,提供算法的配置。
assertThat(find(students, new AgePredicate(18)), notNullValue());
assertThat(find(students, new NamePredicate("horance")), notNullValue());

结构性重复

AgPredicateNamePredicate存在「结构型重复」,为此需要进一步消除重复。经分析两个类的存在无非是为了实现「闭包」的能力,可以使用lambda表达式,「Code As Data」,简明扼要。
assertThat(find(students, s -> s.getAge() == 18), notNullValue());
assertThat(find(students, s -> s.getName().equals("horance")), notNullValue());

引入Iterable

按照向稳定的方向依赖的原则,为了适应诸如List, Set等多种数据结构,甚至包括原生的数组类型,可以将入参重构为重构为更加抽象的Iterable类型。
public static Student find(Iterable<Student> students, StudentPredicate p) {
  for (Student s : students) {
    if (p.test(s)) {
        return s;
    }
  }
  return null;
}
需求3: 存在一个老师列表,查找第一个女老师

类型重复

按照既有的代码结构,可以通过Copy Paste快速地实现这个功能。
public interface TeacherPredicate {
  boolean test(Teacher t);
}
public static Teacher find(Iterable<Teacher> teachers, TeacherPredicate p) {
  for (Teacher t : teachers) {
    if (p.test(t)) {
        return t;
    }
  }
  return null;
}
用户接口依然可以使用Lambda表达式。
assertThat(find(teachers, t -> t.female()), notNullValue());
如果使用Method Reference,可以进一步地改善表达力。
assertThat(find(teachers, Teacher::female), notNullValue());

类型参数化

分析StudentMacher/TeacherPredicatefind(Iterable<Student>)/find(Iterable<Teacher>)的重复,其变化的关注点很明显:参数类型
为此,引入「类型参数化」设计消除重复。首先消除StudentPredicateTeacherPredicate的重复设计,
public interface Predicate<E> {
  boolean test(E e);
}
再对find进行类型参数化设计。
public static <E> E find(Iterable<E> c, Predicate<E> p) {
  for (E e : c) {
    if (p.test(e)) {
        return e;
    }
  }
  return null;
}

型变

find的类型参数缺乏「型变」的能力,为此引入「型变」能力的支持,接口更加具有可复用性。
public static <E> E find(Iterable<? extends E> c, Predicate<? super E> p) {
  for (E e : c) {
    if (p.test(e)) {
        return e;
    }
  }
  return null;
}

复用lambda

观察如下两个测试用例,如果做到极致,可认为两个lambda表达式也是重复的。从分离关注点的角度分析,此lambda表达式承载的「比较算法」与「参数配置」两个职责,应该对其进行分离。
assertThat(find(students, s -> s.getName().equals("Horance")), notNullValue());
assertThat(find(students, s -> s.getName().equals("Tomas")), notNullValue());
可以通过「Static Factory」生产lambda表达式,将比较算法封装起来;而配置参数通过引入「参数化」设计,将「逻辑」与「配置」分离,从而达到最大化的代码复用。
public final class StudentPredicates {
  private StudentPredicates() {
  }

  public static Predicate<Student> age(int age) {
    return s -> s.getAge() == age;
  } 

  public static  Predicate<Student> name(String name) {
    return s -> s.getName().equals(name);
  }
}
import static StudentPredicates.*;

assertThat(find(students, name("horance")), notNullValue());
assertThat(find(students, age(10)), notNullValue());

组合查询

但是,上述将lambda表达式封装在Factory的设计是及其脆弱的,例如,增加如下的需求:
需求4: 查找年龄不等于18岁女生
最简单的方法就是往StudentPredicates不停地增加「Static Factory」,但这样的设计严重违反了「OCP」(开放封闭)原则。
public final class StudentPredicates {
  ......

  public static Predicate<Student> ageEq(int age) {
    return s -> s.getAge() == age;
  } 

  public static Predicate<Student> ageNe(int age) {
    return s -> s.getAge() != age;
  } 
}
从需求看,比较准则增加了众多的语义,再次运用分离关注点的原则,可发现存在两类运算的规则:
  • 比较运算:==, !=
  • 逻辑运算:&&, ||
比较语义
先处理比较运算的关注点,为此建立一个Matcher的抽象:
public interface Matcher<T> {
  boolean matches(T actual);

  static <T> Matcher<T> eq(T expected) {
    return actual -> expected.equals(actual);
  }

  static <T> Matcher<T> ne(T expected) {
    return actual -> !expected.equals(actual);
  }
}
此刻,age的设计运用了「函数式」的思维,通过函数的「组合式设计」完成功能的自由拼装,简单、直接、漂亮。
public final class StudentPredicates {
  ......

  public static Predicate<Student> age(Matcher<Integer> m) {
    return s -> m.matches(s.getAge());
  }
}
查找年龄不等于18岁的学生,可以如此描述。
assertThat(find(students, age(ne(18))), notNullValue());
逻辑语义
为了使得逻辑「谓词」变得更加人性化,可以引入「流式接口」的「DSL」设计,增强表达力。
public interface Predicate<E> {
  boolean test(E e);

  default Predicate<E> and(Predicate<? super E> other) {
    return e -> test(e) && other.test(e);
  }
}
查找年龄不等于18岁的女生,可以表述为:
assertThat(find(students, age(ne(18)).and(Student::female)), notNullValue());

重复再现

仔细的读者可能已经发现了,StudentTeacher两个类也存在「结构型重复」的问题。
public class Student {
  public Student(String name, int age, boolean male) {
    this.name = name;
    this.age = age;
    this.male = male;
  }

  ......

  private String name;
  private int age;
  private boolean male;
}
public class Teacher {
  public Teacher(String name, int age, boolean male) {
    this.name = name;
    this.age = age;
    this.male = male;
  }

  ......

  private String name;
  private int age;
  private boolean male;
}
StudentTeacher的结构性重复,导致TeacherPredicatesTeacherPredicates也存在结构性重复,为此需要进一步消除重复。
public final class StudentPredicates {
  ......

  public static Predicate<Student> age(Matcher<Integer> m) {
    return s -> m.matches(s.getAge());
  }
}
public final class TeacherPredicates {
  ......

  public static Predicate<Teacher> age(Matcher<Integer> m) {
    return t -> m.matches(t.getAge());
  }
}

提取基类

第一个直觉,通过「提取基类」的重构方法,消除StudentTeacher的重复设计。
class Human {
  protected Human(String name, int age, boolean male) {
    this.name = name;
    this.age = age;
    this.male = male;
  }

  ...

  private String name;
  private int age;
  private boolean male;
}
从而实现了进一步消除了StudentTeacher之间的重复设计。
public class Student extends Human {
  public Student(String name, int age, boolean male) {
    super(name, age, male);
  }
}

public class Teacher extends Human {
  public Teacher(String name, int age, boolean male) {
    super(name, age, male);
  }
}
此时,可以通过引入「类型界定」的泛型设计,使得TeacherPredicatesTeacherPredicates合二为一,进一步消除重复设计。
public final class HumanPredicates {
  ......

  public static <E extends Human> 
    Predicate<E> age(Matcher<Integer> m) {
    return s -> m.matches(s.getAge());
  } 
}
消灭继承关系
StudentTeacher依然存在「结构型重复」的问题,可以通过static factory的设计方法,并让Human的构造函数「私有化」,删除StudentTeacher两个子类,彻底消除两者之间的重复设计。
public class Human {
  private Human(String name, int age, boolean male) {
    this.name = name;
    this.age = age;
    this.male = male;
  }

  public static Human student(String name, int age, boolean male) {
    return new Human(name, age, male);
  }

  public static Human teacher(String name, int age, boolean male) {
    return new Human(name, age, male);
  }

  ......
}
消灭类型界定
Human的重构,使得HumanPredicates的「类型界定」变得多余,从而进一步简化了设计。
public final class HumanPredicates {
  ......

  public static Predicate<Human> age(Matcher<Integer> m) {
    return s -> m.matches(s.getAge());
  } 
}

绝不返回null

在最开始,我们遗留了一个问题:find返回了null。用户调用返回null的接口时,常常忘记null的检查,导致在运行时发生NullPointerException异常。
按照「向稳定的方向依赖」的原则,find的返回值应该设计为Optional<E>,使用「类型系统」的特长,取得如下方面的优势:
  • 显式地表达了不存在的语义;
  • 编译时保证错误的发生;
import java.util.Optional;

public <E> Optional<E> find(Iterable<? extends E> c, Predicate<? super E> p) {
  for (E e : c) {
    if (p.test(e)) {
        return Optional.of(e);
    }
  }
  return Optional.empty();
}
  • 什么样的软件设计才算得上是好的?
  • 软件设计的本质是什么?
  • OOFP的本质区别是什么?

Labels

Review (572) System Design (334) System Design - Review (198) Java (189) Coding (75) Interview-System Design (65) Interview (63) Book Notes (59) Coding - Review (59) to-do (45) Linux (43) Knowledge (39) Interview-Java (35) Knowledge - Review (32) Database (31) Design Patterns (31) Big Data (29) Product Architecture (28) MultiThread (27) Soft Skills (27) Concurrency (26) Cracking Code Interview (26) Miscs (25) Distributed (24) OOD Design (24) Google (23) Career (22) Interview - Review (21) Java - Code (21) Operating System (21) Interview Q&A (20) System Design - Practice (20) Tips (19) Algorithm (17) Company - Facebook (17) Security (17) How to Ace Interview (16) Brain Teaser (14) Linux - Shell (14) Redis (14) Testing (14) Tools (14) Code Quality (13) Search (13) Spark (13) Spring (13) Company - LinkedIn (12) How to (12) Interview-Database (12) Interview-Operating System (12) Solr (12) Architecture Principles (11) Resource (10) Amazon (9) Cache (9) Git (9) Interview - MultiThread (9) Scalability (9) Trouble Shooting (9) Web Dev (9) Architecture Model (8) Better Programmer (8) Cassandra (8) Company - Uber (8) Java67 (8) Math (8) OO Design principles (8) SOLID (8) Design (7) Interview Corner (7) JVM (7) Java Basics (7) Kafka (7) Mac (7) Machine Learning (7) NoSQL (7) C++ (6) Chrome (6) File System (6) Highscalability (6) How to Better (6) Network (6) Restful (6) CareerCup (5) Code Review (5) Hash (5) How to Interview (5) JDK Source Code (5) JavaScript (5) Leetcode (5) Must Known (5) Python (5)

Popular Posts