前言
我们潜意识里的一些编程习惯,或许是从大一学C语言开始的
计算机科学就是研究计算:如何表示和处理信息。学会思考,而不只是编程
从代码演化的角度理解继承和接口
我们以前理解继承和接口,都是先知道概念,然后解释这个有什么用。其实,更自然的路径应该是,碰到问题,发现需要这样的一个工具。
《重构 改善既有代码的设计》有一个“提炼接口”一节,中间提到:类之间彼此互用的方式有若干种。“使用一个类”通常意味着用到该类的所有责任区。另一种情况是,某一组客户只使用类责任区中的一个特定子集。再一种情况则是,这个类需要与所有协助处理某些特定请求的类合作。对于后两种情况,将真正用到的这部分责任分离出来通常很有意义,因为这样可以使系统的用法更清晰,同时也更容易看清系统的责任划分。如果新的类需要支持上述子集,也比较能够看清子集内有些什么东西。
《重构 改善既有代码的设计》有一个“梳理并分解继承体系”一节,中间提到:某个继承体系同时承担两项责任,建立两个继承体系,并通过委托关系让其中一个可以调用另一个。
《重构 改善既有代码的设计》有一章名为“处理概括关系”,包括
- 提炼子类、超类、接口
- 折叠继承体系
- 塑造模板函数
- 以委托取代继承
从这里就可以看到:
- 我们分析框架源码时,大量复杂的class/interface diagram等,或许并不是一开始就有的,而是慢慢抽取出来的。
- 类之所以implement 某个接口,或许并不是框架本来就有这个概念,而是与另外一个类作用时,只需部分操作,刚好为这几个操作取了一个有意义的名字。
抽取一个类
假设原来有一个比较复杂的类
class A{
void func(){
1.xx
2.xx
3.xx
4.xx
5.xx
}
}
现在我们代码重构,要将步骤234抽出一个类B来,类B需要A的数据初始化,类A需要类B的计算结果。一般有两种方案
class A{
void func(){
1.xx
2.B b = new B(xx); // b作为A的类成员跟这个差不多
3.xx = b.func();
4.xx
}
}
但一些框架经常
class A{
void func(){
1. xx
2. xx
}
}
class B{
void func(A a){
1. xx = a.getxx();
2. xx
3. a.setxx();
}
}
class Main{
main{
A a = new A();
B b = new B();
b.func(a);
}
}
比如spring ioc初始化的一段代码便是如此
// 定义配置文件
ClassPathResource res = new ClassPathResource(“beans.xml”);
// 创建bean工厂
DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 定义读取配置文件的类
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
// 加载文件中的信息到bean工厂中
reader.loadBeanDefinitions(res);
两种方式的不同在于:
- 前者只是将相关代码抽取为一个函数,然后到了另一个类里。(本质上只算是抽取了一个函数)
- 后者将相关代码完全抽出来,A类中不用保有任何痕迹,可以算是抽取出了一个类
代码和感觉的一致性
对于一个小程序,代码的功能跟它呈现给我们的感觉是一致的(有main方法,有初始化和close等),而使用了框架之后,就出现了我们写的代码和它实际的能力错位的情况。
框架分为两类
- 工具型或功能型框架,比如ibatis之类,封装了数据库操作。
- 容器型框架,使用这些框架,我们只是填几个方法,整个功能就实现了。就好比,唐代的诗人,写诗,从头到尾自己斟酌。宋代的诗人作词,曲牌韵律已定,填词就好了。它的主体,是一个叫“容器”的东西。而我们所写的类,只是其中的一个部件而已。比如quartz,它有一个调度器执行在另一个条“主线”上,不停的查询或执行下一个将要执行的任务。
除此之外,Spring与其它框架的结合,往往从代码上改变了框架的使用“感觉”。其实,spring本质是ioc(及其基础上的aop),spring为框架提供的“方便”主要是ioc提供的,包括bean的生成,生命周期的管理(比如quartz的scheduler随着ioc容器的启动而启动,shutdown而shutdown),并不会改变框架(所提供类的)的使用方式(即一些接口方法的调用)。
方案的重要性
笔者曾经写过一个程序,查询hbase数据,并将查询结果写入到本地文件中。
- 单次查询hbase的效率并不好,所以笔者先缓冲一部分,然后批量查询。
-
使用buffer批量查询后,带来两个问题
- 对结果的处理必须是批量的,即不再像以前一样,查询一个,处理一个。
- 如果是多线程程序(多线程向buffer中添加request),这个buffer需要增加线程安全保证。
- 仅仅批量是不够的,因此笔者使用了缓存,查询时,先到缓存中查询,如果没有命中,则查询hbase,并将查询结果放到缓存中。
- 因为用到了缓存、批量等,中间多了许多字符串判空操作(当然还有其它算是跟业务无关的判断操作,比如集合是否为空)。没错,即使一个“正常”的查询,也要经历这么多判断操作。
好了,其实我的需求很简单,查询数据,然后写入本地文件。但谁知道牵扯到了缓存和批量等问题。考虑到hbase中的数据虽然很多,但基本不会有很大的增长,笔者用了rocksdb,rocksdb本身具有索引和缓存等功能,现在这个世界清静了,查一个,写一个。
- 第一个方案,实现复杂,代码多,易出错,一次正确数据执行的代码较多(为了程序的健壮性,有些判断操作不可避免,业务无关代码过多。)
- 第二个方案,几乎在任何方面都比第一个方案有优势,当然,因为rocksdb的数据在本地存储,所以会占用较多的本地空间。
对象之间的关联关系
对象之间也有一对一、一对多以及多对多关系。类似于数据库表设计,你需要在一个表中新增关联字段(一对一、一对多),或者单独弄一个中间表(多对多)。反映在类与类的关系上,两个类之间建立单向连接(ioc的拿手好戏就是处理单向连接),随着时间推移,你可能发现被引用类需要得到其引用者以便进行某些处理。也就是说,它需要一个反向指针(双向引用关系)。
如果你经常编写一些controller-service-dao之类的代码,你会发现对类的双向关联非常生疏。