边界的概念
边界指类之间协作时衔接点的集合,跟空间上边界的概念类似。
类之间协作时的衔接分为数据与行为两种方式,虽然数据与行为最终都是由类对象携带、并没有本质的不同,只是在设计上,用于类协作时传递数据的数据类往往没有多少行为,而用于类协作时传递行为的行为类往往是接口,不能携带数据。
边界分为内边界和外边届,内边界是指类自身根据内需定义的接口、数据类,外边界是指接口的实现和数据类的初始化。一个类的内边界是对内需的定义,只有一处;而外边界是使用这个类时对内边界的实例化,可能存在很多处。
边界的概念不止在类协作中存在,在包间的协作、模块间的协作、架构的层之间协作等类集合之间的协作也都存在边界。实质上,任何形式的边界最终都是类的边界,类集合间的边界可能分散在若干个重要的类中,但是原则上,边界要尽可能集中在一个类中。
边界的作用
严格的执行定义类内边界的原则,能让类更加内聚,移植性高,并且可单独编译和测试。对外边界严格的管理会让类组合层次清晰可见,对象的构建和生命周期便于管理。
比如,MVP中的View和Model都定义自己的内边界,Persenter中实现两者的外边界。考虑到测试性,View可以实现一个另外的测试外边界,其中提供一些跟Model类似的测试数据。对Model的测试同理。
组件化、插件化、模块化、面向测试编程等设计上的优化,其前提条件都是软件整体拥有清晰完整的边界层次。想象一个场景,所有的业务模块都可以独立测试,所有视图都能不依赖网络数据单独进行UI调试,所有网络接口都可不依赖视图直接用Java代码进行批量、自动化的稳定性测试,是不是很爽?
类边界demo
public static class Teacher {
// Teacher类的内边界接口,内部通过此接口依赖外部的行为
interfaceTeacherCallback {
// Teacher类需要通过此方法获取课程表
StringgetSchedule();
}
// Teacher类的内边界数据类
publicstatic class TeacherData {
// 教师编号
privatefinal int teacherNum;
public TeacherData(int teacherNum) {
this.teacherNum =teacherNum;
}
}
privatefinal TeacherCallback teacherCallback;
private final TeacherData teacherData;
public Teacher(TeacherCallbackteacherCallback, TeacherData teacherData) {
this.teacherCallback = teacherCallback;
this.teacherData = teacherData;
}
publicvoid begin(){
String schedule = teacherCallback.getSchedule();
boolean hasClass = hasClass(teacherData.teacherNum, schedule);
if(hasClass){
goToClassRoom();
}
}
// 通过教师编号和课程表判断有没有自己的课
privateboolean hasClass(int teacherNum, String schedule) {
returntrue;
}
// 去教室
privatevoid goToClassRoom() {}
}
public static class School {
// 学校的一天
privatevoid oneDay(){
// Teacher类需要依赖行为接口的外边届,也就是实现接口
Teacher.TeacherCallbackcallback = new Teacher.TeacherCallback() {
@Override
public String getSchedule() {
return "周三有数学课";
}
};
//Teacher类需要依赖数据类的外边届,也就是数据类对象的初始化
Teacher.TeacherData data = new Teacher.TeacherData(88);
// 创建Teacher对象并开始
TeacheroneTeacher = new Teacher(callback, data);
oneTeacher.begin();
}
}
上图中Teacher类中定义了自己的数据和行为内边界,School类组合了Teacher类并持有其一种特定的外边界(外边界可能会有多个)。
面向边界编程
面向边界即面向协作。一般来说,我们做设计时的注意力更多在内聚上,也就是说要实现一个拥有特定职能的类或模块,完事儿再考虑协作的事儿;而面向边界编程是指,先分职能,然后做内边界设计,其次外边界设计,最后实现类的具体职能。
宏观的设计本身就是边界设计,如架构设计、模块设计等等。
面向边界的设计思路能避免一些不必要的设计坏味,比如我们经常在设计一个类的时候,发现有一些内需访问不到,那么就会让该类多持有一些本不应该可见的引用或对静态方法的访问。很多程序员因可用的静态方法太少而苦恼,然后花大力气打造出非常全面的各种静态类,从而达到新设计任何类时都无需考虑其内需不能得到满足的问题。问题得以解决,但设计坏味也随之产生。
广义的边界
边界本质上是协作的边界,因为类之间的协作也是通过类,所以以类或者接口作为边界最为常见,但也有一些其他的场景。
方法边界指将类的内需定义在一个方法中,常见的如抽象方法。还有一种有意思的方法边界,比如一个MyRecyclerView,其中有很多打印,这个打印既不能用系统打印方法,因为应用内要整体管理打印,又不能用应用内的打印方法,因为MyRecyclerView想当作通用控件发布到github上。怎么办?可以在MyRecyclerView类内部定义一个println方法,此方法转调用系统的log方法,这样任何想用MyRecyclerView的项目只需要改一行代码就行了。这个例子对边界概念的理解有很大帮助。
静态边界是指将类的内需定义在静态方法中,常见的如Router、EventBus等,都可以看作是依赖它们的类的静态边界。大多数情况下,静态边界是不好的,但这是一个很大的话题,这里不展开讨论,我会在后续的文章中专门讨论静态的问题。