Programming DSL: JMatchers


ソフトウェア設計は「守破離」のプロセスです.--袁英傑
デザインのレビュー
前回「ソフトウェア職人グループ」で「直交設計」の基本理論、原則と応用を共有し、活動ラインの下で多くの友人からフィードバックを受けた.その中でDSLの設計について話した人がいます.そのため、findを例に、「直交設計」の応用を通じて、DSLの設計過程を重点的に議論し続けます.
まず,これまでのfind演算子再構成の成果を振り返る.
public  Optional 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 {
      boolean matches(T actual);
        
      static  Matcher eq(T expected) {
        return actual -> expected.equals(actual);
      }
      
      static  Matcher ne(T expected) {
        return actual -> !expected.equals(actual);
      }
    }

    18歳以下の年齢の学生を検索するには、このように説明することができます.
    assertThat(find(students, age(ne(18))).isPresent(), is(true));

    ろんりてきいみ
    public interface Predicate {
      boolean test(E e);
    
      default Predicate and(Predicate super E> other) {
        return e -> test(e) && other.test(e);
      }
      
      default Predicate 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));

    探索して前進する.
    次に、需要の進化と反復を通じて、既存の設計と実現を改善し続けます.「直交設計」の基本原則を応用し、DSL設計に対する理解を深める.
    工場にひきこむ
    public interface Matcher {
      boolean matches(T actual);
        
      static  Matcher eq(T expected) {
        return actual -> expected.equals(actual);
      }
      
      static  Matcher ne(T expected) {
        return actual -> !expected.equals(actual);
      }
    }

    すべてのStatic Factoryメソッドをインタフェースに配置するのは簡単ですが、自然です.しかし、メソッド間で重複コードが生成されると、「抽出関数」が必要になり、インタフェース内のすべてのメソッドがpublicとデフォルト化されるため、設計が非常に柔軟になりません.これは、私たちが望んでいるものではありません.そのため、これらのStatic FactoryメソッドをMatchers実用クラスに移行することができます.
    public final class Matchers {    
      public static  Matcher eq(T expected) {
        return actual -> expected.equals(actual);
      }
      
      public static  Matcher 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 > Matcher gt(T expected) {
        return actual -> Ordering.natural().compare(actual, expected) > 0;
      }
    }

    このうち、naturalは自然な比較規則を表している.
    public final class Ordering {
      public static > Comparator natural() {
        return (t1, t2) -> t1.compareTo(t2);
      }
    }

    実装が小さい
    需要6:18歳未満の学生を探す
    assertThat(find(students, age(lt(18)).isPresent(), is(true));

    順に類推すると、「より小さい」という規則は以下のように実現される.
    public final class Matchers {
      ......
      
      public static > Matcher gt(T expected) {
        return actual -> Ordering.natural().compare(actual, expected) > 0;
      }
      
      public static > Matcher lt(T expected) {
        return actual -> Ordering.natural().compare(actual, expected) < 0;
      }
    }

    抽出関数
    設計は明らかな繰返しを生み出し,「抽出関数」により繰返しを除去できる.
    public final class Matchers {
      ......
      
      public static > Matcher gt(T expected) {
        return actual -> compare(actual, expected) > 0;
      }
      
      public static > Matcher lt(T expected) {
        return actual -> compare(actual, expected) < 0;
      }
      
      private static > int compare(T actual, T expected) {
        return Ordering.natural().compare(actual, expected);
      }
    }

    残りの比較動作、例えばの設計および実装は、ここでは再記述しない.
    サブストリングを含める
    需要7:名前にhoranceが含まれている学生を検索
    assertThat(find(students, name(contains("horance")).isPresent(), is(true));
    public final class Matchers {    
      ......
      
      public static Matcher 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 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 starts(String substr) {
        return str -> str.startsWith(substr);
      }
      
      public static Matcher starts_ignoring_case(String substr) {
        return str -> lower(str).startsWith(lower(substr));
      }
    
      private static String lower(String s) {
        return s.toLowerCase();
      }
    }
    startsstarts_ignoring_caseの間には微妙な重複設計が存在するため、重複をさらに解消する必要がある.
    コンビネーションデザイン
    assertThat(find(students, name(ignoring_case(Matchers::starts, "Horance"))).isPresent(), is(true));

    関数の「組合せ設計」を用いて,コードの最大多重性を達成する.OOの観点から見ると、ignoring_casestarts, ends, containsに対する機能強化であり、典型的な「修飾」関係である.
    public static Matcher ignoring_case(
      Function> m, String substr) {
      return str -> m.apply(lower(substr)).matches(lower(str));
    }

    ここで、Function>は1元関数であり、パラメータはStringであり、戻り値はMatcherである.
    @FunctionalInterface
    public interface Function {
        R apply(T t);
    }

    ユーザーの強制ignoring_caseの設計は高度に多重化可能であるが、ユーザは実際の状況に応じて、様々な演算子を自由に組み合わせることができる.しかし「メソッドリファレンス」の文法は,ユーザに不要な負担を与える.
    assertThat(find(students, name(ignoring_case(Matchers::starts, "Horance"))).isPresent(), is(true));
    starts_ignoring_caseの構文糖を提供し、ユーザーがミスを犯す確率を最小限に抑えることができますが、重複設計が存在しないことを保証します.
    assertThat(find(students, name(starts_ignoring_case("Horance"))).isPresent(), is(true));

    この場合、ignoring_caseprivateに再構成され、「再利用可能」な関数になるべきである.
    public static Matcher starts_ignoring_case(String substr) {
      return ignoring_case(Matchers::starts, substr);
    }
     
    private static Matcher ignoring_case(
      Function> m, String substr) {
      return str -> m.apply(lower(substr)).matches(lower(str));
    }

    意味を修飾する
    需要13:検索名にhoranceが含まれていない最初の学生
    assertThat(find(students, name(not_contains("horance")).isPresent(), is(true));
    public final class Matchers {    
      ......
      
      public static Matcher 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  Matcher ne(T expected) {
        return actual -> !expected.equals(actual);
      }
    }

    両者は「反義」の記述に対して2つの異なる表現が存在し、暗い「繰り返し設計」であり、巧みな設計が繰り返しを解消する必要がある.
    アンチセンスを抽出する
    このためには、not_contains, neのキーワードを削除し、統一されたnotのキーワードを提供する必要があります.
    assertThat(find(students, name(not(contains("horance")))).isPresent(), is(true));
    notの実現は「修飾」の手法であり、既存のMatcher機能の増強に対して、巧みに「反義」機能を得た.
    public final class Matchers {    
      ......
      
      public static  Matcher not(Matcher 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  Matcher 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
      public static  Matcher 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
      public static  Matcher allof(Matcher super T>... matchers) {
        return actual -> {
          for (Matcher super T> matcher : matchers)
            if (!matcher.matches(actual))
              return false;
          return true;
        };
      }
    }

    たんらくallofanyofとの間の実装には重複設計が存在し、抽出関数によって重複を除去することができる.
    public final class Matchers {    
      ......
      
      @SafeVarargs
      private static  Matcher 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  Matcher allof(Matcher super T>... matchers) {
        return combine(false, matchers);
      }
        
      @SafeVarargs
      public static  Matcher 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  Matcher always(boolean bool) {
        return e -> bool;
      }
    }

    レビュー15個の需要の反復と進化を通じて、「直交設計」と「組合せ式設計」の基本思想を運用することによって、インタフェースが豊富で、表現力が極めて強いDSLを得た.
    この簡単なDSLセットは、Matcherの方法論とOOの思考を含み、全体的な設計は高度な一致性と統一性を維持する高度に多重化可能なFP集合である.
    に感謝
    「直交設計」の理論、原則、その方法論は前ThoughtWorksソフトウェア巨匠「袁英傑」さんから出た.英傑は私の先生であり、私の親友でもある.その高深莫測のソフトウェア設計の修成、およびソフトウェア設計に対する独特な哲学的思考方式は、私たちの後輩が勉強した模範である.