另载于 http://www.qingjingjie.com/blogs/9
现代对象设计主张“组合优于继承”。总之无论组合还是继承,对象都成了涉及多个类的复合结构。
“对象的有效范围”,是指对象从创建到丢弃(不再引用)的这段时间,不包括等待被GC销毁的时间。可以近似认为是对象的生命期。
单例对象(Singleton)的有效范围几乎是整个应用的开启时间,Socket的有效范围通常是网络连接的持续时间,而一个临时的Integer则可能瞬间就被丢弃了。Let's 注意,不同范围的对象/类,不能随意地组合/继承在一起。
1. 不同范围的对象避免打包在一起
(代码有点多,如果嫌烦可以跳过1.先看2.)
反面教材:我们来看一个客户端程序,它通过socket与某个服务器保持通信,不断发消息并收取响应。
public class Communication {
private Topic topic;
private Socket socket;
public Communication(String host, int port) {
socket = new Socket(host, port);
}
public void close() {
socket.close();
}
public void setTopic(Topic topic) {
this.topic = topic;
}
public String sendReceive(String msg) {
return sendRecv_(topic, msg);
}
private String sendRecv_(Topic topic, String msg) {
... // 具体处理
}
}
我们有两个主题(topic) A和B,以不同主题发的消息,服务器会做不同处理。我们一会儿用主题A发消息,一会用主题B发消息。代码如下:
Communication comm = new Communication(host, port);
comm.setTopic(A);
comm.sendReceive("Hello!");
comm.sendReceive("How are you?");
comm.setTopic(B);
comm.sendReceive("Good morning!");
comm.sendReceive("Let's begin");
comm.sendTopic(A);
comm.sendReceive("How old are you?");
切换来切换去,真麻烦。如果你不嫌麻烦,假设给Communication再加一个域"config",平均每发100条消息,要切换一次config,平均每发10条消息,要切换一次topic,还是交替进行,烦不烦!
再想想,如果多个线程在使用comm对象呢? 呵呵呵,完蛋了。
Communication的有效范围与socket一致,而topic的有效范围就小于socket了,因此topic就不该放在这个类里。虽然sendReceive()可以少填一个参数,看似方便,但是引发了更多麻烦。
对于继承也是同理,父类和子类应当有相同的有效范围。
所以还是这么写吧:
comm.sendReceive(A, "Hello!");
comm.sendReceive(A, "How are you?");
comm.sendReceive(B, "Good morning!");
稍微有点麻烦呢
或者这么写:
class CommByTopic {
private Communication comm;
private Topic topic;
// 构造函数省略
public String sendReceive(String msg) {
return comm.sendReceive(topic, msg);
}
}
CommByTopic onA = new CommByTopic(comm, A);
onA.sendReceive(msg);
onB.sendReceive(msg);
缺点是comm关闭后要注意不能继续使用onA。所以不要长时间持有onA对象,最好能局限在方法作用域内。
或者试试简洁的lamda~
Lamda in Java 8:
Function<String, String> onA = msg -> comm.sendReceive(A, msg);
onA.apply("Hello!");
onA.apply("How are you?");
Lamda in Scala:
val onA: String => String = comm.sendReceive(A, _)
onA("Hello!")
onA("How are you?")
// 柯里化的写法 val onA = comm.sendReceive(A) _
2. 大范围对象不要持有小范围对象
上面说的comm就是大范围对象,socket也是大范围对象,topic是小范围对象。它们生命长短不同。
如果大范围对象持有了小范围对象,你就要疲于切换,甚至担心线程安全性。反过来,小范围对象持有大范围对象,就好了。当然了,持有相同范围的对象也是好的。
对运行于IoC容器的程序尤其明显。来溜一段基于Spring MVC的应用代码:
@Component
@Scope("singleton") //单例对象
public class Manager {
@Autowired
private Account account;
public void freezeAccount() {
account.freeze();
merge(account);
}
}
@Component
@Scope("request") //request范围的对象
public class Account {
...
}
这样的代码在系统启动时就崩了,因为account还没出现。就算你给Manager标上@Lazy (延迟初始化),让它在账户A发来请求时才初始化,它也只能正确处理这次的请求。下次账户B再来请求时,它还是使用上次的A的account来操作,而不会用B的account。呵呵呵,完蛋了。
同理,session级的对象不要持有request级的对象。
对于Servlet和Filter也是如此,它们是近似于单例的对象,让它们持有一些配置数据和常量就行了,如果让它们持有当前的userId,也很危险。
再提醒一下,其实小范围对象持有大范围对象也不要滥用,一不小心就会让对象承担过多职责,有过多依赖。设计要从职责出发。
结语
之所以要从“有效范围”的角度谈对象设计的问题,就是想给大家提供一个明确可操作的分析视角,这可比“设计哲学”容易多了。
不过光会这个还不够,知识要全面。