角色接口
2006年12月22日
角色接口是通过查看供应商和消费者之间的特定交互来定义的。供应商组件通常会实现多个角色接口,每个接口对应于这些交互模式之一。这与头接口形成对比,在头接口中,供应商只有一个接口。
让我们用一个例子来解释。考虑一个用于PERT 风格项目计划的程序。在这个方案中,我们将项目分解成一组活动。然后,我们将这些活动排列成一个网络(严格来说是一个有向无环图)来显示任务之间的依赖关系。因此,如果“吃早餐”是一个任务,那么“煮咖啡”和“混合谷物”可能是前置活动。这意味着我必须等到所有前置活动完成后才能开始吃早餐。
每个活动都有一个持续时间,即我们预计它需要多长时间。有了这个持续时间,再加上网络中的关系,我们可以计算出其他信息。我们可以将活动的“最早开始时间”计算为其前置活动的“最新最早结束时间”。我们将活动的“最早结束时间”计算为其“最早开始时间”加上其持续时间。类似地,我们可以计算出“最新结束时间”和“最新开始时间”。代码看起来像这样。
private int duration; public MfDate earliestStart() { MfDate result = MfDate.PAST; for (?TYPE? p : predecessors()) if (p.earliestFinish().after(result)) result = p.earliestFinish(); return result; } public MfDate earliestFinish() { return earliestStart().addDays(duration); } public MfDate latestFinish() { MfDate result = MfDate.FUTURE; for (?TYPE? s : successors()) if (s.latestStart().before(result)) result = s.latestStart(); return result; } public MfDate latestStart() { return latestFinish().minusDays(duration); }
你会注意到上面的代码中有一个漏洞——?TYPE?
。如果我们询问一个活动的前置活动和后续活动,我们应该期望返回什么类型的对象?(准确地说,我们期望返回一个集合,所以真正的问题是返回集合的元素类型应该是什么?)
如果你使用头接口,返回的接口将是一个活动,并且会镜像我们活动类的公共方法来创建一个接口实现对。
public interface Activity ... MfDate earliestStart(); MfDate earliestFinish(); MfDate latestFinish(); MfDate latestStart(); class ActivityImpl... List<Activity> predecessors() ... List<Activity> successors() ...
然而,使用角色接口,我们查看协作对象是如何实际使用的。在这种情况下,后续活动仅用于其latestStart
,而前置活动仅用于其earliestFinish
。因此,我们创建了两个接口,它们只包含我们实际使用的那些方法。
public interface Successor { MfDate latestStart(); } public interface Predecessor { MfDate earliestFinish(); } class Activity List<Predecessor> predecessors() ... List<Successor> successors() ...
我们可以将后续活动视为协作对象相对于此对象所扮演的角色。这种关于对象及其在与他人协作中所扮演的角色的思考方式在面向对象的世界中有着悠久的历史。
角色接口的优势在于它清楚地传达了活动与其后续活动之间的实际协作。通常,一个类不会使用另一个类中的所有方法,因此显示哪些方法实际上是需要的很有用。这在您需要稍后替换它时尤其有用。头接口迫使您实现每个方法,即使您不需要它们;但使用角色接口,您只需要实现所需的方法。
角色接口的缺点是它需要更多精力来设计,因为您需要查看每个协作来形成角色接口。使用头接口,您只需要复制公共方法,无需思考。还有一种对消费者的依赖感。我之所以说“感觉”,是因为没有正式的依赖关系,但这足以让许多人感到不舒服。他们更喜欢头接口,因为他们认为你不应该关心谁使用你的服务,或者如何使用。你发布一个接口,如果他们觉得有用,他们就可以使用它。
总的来说,我更喜欢角色接口,因此我建议您尽可能地使用它们。这样做需要付出努力,但我一直相信,您应该只在真正需要可替换性时才使用接口,如果您确实需要接口,您应该认真思考接口的消费者需要什么。
如果您在使用 Web 服务等远程环境中考虑这一点,就会有一个有趣的转折。如果我们要向远程服务请求有关前置活动的信息,我们应该期望返回什么?有些人可能会争辩说,为了成为角色接口,它应该只返回包含最早结束时间数据的文档。我不同意——我认为它返回包含比我要求更多数据的文档是完全有效的。关键是,任何涉及的类型检查都应该只检查最早结束时间数据是否存在。如果可以忽略额外的數據,那么提供它就不是犯罪;就像一个类可以实现多个接口一样。这种想法与消费者驱动契约的理念相一致,这也是我认为消费者驱动契约如此引人注目的原因之一。
正如我所指出的,这个概念已经存在很长时间了。Trygve Reenskaug 编写了一本方法论书籍,该书基于分析角色并将它们合成到类中。Robert Martin 将此主题称为接口隔离原则:角色接口遵循该原则,而头接口则不遵循。