摘要我们在设计系统接口时经常会遇到这样的问题 我们的接口应该提供多少方法才合适? 我们的接口应该提供原子方法还是复合方法? 我们的接口是否应该封装(或者能否封装)所有的细节? 接口的设计需要考虑用户的使用习惯使用的方便程度使用的安全程度根据我的编程经验下面会详细讨论接口设计的个需要权衡的方面接口的单一化 & 复合化 接口 接口提供了不同系统之间或者系统不同组件之间的界定在软件中接口提供了一个屏障从而从实现中分离目标从具体中分离抽象从作者中分离用户 站在用户的角度看一个接口建立并命名了一个目标对象的使用方法一些约束(例如编译时的类型系统运行时的异常机制及返回值)使得类作者的目的得以体现和加强供给(affordances)指事物的被感知的真实的属性这些属性可以决定事物使用的可能方法供给提供了对事物操作的线索 类设计者的一个职责便是在接口中减小约束与供给之间的隔阂匹配目标以及一定程度上的自由度尽可能减小错误使用目标对象的可能 封装 对于封装来说远不止数据私有那么简单在设计中封装往往会涉及到自我包含(selfcontainment)如果一个类需要你知道如何调用它方法(eg 在一个线程的环境中在一个方法调用后调用另一个方法你必须明确地同步对象)那么它的封装性就不如将所有这些全部包含并隐藏的类(eg 这个类是threadsafe的)好前一个设计存在着设计的漏洞它的许多限定条件是模糊的而且把部分责任推给了用户而不是让类提供者做这些工作来完成类的设计 在空间或者时间上分离方法的执行(例如线程远程方法调用消息队列)能够对设计的正确性和效率产生意义深远的影响这种分离带来的结果是不可忽视的 并发引入了不确定性和环境(context)选择的开销 分布引入了回调的开销这些开销可能不断增加而且会导致错误 这些是设计的问题修改它们可不是象修改bug那样简单 如果一个接口主要由存取方法(set和get方法)组成每个方法都相应的直接指向某个私有域那么它的封装性会很差接口中的域存取方法通常是不会提供信息的他们在对象的使用中不能通讯简单化和抽象化这通常会导致代码冗长并且容易出错 所以我们首先考虑接口设计的第一个原则 命令与查询分离(CommandQuery Separation) 要求保证一个方法不是命令(Command)就是查询(Query) 定义 查询当一个方法返回一个值来回应一个问题的时候它就具有查询的性质 命令当一个方法要改变对象的状态的时候它就具有命令的性质 通常一个方法可能是纯的Command模式或者是纯的Query模式或者是两者的混合体在设计接口时如果可能应该尽量使接口单一化保证方法的行为严格的是命令或者是查询这样查询方法不会改变对象的状态没有副作用(side effects)而会改变对象的状态的方法不可能有返回值也就是说如果我们要问一个问题那么就不应该影响到它的答案实际应用要视具体情况而定语义的清晰性和使用的简单性之间需要权衡 例如在javautilIterator中hasNext可以被看作一种查询remove是一种命令next合并了命令和查询 public interface Iterator{ boolean hasNext(); Object next(); void remove(); } 这里如果不将一个Iterator对象的当前值向前到下一个的话就不能够查询一个Iterator对象如果没有提供一个复合方法next我们将需要定义一系列的命令方法例如初始化(initialization)继续(continuation)访问(access)和前进(advance)它们虽然清晰定义了每个动作但是客户代码过于复杂 for(initialization; continuation condition; advance){ access for use } 将Command和Query功能合并入一个方法方便了客户的使用但是降低了清晰性而且可能不便于基于断言的程序设计并且需要一个变量来保存查询结果 Iterator iterator = erator(); while(iteratorhasNext();){ Object current = iteratornext(); use current } 下面我们考虑接口设计的第二个原则 组合方法(Combined Method) 组合方法经常在线程和分布环境中使用来保证正确性并改善效率 一些接口提供大量的方法起初这些方法看来是最小化的而且相关性强然而在使用的过程中一些接口显现得过于原始它们过于简单化从而迫使类用户用更多的工作来实现普通的任务并且方法之间的先后顺序及依赖性比较强(即暂时耦合)这导致了代码重复而且非常麻烦和容易出错 一些需要同时执行成功的方法在多线程异常和分布的情况下会遇到麻烦如果两个动作需要同时执行它们由两个独立的方法进行描述必须都完全成功的执行否则会导致所有动作的回滚 线程的引入使这种不确定性大大增加一系列方法同时调用一个易变的(mutable)对象如果这个对象在线程之间共享即使我们假设单独的方法是线程安全的也无法确保结果是意料之中的看下面对Event Source的接口它允许安置句柄和对事件的查询 interface EventSource{ Handler getHandler(Event event); void installHandler(Event event Handler newHandler); } 线程之间的交叉调用可能会引起意想不到的结果假设source域引用一个线程共享的对象对象很可能在之间被另一个线程安装了一个新的句柄 class EventSourceExample{ public void example(Event event Handler newHandler){ oldHandler = eventSourcegetHandler(event); // //对象很可能在这里被另一个线程安装了一个新的句柄 eventSourceinstallHandler(event newHandler); // } private EventSource eventSource; private Handler oldHandler; } 为了解决问题也需要由类的使用者而不是类的设计者来完成 class EventSourceExample{ public void example(Event event Handler newHandler){ synchronized(eventSource){ oldHandler = eventSourcegetHandler(event); eventSourceinstallHandler(event newHandler); } } private EventSource eventSource; private Handler oldHandler; } 我们假设目标对象eventSource是远程的执行每一个方法体的时间和通讯的延迟相比是很短的在这个例子中eventSource的方法被调用了两次并可能在其他的实例中重复多次因而开销也是至少两倍 此外还有一个问题是对外部的synchronized同步块的使用需求对synchronized块的使用之所以会失败主要因为我们通过代理对象来完成工作所以调用者的synchronized块同步的是代理对象而不是最终的目标对象调用者不可能对其行为做太多的保证 Combined Method必须在分布的环境或者线程环境中同时执行它反映了用户直接的应用恢复策略和一些笨拙的方法被封装到Combined Method中并简化了接口减少了接口中不需要的累赘Combined Method的效果是支持一种更像事务处理风格的设计 在一个组合的CommandQuery中提供一个单独的Query方法通常是合理的提供分离的Command方法是不太常见的因为Combined Method可以完成这一工作只要调用者简单的忽略返回结果如果返回一个结果招致一个开销的话才可能会提供一个单独的Command方法 回到前一个例子中如果installHandler method返回上一次安装的句柄则设计变得更加简单和独立 interface EventSource{ Handler installHandler(Event event Handler newHandler); } 客户代码如下 class EventSourceExample{ public void example(Event event Handler newHandler){ oldHandler = eventSourceinstallHandler(event newHandler); } private EventSource eventSource; private Handler oldHandler; } 这样我们给调用者提供了一个更加安全的接口并且不再需要他们解决线程的问题从而降低了风险和代码量将类设计的职责全部给了类设计者而不是推给用户即使有代理对象的出现也不会影响到正确性 一个Combined Method可以是许多Query的集合许多Command的集合或者两者兼有这样它可能补充CommandQuery方法也可能与之相抵触当沖突发生的时候优先选择Combined Method会产生一个不同的正确性和适用性 在另一个例子中我们考虑获得资源的情况假设在下面的接口中方法acquire在资源可用前阻塞 interface Resource{ boolean isAcquired(); void acquire(); void release(); } 类似于下面的代码会在一个线程系统中推荐使用 class ResourceExample{ public void example(){ bool |