This principle deals with the disadvantages of “fat” interfaces. Classes whose interfaces are not cohesive have “fat” interfaces. In other words, the interfaces of the class can be broken up into groups of methods. Each group serves a different set of clients. Thus, some clients use one group of methods, and other clients use the other groups.
ISP acknowledges that there are objects that require noncohesive interfaces; however, it suggests that clients should not know about them as a single class. Instead, clients should know about abstract base classes that have cohesive interfaces.
Consider a security system in which Door
objects can be locked and unlocked and know whether they are open or closed. (See Listing 12-1.) This Door
is coded as an interface so that clients can use objects that conform to the Door
interface without having to depend on particular implementations of Door
.
public interface Door
{
void Lock();
void Unlock();
bool IsDoorOpen();
}
Now consider that one such implementation, TimedDoor
, needs to sound an alarm when the door has been left open for too long. In order to do this, the TimedDoor
object communicates with another object called a Timer
. (See Listing 12-2.)
public class Timer
{
public void Register(int timeout, TimerClient client)
{/*code*/}
}
public interface TimerClient
{
void TimeOut();
}
When an object wishes to be informed about a timeout, it calls the Register
function of the Timer
. The arguments of this function are the time of the timeout and a reference to a TimerClient
object whose TimeOut
function will be called when the timeout expires.
How can we get the TimerClient
class to communicate with the TimedDoor
class so that the code in the TimedDoor
can be notified of the timeout? There are several alternatives. Figure 12-1 shows a common solution. We force Door
, and therefore TimedDoor
, to inherit from TimerClient
. This ensures that TimerClient
can register itself with the Timer
and receive the TimeOut
message.
The problem with this solution is that the Door
class now depends on TimerClient
. Not all varieties of Door
need timing. Indeed, the original Door
abstraction had nothing whatever to do with timing. If timing-free derivatives of Door
are created, they will have to provide degenerate implementations for the TimeOut
method a potential violation of LSP. Moreover, the applications that use those derivatives will have to import the definition of the TimerClient
class, even though it is not used. That smells of needless complexity and needless redundancy.
This is an example of interface pollution, a syndrome that is common in statically typed languages, such as C#, C++, and Java. The interface of Door
has been polluted with a method that it does not require. It has been forced to incorporate this method solely for the benefit of one of its subclasses. If this practice is pursued, every time a derivative needs a new method, that method will be added to the base class. This will further pollute the interface of the base class, making it “fat.”
Moreover, each time a new method is added to the base class, that method must be implemented or allowed to default in derived classes. Indeed, an associated practice is to add these methods to the base class, giving them degenerate, or default, implementations specifically so that derived classes are not burdened with the need to implement them. As we learned previously, such a practice can violate LSP, leading to maintenance and reusability problems.
Door
and TimerClient
represent interfaces that are used by complely different clients. Timer
uses TimerClient
, and classes that manipulate doors use Door
. Since the clients are separate, the interfaces should remain separate, too. Why? Because clients exert forces on their server interfaces.
When we think of forces that cause changes in software, we normally think about how changes to interfaces will affect their users. For example, we would be concerned about the changes to all the users of TimerClient
if its interface changed. However, there is a force that operates in the other direction. Sometimes, the user forces a change to the interface.
For example, some users of Timer
will register more than one timeout request. Consider the TimedDoor
. When it detects that the Door
has been opened, it sends the Register
message to the Timer
, requesting a timeout. However, before that timeout expires, the door closes, remains closed for a while, and then opens again. This causes us to register a new timeout request before the old one has expired. Finally, the first timeout request expires, and the TimeOut
function of the TimedDoor
is invoked. The Door
alarms falsely.
We can correct this situation by using the convention shown in Listing 12-3. We include a unique timeOutId
code in each timeout registration and repeat that code in the TimeOut
call to the TimerClient
. This allows each derivative of TimerClient
to know which timeout request is being responded to.
Clearly, this change will affect all the users of TimerClient
. We accept this, since the lack of the timeOutId
is an oversight that needs correction. However, the design in Figure 12-1 will also cause Door
, and all clients of Door
, to be affected by this fix! This smells of rigidity and viscosity. Why should a bug in TimerClient
have any affect on clients of Door
derivatives that do not require timing? This kind of strange interdependency chills customers and managers to the bone. When a change in one part of the program affects other, completely unrelated parts of the program, the cost and repercussions of changes become unpredictable, and the risk of fallout from the change increases dramatically.
public class Timer
{
public void Register(int timeout,
int timeOutId,
TimerClient client)
{/*code*/}
}
public interface TimerClient
{
void TimeOut(int timeOutID);
}
The Interface Segregation Principle
Clients should not be forced to depend on methods they do not use.
When clients are forced to depend on methods they don’t use, those clients are subject to changes to those methods. This results in an inadvertent coupling between all the clients. Said another way, when a client depends on a class that contains methods that the client does not use but that other clients do use, that client will be affected by the changes that those other clients force on the class. We would like to avoid such couplings where possible, and so we want to separate the interfaces.
Consider the TimedDoor
again. Here is an object that has two separate interfaces used by two separate clients: Timer
and the users of Door
. These two interfaces must be implemented in the same object, since the implementation of both interfaces manipulates the same data. How can we conform to ISP? How can we separate the interfaces when they must remain together?
The answer lies in the fact that clients of an object do not need to access it through the interface of the object. Rather, they can access it through delegation or through a base class of the object.
One solution is to create an object that derives from TimerClient
and delegates to the TimedDoor
. Figure 12-2 shows this solution. When it wants to register a timeout request with the Timer
, the TimedDoor
creates a DoorTimerAdapter
and registers it with the Timer
. When the Timer
sends the TimeOut
message to the DoorTimerAdapter
, the DoorTimerAdapter
delegates the message back to the TimedDoor
.
This solution conforms to ISP and prevents the coupling of Door
clients to Timer
. Even if the change to Timer
shown in Listing 12-3 were to be made, none of the users of Door
would be affected. Moreover, TimedDoor
does not have to have the exact same interface as TimerClient
. The DoorTimerAdapter
can translate the TimerClient
interface into the TimedDoor
interface. Thus, this is a very general-purpose solution. (See Listing 12-4.)
public interface TimedDoor : Door
{
void DoorTimeOut(int timeOutId);
}
public class DoorTimerAdapter : TimerClient
{
private TimedDoor timedDoor;
public DoorTimerAdapter(TimedDoor theDoor)
{
timedDoor = theDoor;
}
public virtual void TimeOut(int timeOutId)
{
timedDoor.DoorTimeOut(timeOutId);
}
}
However, this solution is also somewhat inelegant. It involves the creation of a new object every time we wish to register a timeout. Moreover, the delegation requires a very small, but still nonzero, amount of runtime and memory. In some application domains, such as embedded real-time control systems, runtime and memory are scarce enough to make this a concern.
Figure 12-3 and Listing 12-5 show how multiple inheritance can be used to achieve ISP. In this model, TimedDoor
inherits from both Door
and TimerClient
. Although clients of both base classes can make use of TimedDoor
, neither depends on the TimedDoor
class. Thus, they use the same object through separate interfaces.
public interface TimedDoor : Door, TimerClient
{
}
This solution is my normal preference. The only time I would choose the solution in Figure 12-2 over that in Figure 12-3 is if the translation performed by the DoorTimerAdapter
object were necessary or if different translations were needed at different times.
Now let’s consider a slightly more significant example: the traditional automated teller machine (ATM) problem. The user interface of an ATM needs to be very flexible. The output may need to be translated into many different languages and it may need to be presented on a screen, on a braille tablet, or spoken out a speech synthesizer (Figure 12-4). Clearly, this flexibility can be achieved by creating an abstract base class that has abstract methods for all the different messages that need to be presented by the interface.
Consider also that each transaction that the ATM can perform is encapsulated as a derivative of the class Transaction
. Thus, we might have such classes as DepositTransaction
, WithdrawalTransaction
, TransferTransaction
, and so on. Each of these classes invokes UI
methods. For example, in order to ask the user to enter the amount to be deposited, the DepositTransaction
object invokes the RequestDepositAmount
method of the UI
class. Likewise, in order to ask the user how much money to transfer between accounts, the TransferTransaction
object calls the RequestTransferAmount
method of UI
. This corresponds to the diagram in Figure 12-5.
Note that this is precisely the situation that ISP tells us to avoid. Each of the transactions is using UI
methods that no other class uses. This creates the possibility that changes to one of the derivatives of Transaction
will force corresponding change to UI
, thereby affecting all the other derivatives of Transaction
and every other class that depends on the UI
interface. Something smells like rigidity and fragility around here.
For example, if we were to add a PayGasBillTransaction
, we would have to add new methods to UI
in order to deal with the unique messages that this transaction would want to display. Unfortunately, since DepositTransaction
, WithdrawalTransaction
, and TransferTransaction
all depend on the UI
interface, they are all likely to be rebuilt. Worse, if the transactions were all deployed as components in separate assemblies, those assemblies would very likely have to be redeployed, even though none of their logic was changed. Can you smell the viscosity?
This unfortunate coupling can be avoided by segregating the UI
interface into individual interfaces, such as DepositUI
, WithdrawUI
, and TransferUI
. These separate interfaces can then be multiply inherited into the final UI
interface. Figure 12-6 and Listing 12-6 show this model.
Whenever a new derivative of the Transaction
class is created, a corresponding base class for the abstract UI
interface will be needed, and so the UI
interface and all its derivatives must change. However, these classes are not widely used. Indeed, they are probably used only by main
or whatever process boots the system and creates the concrete UI
instance. So the impact of adding new UI
base classes is minimized.
A careful examination of Figure 12-6 shows one of the issues with ISP conformance that was not obvious from the TimedDoor
example. Note that each transaction must somehow know about its particular version of the UI
. DepositTransaction
must know about DepositUI
, WithdrawTransaction
must know about WithdrawalUI
, and so on. In Listing 12-6, I have addressed this issue by forcing each transaction to be constructed with a reference to its particular UI
. Note that this allows me to use the idiom in Listing 12-7.
This is handy but also forces each transaction to contain a reference member to its UI
. In C#, one might be tempted to put all the UI
components into a single class. Listing 12-8 shows such an approach. This, however, has an unfortunate effect. The UIGlobals
class depends on DepositUI
, WithdrawalUI
, and TransferUI
. This means that a module wishing to use any of the UI
interfaces transitively depends on all of them, exactly the situation that ISP warns us to avoid. If a change is made to any of the UI
interfaces, all modules that use UIGlobals
may be forced to recompile. The UIGlobals
class has recombined the interfaces that we had worked so hard to segregate!
Listing 12-6. Segregated ATM UI interface
public interface Transaction
{
void Execute();
}
public interface DepositUI
{
void RequestDepositAmount();
}
public class DepositTransaction : Transaction
{
privateDepositUI depositUI;
public DepositTransaction(DepositUI ui)
{
depositUI = ui;
}
public virtual void Execute()
{
/*code*/
depositUI.RequestDepositAmount();
/*code*/
}
}
public interface WithdrawalUI
{
void RequestWithdrawalAmount();
}
public class WithdrawalTransaction : Transaction
{
private WithdrawalUI withdrawalUI;
public WithdrawalTransaction(WithdrawalUI ui)
{
withdrawalUI = ui;
}
public virtual void Execute()
{
/*code*/
withdrawalUI.RequestWithdrawalAmount();
/*code*/
}
}
public interface TransferUI
{
void RequestTransferAmount();
}
public class TransferTransaction : Transaction
{
private TransferUI transferUI;
public TransferTransaction(TransferUI ui)
{
transferUI = ui;
}
public virtual void Execute()
{
/*code*/
transferUI.RequestTransferAmount();
/*code*/
}
}
public interface UI : DepositUI, WithdrawalUI, TransferUI
{
}
Listing 12-7. Interface initialization idiom
UI Gui; // global object;
void f()
{
DepositTransaction dt = new DepositTransaction(Gui);
}
Listing 12-8. Wrapping the Globals in a class
public class UIGlobals
{
public static WithdrawalUI withdrawal;
public static DepositUI deposit;
public static TransferUI transfer;
static UIGlobals()
{
UI Lui = new AtmUI(); // Some UI implementation
UIGlobals.deposit = Lui;
UIGlobals.withdrawal = Lui;
UIGlobals.transfer = Lui;
}
}
Consider now a function g
that needs access to both the DepositUI
and the TransferUI
. Consider also that we wish to pass the user interfaces into this function. Should we write the function declaration like this:
void g(DepositUI depositUI, TransferUI transferUI)
Or should we write it like this:
void g(UI ui)
The temptation to write the latter (monadic) form is strong. After all, we know that in the former (polyadic) form, both arguments will refer to the same object. Moreover, if we were to use the polyadic form, its invocation might look like this:
g(ui, ui);
Somehow this seems perverse.
Perverse or not, the polyadic form is often preferable to the monadic form. The monadic form forces g
to depend on every interface included in UI
. Thus, when WithdrawalUI
changes, g
and all clients of g
could be affected. This is more perverse than g(ui,ui)
! Moreover, we cannot be sure that both arguments of g
will always refer to the same object! In the future, it may be that the interface objects are separated for some reason. The fact that all interfaces are combined into a single object is information that g
does not need to know. Thus, I prefer the polyadic form for such functions.
Clients can often be grouped together by the service methods they call. Such groupings allow segregated interfaces to be created for each group instead of for each client. This greatly reduces the number of interfaces that the service has to realize and prevents the service from depending on each client type.
Sometimes, the methods invoked by different groups of clients will overlap. If the overlap is small, the interfaces for the groups should remain separate. The common functions should be declared in all the overlapping interfaces. The server class will inherit the common functions from each of those interfaces but will implement them only once.
When object-oriented applications are maintained, the interfaces to existing classes and components often change. Sometimes, these changes have a huge impact and force the recompilation and redeployment of a very large part of the system. This impact can be mitigated by adding new interfaces to existing objects rather than changing the existing interface. If clients of the old interface wish to access methods of the new interface, they can query the object for that interface, as shown in Listing 12-9.
void Client(Service s)
{
if(s is NewService)
{
NewService ns = (NewService)s;
// use the new service interface
}
}
As with all principles, care must be taken not to overdo it. The specter of a class with hundreds of different interfaces, some segregated by client and other segregated by version, is frightening indeed.
Fat classes cause bizarre and harmful couplings between their clients. When one client forces a change on the fat class, all the other clients are affected. Thus, clients should have to depend only on methods that they call. This can be achieved by breaking the interface of the fat class into many client-specific interfaces. Each client-specific interface declares only those functions that its particular client or client group invoke. The fat class can then inherit all the client-specific interfaces and implement them. This breaks the dependence of the clients on methods that they don’t invoke and allows the clients to be independent of one another.
[GOF95] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides, Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1995.
3.129.13.201