Tuesday, January 12, 2016

Programming DSL: Implements JHamcrest



https://codingstyle.cn/topics/79
接下来继续以find为例,通过应用「正交设计」的基本原则,重点讨论DSL的设计过程。首先回顾一下之前find算子重构的成果。
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();
}
另外根据需求1~4,抽象了2个变化方向:
  • 比较运算:==, !=
  • 逻辑运算:&&, ||

比较语义

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);
  }
}
查找年龄不等于18岁的学生,可以如此描述。
assertThat(find(students, age(ne(18))).isPresent(), is(true));

逻辑语义

public interface Predicate<E> {
  boolean test(E e);

  default Predicate<E> and(Predicate<? super E> other) {
    return e -> test(e) && other.test(e);
  }

  default Predicate<E> or(Predicate<? super E> other) {
    return e -> test(e) || other.test(e);
  }
}
查找名字叫horance的男生,可以表述为:
assertThat(find(students, name(eq("horance")).and(Human::male)).isPresent(), is(true));

引入工厂

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);
  }
}
将所有的static factory方法都放在接口中,虽然简单,也很自然。但如果方法之间产生重复代码,需要提取函数,设计将变得非常不灵活,因为接口内所有方法都将默认为public,这往往不是我们所期望的,为此可以将这些static factory方法搬迁到Matchers实用类中去。
public final class Matchers {    
  public static <T> Matcher<T> eq(T expected) {
    return actual -> expected.equals(actual);
  }

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

  private Matchers() {
  }
}

完备的比较规则

实现大于

