摘要:为了消除重复,可以将查找算法与比较准则这两个变化方向进行分离。此刻,查找算法的方法名也应该被重命名,使其保持在同一个抽象层次上。结构性重复和存在结构型重复,需要进一步消除重复。
Refactoring to DSL
软件设计的目标OO makes code understandable by encapsulating moving parting, but FP makes code understandable by minimizing moving parts. -Michael Feathers
实现功能
易于重用
易于理解
没有冗余
正交设计软件设计是一个「守破离」的过程。 -- 袁英杰
消除重复
分离变化方向
缩小依赖范围
向稳定的方向依赖
实战快速实现需求1: 存在一个学生的列表,查找一个年龄等于18岁的学生
public static Student findByAge(Student[] students) { for (int i=0; i上述实现存在很多设计的「坏味道」:
缺乏弹性参数类型:只支持数组类型,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());结构性重复AgePredicate和NamePredicate存在「结构型重复」,需要进一步消除重复。经分析两个类的存在无非是为了实现「闭包」的能力,可以使用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(Iterablestudents, 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(Iterableteachers, 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/TeacherPredicate, find(Iterable
)/find(Iterable 的重复,为此引入「类型参数化」的设计。) 首先消除StudentPredicate和TeacherPredicate的重复设计。
public interface Predicate{ boolean test(E e); } 再对find进行类型参数化设计。
public static型变E find(Iterable c, Predicate p) { for (E e : c) if (p.test(e)) return e; return null; } 但find的类型参数缺乏「型变」的能力,为此引入「型变」能力的支持,接口更加具有可复用性。
public static复用lambdaE find(Iterable extends E> c, Predicate super E> p) { for (E e : c) if (p.test(e)) return e; return null; } Parameterize all the things.
观察如下两个测试用例,如果做到极致,可认为两个lambda表达式也是重复的。从「分离变化的方向」的角度分析,此lambda表达式承载的「比较算法」与「参数配置」两个职责,应该对其进行分离。
assertThat(find(students, s -> s.getName().equals("Horance")), notNullValue()); assertThat(find(students, s -> s.getName().equals("Tomas")), notNullValue());可以通过「Static Factory Method」生产lambda表达式,将比较算法封装起来;而配置参数通过引入「参数化」设计,将「逻辑」与「配置」分离,从而达到最大化的代码复用。
public final class StudentPredicates { private StudentPredicates() { } public static Predicateage(int age) { return s -> s.getAge() == age; } public static Predicate 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 Method」,但这样的设计严重违反了「OCP」(开放封闭)原则。
public final class StudentPredicates { ...... public static PredicateageEq(int age) { return s -> s.getAge() == age; } public static Predicate ageNe(int age) { return s -> s.getAge() != age; } } 从需求看,比较准则增加了众多的语义,再次运用「分离变化方向」的原则,可发现存在两类运算的规则:
比较运算:==, !=
逻辑运算:&&, ||
比较语义先处理比较运算的变化方向,为此建立一个Matcher的抽象:
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); } } Composition everywhere.
此刻,age的设计运用了「函数式」的思维,其行为表现为「高阶函数」的特性,通过函数的「组合式设计」完成功能的自由拼装组合,简单、直接、漂亮。
public final class StudentPredicates { ...... public static Predicateage(Matcher m) { return s -> m.matches(s.getAge()); } } 查找年龄不等于18岁的学生,可以如此描述。
assertThat(find(students, age(ne(18))), notNullValue());逻辑语义为了使得逻辑「谓词」变得更加人性化,可以引入「流式接口」的「DSL」设计,增强表达力。
public interface Predicate{ boolean test(E e); default Predicate and(Predicate super E> other) { return e -> test(e) && other.test(e); } } 查找年龄不等于18岁的女生,可以表述为:
assertThat(find(students, age(ne(18)).and(Student::female)), notNullValue());重复再现仔细的读者可能已经发现了,Student和Teacher两个类也存在「结构型重复」的问题。
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; }级联反应Student与Teacher的结构性重复,导致StudentPredicates与TeacherPredicates也存在「结构性重复」。
public final class StudentPredicates { ...... public static Predicateage(Matcher m) { return s -> m.matches(s.getAge()); } } public final class TeacherPredicates { ...... public static Predicateage(Matcher m) { return t -> m.matches(t.getAge()); } } 为此需要进一步消除重复。
提取基类第一个直觉,通过「提取基类」的重构方法,消除Student和Teacher的重复设计。
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; }从而实现了进一步消除了Student和Teacher之间的重复设计。
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); } }类型界定此时,可以通过引入「类型界定」的泛型设计,使得StudentPredicates与TeacherPredicates合二为一,进一步消除重复设计。
public final class HumanPredicates { ...... public static消灭继承关系Predicate age(Matcher m) { return s -> m.matches(s.getAge()); } } Student和Teacher依然存在「结构型重复」的问题,可以通过Static Factory Method的设计方法,并让Human的构造函数「私有化」,删除Student和Teacher两个子类,彻底消除两者之间的「重复设计」。
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绝不返回nullage(Matcher m) { return s -> m.matches(s.getAge()); } } Billion-Dollar Mistake
在最开始,我们遗留了一个问题:find返回了null。用户调用返回null的接口时,常常忘记null的检查,导致在运行时发生NullPointerException异常。
按照「向稳定的方向依赖」的原则,find的返回值应该设计为Optional
,使用「类型系统」的特长,取得如下方面的优势: 显式地表达了不存在的语义;
编译时保证错误的发生;
import java.util.Optional; 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(); } 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 Matcherstarts(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 Matcherstarts(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(); } } starts与starts_ignoring_case之间存在微妙的重复设计,为此需要进一步消除重复。
组合式设计assertThat(find(students, name(ignoring_case(Matchers::starts, "Horance"))).isPresent(), is(true));运用函数的「组合式设计」,达到代码的最大可复用性。从OO的角度看,ignoring_case是对starts, ends, contains的功能增强,是一种典型的「修饰」关系。
public static Matcherignoring_case( Function > m, String substr) { return str -> m.apply(lower(substr)).matches(lower(str)); } 其中,Function
> 是一个一元函数,参数为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_case也应该重构为private,变为一个「可重用」的函数。
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 Matchernot_contains(String substr) { return str -> !str.contains(substr); } } 在这之前,也曾遇到过类似的「反义」的操作。例如,查找年龄不等于18岁的学生,可以如此描述。
assertThat(find(students, age(ne(18))).isPresent(), is(true));public final class Matchers { ...... public staticMatcher 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语法糖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; }; } } allof与anyof之间的实现存在重复设计,可以通过提取函数消除重复。
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软件大师「袁英杰」先生。英杰既是我的老师,也是我的挚友;其高深莫测的软件设计的修为,及其对软件设计独特的哲学思维方式,是我等后辈学习的楷模。
思考软件设计的本质是什么?
OO与FP的本质区别是什么?
组合式设计的精髓是什么?
文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。
转载请注明本文地址:https://www.ucloud.cn/yun/65596.html
摘要:例如中的操作数据库时,先取得数据库的连接,操作数据后确保释放连接当操作文件时,先打开文件流,操作文件后确保关闭文件流。例如遍历文件中所有行,并替换制定模式为其他的字符串。使用实现行为的参数化。 OO makes code understandable by encapsulating moving parting, but FP makes code understandable by...
摘要:获取试读文章高阶函数高阶函数就是参数为可以为,并且返回值也可为的函数。比方说,我们现在有顾客名单,但我们需要得到他们的邮箱地址我们现在不用高阶函数用一个来实现它,代码如下。 《Refactoring To Collection》 本文是翻译Adam Wathan 的《Collection To Refactoring》的试读篇章,这篇文章内容不多,但是可以为我们Laraver使用者能更...
摘要:注本文是翻译写的关于调试技巧,读完以后很实用,分享给大家阅读过程中,翻译有错误的希望大家指正原文链接最近我一直在使用的,如果你还不了解,我简单说下一个集合就是一个功能强大的数组有很多强大处理其内部数据的函数但是唯一让我头疼的地方是如何调试的 注:本文是翻译Freek Van der Herten写的关于Collection调试技巧,,读完以后很实用,分享给大家.阅读过程中,翻译有错误的...
摘要:配置安装插件安装包用于代码的格式化将的安装路径添加到环境变量中。生成文件,编辑调试将文件中的改为,这样无断点时按或,直接运行程序不调试。方法提取选中要提取的表达式,,输入或选择命令。可对选项设置快捷键。 VSCode Python 配置 安装 Python 插件:ext install python 安装包(用于 Python 代码的格式化): pip install pep8 ...
阅读 636·2021-11-23 09:51
阅读 3581·2021-11-15 11:38
阅读 903·2021-10-14 09:42
阅读 3114·2021-09-29 09:35
阅读 2073·2021-09-03 10:33
阅读 749·2021-07-30 16:33
阅读 1541·2019-08-30 15:55
阅读 1827·2019-08-30 14:04