面向对象设计的关键,我认为是识别职责,封装合理的对象。缺乏合理的封装,就会缺少正确的领域对象,使得属于共同职责的领域信息散乱分布到系统的各个方法中,导致概念不够清晰,职责混乱,以及代码的重复。然而,如果没有正确地识别职责,又可能导致封装无从谈起,因为我们获得的需求可能是散乱的,既难以抽象概念,又缺乏层次。故而,职责与封装是相辅相成的概念。如下图所示:
我们不能简单地将封装当做是“信息隐藏”。所谓隐藏信息以及具体行为的细节,确实是封装的本质所在,但面向对象思想中的封装,其目的还在于对领域概念以及设计概念的识别。前者是对业务模型的抽象,如电子商务系统中的Product、Order、OrderItem等概念;后者是对设计模型的改进,如在设计模式中引入策略对象、命令对象,DDD中提倡的Repository、Factory对象。这也是为何许多设计者很容易理解封装的概念,但却始终无法做到合理封装的根本原因。
此时,我们需要运用职责驱动设计,通过对职责的识别来提炼这些概念。概念可以起到分类的作用,根据职责对行为与数据进行分类,找到其应该归属的对象,散乱的逻辑就会变得清晰起来。就好似我们对彩球按照颜色进行分类,并放置到不同的位置:
不同的颜色有分明不同的特征,只要不是色盲,分类自然水到渠成。现实中的系统需求自然不如颜色这般泾渭分明,要从纷繁复杂的混沌需求中超脱出来,最好的办法就是按照不同的角度或层次去寻找职责,并用最简单的语言一句话描述这些职责。根据职责的特征,我大致将职责分为三个层次,由外自内分别为:业务价值、业务功能与业务实现,如下图所示:
业务价值基本上体现了这个需求用例(或用户故事)存在的目的,即解释了需求的Why。简言之,只有提供了该职责,则此需求对于客户才是有价值的。这也符合用户故事INVEST原则中的V(Valuable)。没有价值的需求,自然是应该放弃的。故而在识别业务价值时,常常从用户的视角来分析,辨别。
业务价值是职责驱动的入口,因为在寻找到了业务价值之后,我们就可以剖析该价值需要哪些支撑功能(它可以解释需求的What),再由功能继续深入分析,找到实现功能的职责(它可以解释需求的How)。因此,这个模型是一个层层推进的过程。
职责驱动不仅仅可以从文字需求入手,同样可以针对已经实现的代码。甚至我们可以将这种职责驱动看做是一种阅读代码的技巧,通过寻找业务价值,进而分析业务功能和业务实现,对代码形成一个整体的感观,进而通过合理地分配职责改善原有代码。如下代码是《修改代码的艺术》一书中给出的例子:
import javax.mail.*;
import javax.mail.internet.*;
public class MailingListServer {
public static final String SUBJECT_MARKER = "[list]";
public static final String LOOP_HEADER = "X-LOOP";
public static void main(String[] args) {
if (args.length != 8) {
System.err.println("Usage: java MailingList <popHost> " +
"<smtpHost> <pop3user> <pop3password> <smtpuser> <smtppassword> <listname> " +
"<relayinterval");
}
return;
HostInformation host = new HostInformation(arg[0]);
String listAddress = args[6];
int interval = new Integer(args[7]).intValue();
Roster roster = null;
try {
roster = new FileRoster("roster.txt");
}catch(Exception e) {
}
try {
do {
try {
Properties properties = System.getProperties();
Session session = Session.getDefaultInstance(properties, null);
Store store = session.getStore("pop3");
store.connect(host.pop3Host, -1, host.pop3User, host.pop3Password);
Folder defaultFolder = store.getDefaultFolder();
if (defaultFolder == null) {
return;
}
Folder folder = defaultFolder.getFolder("INBOX");
if (folder == null) {
return;
}
folder.open(FOLDER.READ_WRITE);
process(host, listAddress, roster, session, store, folder);
}catch () {
}
try {
Thread.sleep(interval * 1000);
} catch() {
}
} while (true)
}catch () {
}
}
private static void process(
HostInformation host, String listAddress, Roster roster,
Session session, Store store, Folder folder) throws MessagingException {
try {
if (folder.getMessageCount() != 0) {
Message[] messages = folder.getMessages();
doMessage(host, listAddress, roster, session, folder, messages);
}
}catch () {
}finally {
folder.close(true);
store.close();
}
}
private static void doMessage(
HostInformation host,
String listAddress,
Roster roster,
Session session,
Folder folder,
Message[] messages) throws MessageingException {
FetchProfile fp = new FetchProfile();
fp.add(FetchProfile.Item.ENVELOPE);
fp.add(FetchProfile.Item.FLAGS);
fp.add("X-Mailer");
folder.fetch(messages, fp);
for (int i = 0; i < messages.length; i++) {
Message message = messages[i];
if (message.getFlags().contains(Flags.Flag.DELETED)) continue;
System.out.println("message received: " + message.getSubject());
if (!roster.constainsOneOf(message.getFrom())) continue;
MimeMessage forward = new MimeMessage(session);
Address[] fromAddress = message.getFrom();
InternetAddress from = null;
if (fromAddress != null && fromAdress.length > 0) {
from = new InternetAddress(fromAddress[0].toString());
}
forward.setFrom(from);
forward.setReplyTo(new Address[] {
new InternetAddress(listAddress)
});
forward.addRecipients(Message.RecipientType.BCC, roster.getAddresses());
String subject = message.getSubject();
if (-1 == message.getSubject().indexOf(SUBJECT_MARKER)) {
subject = SUBJECT_MARKER + " " + message.getSubject();
}
forward.setSubject(subject);
forward.setSentDate(message.getSentDate());
forward.addHeader(LOOP_HEADER, listAddress);
Object content = message.getContent();
if (content instanceof Multipart) {
forward.setContent((Multipart)content);
} else {
forward.setText((String)content);
}
Properties props = new Properties();
props.put("mail.smtp.host", host.smtpHost);
Session smtpSession = Session.getDefaultInstance(props, null);
Transport transport = smtpSession.getTransport("smtp");
transport.connect(host.smtpHost, host.smtpUser, host.smtpPassword);
transport.sendMessage(forward, roster.getAddresses());
message.setFlag(Flags.Flag.DELETED, true);
}
}
}
|
这段代码集中了所有劣质代码的特性,如可读性差,可测试性差,可重用性差,可扩展性差,也很难体现设计者的意图。
是的,意图!意图就是体现业务价值的关键所在,最好能够通过类的名称直接传达这种业务价值。显然,上述代码完全没有做到这一点。通过仔细阅读这段代码,然后从职责的角度入手,就驱使我们思考它究竟做了什么?显然,这段代码的功能就是侦听邮件服务器,并根据实现给定的邮件名单,将收到的邮件转发给邮件名单的相关人士。所以,它的业务价值就是转发邮件。
要做到转发邮件,基本的功能是要能够侦听邮件服务器。我们可以将侦听看做是业务功能。通常,对于实际的需求而言,职责模型并非简单的三层结构,它可能是一种递归的方式,也即可以对业务价值、业务功能甚至业务实现进行不断深入的分解。例如,对于这里的侦听业务功能而言,还可以分解为下一个业务功能,即接收邮件。同时,要实现邮件的转发,还需要发送邮件的支撑。下面就是我们所能够识别出来的职责:
* 转发邮件
** 侦听邮件
*** 接收邮件
** 发送邮件
面对如此简短的职责描述,再要识别对象所要封装的概念就变得非常容易了。下图是根据识别出来的职责获得的类图结构:
比较这个设计模型与之前的代码,新的模型无疑在职责分配上占了胜场。并且,每个对象的名称都很好地传达了它所能完成的功能。由于封装将职责的实现细节有效地隐藏,并为它们各自划分了空间,形成各自的职责单元,然后再以弱耦合的形式进行协作。因而,我们可以很容易地对它们进行单元测试,或者对参与协作的行为进行Mock。接口的引入则是为了未来的功能扩展,例如我们不再使用javax.mail库来实现邮件收发,就可以提供不同的实现类。可独立封装的MessageReceiver与MessageSender还可以为系统的其他模块所重用。职责与封装相得益彰,有效地改善了整个设计模型。