|
Interfaces for software elements take two forms, what I call role
interface and HeaderInterface. A role interface is defined by
looking at a specific interaction between suppliers and consumers. A
supplier component will usually implement several role interfaces,
one for each of these patterns of interaction. This contrasts to a
header interface, where the supplier will only have a single interface. Let's look at this with an example. Consider a program for PERT style project
planning. In this scheme we break down a project into a set of
activities. We then arrange these activities into a network (strictly
a directed
acyclic graph) to show the dependencies between the tasks. So if
'have breakfast' is a task, then 'make coffee' and 'mix cereal' might
be predecessor activities. This means I cannot begin to have my
breakfast until all the predecessors have completed. Each activity has a duration, how long we expect it to take. With
that duration, together with the relationships in the network, we
can figure out other information. We can calculate the earliest
start of an activity as the latest earliest finish of its
predecessors. We calculate the earliest finish of an activity as its
earliest start plus its duration. Similarly we can work out the
latest finish and latest start. The code would look something like
this.
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);
}
You'll notice a hole in that code above - the ?TYPE?s. If we
ask an activity for its predecessors and successors, what type of objects
should we expect back? (To be precise we expect to return a
collection, so the real question is what should the type of the elements
of the returned collection be?) If you use a header interface, the returned interface would be an
activity and would mirror the public methods of our activity class to
create an InterfaceImplementationPair.
public interface Activity ...
MfDate earliestStart();
MfDate earliestFinish();
MfDate latestFinish();
MfDate latestStart();
class ActivityImpl...
List<Activity> predecessors() ...
List<Activity> successors() ...
With a role interface, however, we look at how the collaborating
objects are actually used. In this case a successor is only used for
its latestStart and a predecessor is only used for its
earliestFinish. So as a result we create two interfaces which only
have the methods we actually use.
public interface Successor {
MfDate latestStart();
}
public interface Predecessor {
MfDate earliestFinish();
}
class Activity
List<Predecessor> predecessors() ...
List<Successor> successors() ...
We can think of a successor as a role that a collaborating object
plays with respect to this object. This approach of thinking about
objects and the roles that they play in collaborating with others
has a long history in the object-oriented world. The strength of a role interface is that it clearly communicates
the actual collaboration between an activity and its successors. Often a
class doesn't use all the methods of a class, so it's good to show which
ones are actually needed. This can be particularly useful if you
need to substitute it later on. A header interface forces you to
implement every method, even if you're not going to need them; but
with a role interface you only need to implement exactly what's needed. The disadvantage of a role interface is that it's more effort to
come up with, since you need to look at each collaboration to form
the role interface. With a header interface all you have to do is
duplicate the public methods, no thought needed. On the whole I much prefer role interfaces, so I suggest pushing
towards them as much as you can. There is work involved in doing it,
but my belief has always been that you should only use interfaces
when you really need substitutability, and if you do need
interfaces you should think hard about what the consumer of that
interface needs. There is an interesting twist if you think of this in
a remoting context using something like web services. If we were to
ask a remote service for details on predecessors, what should we
expect back? Some might argue that for it to be a role interface, it
should only return a document with earliest finish data. I would
disagree - I think it's perfectly valid for it to return a document
with more data than I asked for. The point is that any type checking
involved should only check that the earliest finish data is present. If
extra data can be ignored, then it's no crime to supply it; just as a
class may implement multiple interfaces. This thinking matches the philosophy
of Consumer
Driven Contracts, which is one of the reasons I find consumer
driven contracts so compelling. As I've indicated, this notion has been around for a long
time. Trygve Reenskaug wrote a methodology book based around
analyzing roles and synthesizing them into classes. Robert Martin
talks about this topic as the Interface Segregation Principle: role interfaces follow that principle but header interfaces do not.
|