高性能聊天系统
作者:板桥banq
1.4.5 重整Refactoring
现在,在Socket I/O的读写部分已经有不少语句,需要完成几个功能:从Queue中取出或放入;协议封装;获得特定大小的byte数组。最后一个功能特别说明一下:设定byte数组需要初始化,因此在未读取数据之前只能规定好数组的大小。但是如果数据包很大,可能需要调整初始化参数,这个调整为以后可能修改Socket I/O部分留下隐患,这是面向对象程序设计的一个忌讳。
在目前的考虑范围已经预计到将来可能需要修改这部分代码,而设计者的初衷是不想这部分代码随便被修改,希望被完整封装,因为Socket I/O部分是整个系统的核心,不希望没有专门相关非堵塞I/O知识的程序员再深入Socket I/O进行修改。
在这种情况下,就开始要考虑重整(Refactoring),消灭这个隐患。决定重整之前,如果还有其他因素促进重整就更好了,Socket I/O读写部分功能有两个,整个Socket底层有4处代码:UDPClient、服务器端的UDPHandler、TCPCLient和服务器端的TCPHandler,每个类中有两次Socket读写操作,Socket读写操作代码发生重复两次以上。
当一种现象重复3次以上时,就需要使用重整了。
重整是在不改变代码功能的情况下,提高代码质量的一种手段。在一般编程中,程序员和项目经理更多的是看重功能如何迅速地被完成,而对代码质量则不重视,其实,代码质量对有效率地快速完成新的功能是有重要意义的,正所谓“磨刀不误砍柴工”,在系统开发初期,只是一味地增加新功能,不实现代码重整,这种弊病可能看不出来,但是系统达到一定规模,矛盾就开始出现甚至激化。
客户抱怨自己的新功能无法快速完成,甚至拖延很长的时间,但是在完成后,又影响了其他已经成熟运行的功能,客户不得不再去测试以前的程序功能,疲于奔命,后悔上了一条“不归路”;而程序员也在抱怨诉苦:系统增加修改功能很困难,原来很多功能代码纠缠在一起,要分开它们已经很困难,不用说在其之间增加新的功能。
这些问题都是由于没有经常实现重整造成的,重整和新功能开发如同两条腿走路,特别是新功能开发,是促使重整的最好前提,只有将旧代码重整理顺,增加新功能才会更迅速,因此重整和新功能增加在一个系统开发中是轮流交替进行的。
现在,开始对Socket I/O部分实现重整,将这些重复的Socket读写代码集中到一个类中,这样将与Socket无关的代码全部从Socket底层分离出来。
建立一个SocketDataHandler,主要封装Socket I/O读写方面的功能,如下(程序1-10):
程序1-10
/**
* 本类是供Socket I/O调用,以实现信号的发送和收取
* 是MessageQueue与Socket I/O的接口部分
* <p>Copyright: Jdon.com Copyright (c) 2003</p>
* <p>Company: 上海极道计算机技术有限公司</p>
* @author banq
* @version 1.0
*/
public class SocketDataHandler {
private final static String module = SocketDataHandler.class.getName();
//包装工厂
private final static WrapFactory wrapFactory = WrapFactory.getInstance();
//队列Queue
private final static MessageQueue messageQueue = MessageQueue.getInstance();
private final static int DATA_LENGTH = 1024;
private int queueType;
public SocketDataHandler(){}
//获得特定大小的byte数组,以接受数据,可根据应用调整
public byte[] getByte() {
return new byte[DATA_LENGTH];
}
/**
* 从Queue中取出Request 并实行协议包装
*/
public byte[] sendRequest() {
byte[] request = null;
try {
ByteArrayOutputStream outByte = (ByteArrayOutputStream) messageQueue
removeReqFirst();
request = wrapFactory.getRequest(outByte.toByteArray());
} catch (Exception ex) {
Debug.logError("sendRequest() error:" + ex, module);
}
return request;
}
/**
* 将Request去除协议解码,正文内容保存到Queue中
*/
public void receiveRequest(byte[] array) {
try {
byte[] content = wrapFactory.getContentFromRequest(array);
InputStream bin = new ByteArrayInputStream(content);
messageQueue.pushRequest(bin);
} catch (Exception ex) {
Debug.logError("sendRequest() error:" + ex, module);
}
}
/**
* 从Queue中取出Response 并实行协议包装
*/
public byte[] sendResponse() {
byte[] response = null;
try {
ByteArrayOutputStream bout = (ByteArrayOutputStream) messageQueue.
removeResFirst();
response = wrapFactory.getResponse(bout.toByteArray());
} catch (Exception ex) {
Debug.logError("sendRequest() error:" + ex, module);
}
return response;
}
/**
* 将Response去除协议解码,正文内容保存到Queue中
* @param array
*/
public void receiveResponse(byte[] array) {
try {
MessageQueue messageQueue = queueFactory.getQueue(queueType);
byte[] response = wrapFactory.getContentFromResponse(array);
InputStream bin = new ByteArrayInputStream(response);
messageQueue.pushResponse(bin);
} catch (Exception ex) {
Debug.logError("sendRequest() error:" + ex, module);
}
}
}
在这个SocketDataHandler中集中了Socket读写的4种方法,这样可以被客户端和服务器两个方面使用。以UDPClient为例子,它的doKey部分代码重整为如下:
private void doKey(SelectionKey key) throws Exception {
DatagramChannel keyChannel = null;
try {
keyChannel = (DatagramChannel) key.channel();
if (key.isReadable()) { //如果可以从服务器读取response数据
Debug.logVerbose("get response from the Server", module);
//可以在不更改本类代码情况下,更改byte数组大小
byte[] array = socketDataHandler.getByte();
ByteBuffer buffer = ByteBuffer.wrap(array);
keyChannel.receive(buffer);
//精简为一条语句
socketDataHandler.receiveResponse(array);
key.interestOps(SelectionKey.OP_WRITE);
selector.wakeup();
} else if (key.isWritable()) { //如果可以向服务器发送request数据
Debug.logVerbose("-->begin to send request", module);
//精简为一条语句
byte[] request = socketDataHandler.sendRequest();
ByteBuffer buffer = ByteBuffer.wrap(request);
keyChannel.write(buffer);
key.interestOps(SelectionKey.OP_READ);
selector.wakeup();
}
} catch (Exception e) {
Debug.logError("run error:" + e, module);
close(keyChannel);
throw new Exception(e);
}
}
这样,通过SocketDataHandler的屏蔽,使得外界不必直接在Socket I/O读写部分改动代码,Socket底层部分变得更加稳定和健壮。
这样重整后,为增加新功能提供了方便。例如,因为Socket底层有两种协议实现TCP和UDP,而MessageQueue是负责传送数据给Socket的,那么MessageQueue对象也需要两种,分别对应两种Socket连接。
原来设计开发时,使用了单态模式保证全局只有一个MessageQueue对象,这与功能设计是矛盾的。因此,必须使得一个JVM中有两个MessageQueue对象,使用工厂方法模式可以创建一个专门生产Queue产品的Queue工厂,代码如下:
public class QueueFactory {
//TCP队列Queue
private final static MessageQueue tcp_queue = new MessageQueue();
//UDP队列Queue
private final static MessageQueue udp_queue = new MessageQueue();
public final static int TCP_QUEUE = 1;
public final static int UDP_QUEUE = 2;
private final static QueueFactory factory = new QueueFactory();
public static QueueFactory getInstance(){
return factory;
}
//工厂方法
public MessageQueue getQueue(int queueType){
if (queueType == TCP_QUEUE)
return tcp_queue;
else
return udp_queue;
}
}
在GOF的《设计模式》中关于工厂方法的定义如下:定义一个用户创建对象的接口,让子类决定实例化哪个类,在QueueFactory中使用Java的抽象类实现了接口和子类的合二为一。
QueueFactory是获得不同的队列Queue,使用修改SocketDataHandler一个类就可以了,修改SocketDataHandler如下:
public class SocketDataHandler {
//使用QueueFactory
private final static QueueFactory queueFactory = QueueFactory.getInstance();
//更改构造方法,加入Queue类型参数,由外界决定使用Queue的类型
public SocketDataHandler(int queueType){
this.queueType = queueType;
}
…
}
这样,通过少许变动就将整个接口层拓展为支持两种Socket同时运行的系统了,如果没有之前Socket I/O的读写重整,那么这段代码增加将在4个类中进行:UDPClient、UDPHandler、TCPClient和TCPHandler,不但琐碎麻烦,还可能在简单重复编码中出现人为的失误。
下页
first