需求5: 查找年龄大于18岁的学生
assertThat(find(students, age(gt(18)).isPresent(), is(true));
public final class Matchers {
  ......

  public static <T extends Comparable<T>> Matcher<T> gt(T expected) {
    return actual -> Ordering.<T>natural().compare(actual, expected) > 0;
  }
}
其中,natural代表了一种自然的比较规则。
public final class Ordering {
  public static <T extends Comparable<T>> Comparator<T> natural() {
    return (t1, t2) -> t1.compareTo(t2);
  }
}

实现小于

需求6: 查找年龄小于18岁的学生
assertThat(find(students, age(lt(18)).isPresent(), is(true));
依次类推,「小于」的规则实现如下:
public final class Matchers {
  ......

  public static <T extends Comparable<T>> Matcher<T> gt(T expected) {
    return actual -> Ordering.<T>natural().compare(actual, expected) > 0;
  }

  public static <T extends Comparable<T>> Matcher<T> lt(T expected) {
    return actual -> Ordering.<T>natural().compare(actual, expected) < 0;
  }
}

提取函数

设计产生了明显的重复,可以通过「提取函数」来消除重复。
public final class Matchers {
  ......

  public static <T extends Comparable<T>> Matcher<T> gt(T expected) {
    return actual -> compare(actual, expected) > 0;
  }

  public static <T extends Comparable<T>> Matcher<T> lt(T expected) {
    return actual -> compare(actual, expected) < 0;
  }

  private static <T extends Comparable<T>> int compare(T actual, T expected) {
    return Ordering.<T>natural().compare(actual, expected);
  }
}
其余比较操作,例如大于等于,小于等于的设计和实现依此类推,在此不再重述。

完备的字符串操作

包含子串

需求7: 查找名字中包含horance的学生
assertThat(find(students, name(contains("horance")).isPresent(), is(true));
public final class Matchers {    
  ......

  public static Matcher<String> contains(String substr) {
    return str -> str.contains(substr);
  }
}

子串开头

需求8: 查找名字以horance开头的学生
assertThat(find(students, name(starts("horance")).isPresent(), is(true));
public final class Matchers {    
  ......

  public static Matcher<String> starts(String substr) {
    return str -> str.startsWith(substr);
  }
}
「子串结尾」的逻辑,可以设计ends的关键字,实现依此类推,在此不再重述。

不区分大小写

需求9: 查找名字以horance开头,但不区分大小写的学生
assertThat(find(students, name(starts_ignoring_case("horance")).isPresent(), is(true));
public final class Matchers {    
  ......

  public static Matcher<String> starts_ignoring_case(String substr) {
    return str -> lower(str).startsWith(lower(substr));
  }

  private static String lower(String s) {
    return s.toLowerCase();
  }
}

修饰语义

需求13: 查找名字中不包含horance的第一个学生
assertThat(find(students, name(not_contains("horance")).isPresent(), is(true));
public final class Matchers {    
  ......

  public static Matcher<String> not_contains(String substr) {
    return str -> !str.contains(substr);
  }
}
在这之前,也曾遇到过类似的「反义」的操作。例如,查找年龄不等于18岁的学生,可以如此描述。
assertThat(find(students, age(ne(18))).isPresent(), is(true));
public final class Matchers {    
  ......

  public static <T> Matcher<T> ne(T expected) {
    return actual -> !expected.equals(actual);
  }
}
两者对「反义」的描述存在两份不同的表示,是一种隐晦的「重复设计」,需要一种巧妙的设计消除重复。

提取反义

为此,应该删除not_contains, ne的关键字,并提供统一的not关键字。
assertThat(find(students, name(not(contains("horance")))).isPresent(), is(true));
not的实现是一种「修饰」的手法,对既有的Matcher功能的增强,巧妙地取得了「反义」功能。
public final class Matchers {    
  ......

  public static <T> Matcher<T> not(Matcher<T> matcher) {
    return actual -> !matcher.matches(actual);
  }
}

语法糖

对于not(eq(18))可以设计类似于not(18)的语法糖,使其更加简单。
assertThat(find(students, age(not(18))).isPresent(), is(true));
其实现就是对eq的一种修饰操作。
public final class Matchers {    
  ......

  public static <T> Matcher<T> not(T expected) {
    return not(eq(expected));
  }
}

组合语义

逻辑或

需求13: 查找名字中包含horance,或者以liu结尾的学生
assertThat(find(students, name(anyof(contains("horance"), ends("liu")))).isPresent(), is(true));
public final class Matchers {    
  ......

  @SafeVarargs
  private static <T> Matcher<T> anyof(Matcher<? super T>... matchers) {
    return actual -> {
      for (Matcher<? super T> matcher : matchers) {
        if (matcher.matches(actual)) {
          return true;
        }
      }
      return false;
    };
  }
}

逻辑与

需求14: 查找名字中以horance开头,并且以liu结尾的学生
assertThat(find(students, name(allof(starts("horance"), ends("liu")))).isPresent(), is(true));
public final class Matchers {    
  ......

  @SafeVarargs
  private static <T> Matcher<T> allof(Matcher<? super T>... matchers) {
    return actual -> {
      for (Matcher<? super T> matcher : matchers) {
        if (matcher.matches(actual)) {
          return true;
        }
      }
      return false;
    };
  }
}

短路

allofanyof之间的实现存在重复设计,可以通过提取函数消除重复。
public final class Matchers {    
  ......

  @SafeVarargs
  private static <T> Matcher<T> combine(boolean shortcut, Matcher<? super T>... matchers) {
    return actual -> {
      for (Matcher<? super T> matcher : matchers) {
        if (matcher.matches(actual) == shortcut) {
          return shortcut;
        }
      }
      return !shortcut;
    };
  }

  @SafeVarargs
  public static <T> Matcher<T> allof(Matcher<? super T>... matchers) {
    return combine(false, matchers);
  }

  @SafeVarargs
  public static <T> Matcher<T> anyof(Matcher<? super T>... matchers) {
    return combine(true, matchers);
  }
}

占位符

需求15: 查找算法始终失败
assertThat(find(students, age(always(false))).isPresent(), is(false));
public final class Matchers {    
  ......

  public static <E> Matcher<E> always(boolean bool) {
    return e -> bool;
  }
}

结论

通过一系列的需求的演进和迭代,通过遵循「正交设计」的基本原则,我们得到了一套接口丰富、表达力极强的DSL。这一套简单的DSL是一个高度可复用的Matcher集合,为此我将其称为JHamcrest,是一个简化版的Hamcrest实现,其设计既包含了OO的方法论,也涉及到了一些FP的思维,整体性设计保持高度的一致性和统一性。

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