Interface segregation - Writing SOLID code - Adaptive Code via C#. Agile coding with design patterns and SOLID principles (2014)

Adaptive Code via C#. Agile coding with design patterns and SOLID principles (2014)

Part II: Writing SOLID code

Chapter 8. Interface segregation

After completing this chapter, you will be able to

Image Understand the importance of interface segregation.

Image Write interfaces with the client code’s requirements as a primary concern.

Image Create smaller interfaces with more directed purposes.

Image Identify scenarios where interface segregation can be used.

Image Split interfaces by their implementations’ dependencies.

The interface, as earlier chapters have established, is a key tool in the modern object-oriented programmer’s toolkit. Interfaces represent the boundaries between what client code requires and how that requirement is implemented. The interface segregation principle states that interfaces should be small.

Each member of an interface needs to be implemented in its entirety: properties, events, and methods. Unless every client of an interface requires every member, it does not make sense to require every implementation to fulfill such a large contract. Bearing in mind the single responsibility principle and how developers can make liberal use of the Decorator pattern, for every member present in an interface, there needs to be a valid analogy for the decoration being implemented.

At their simplest, interfaces contain single methods that serve a single purpose. At this level of granularity, they are akin to delegates, but with many added benefits.

A segregation example

This chapter works through a complete example that progresses from a single monolithic interface to multiple smaller interfaces. Along the way, a variety of decorators will be created to elaborate on one of the key benefits of liberally applying interface segregation.

A simple CRUD interface

The interface itself is quite simple, with only five methods. It is used to allow clients to interact with persistent storage for an entity through the traditional CRUD operations. CRUD stands for create, read, update, and delete. These are the common operations that clients need to maintain persistent storage for an entity. Figure 8-1 shows a UML class diagram explaining the operations available to the ICreateReadUpdateDelete interface.

Image

FIGURE 8-1 The initial interface before segregation.

The read operations are split into two methods, one for retrieving a single record from storage and another for reading all of the records. In code, this interface is as shown in Listing 8-1.

LISTING 8-1 A simple interface for CRUD operations on an entity.


public interface ICreateReadUpdateDelete<TEntity>
{
void Create(TEntity entity);

TEntity ReadOne(Guid identity);

IEnumerable<TEntity> ReadAll();

void Update(TEntity entity);

void Delete(TEntity entity);
}


The ICreateReadUpdateDelete interface is generic, allowing reuse across different entity types. However, by making the interface generic, rather than making each individual method generic, you force clients to declare up front which TEntity you are dealing with, which clarifies its dependencies. If a client wants to perform CRUD operations on more than one entity, it will have to request multiple ICreateReadUpdateDelete<TEntity> instances, one for each entity type.


Image Note

Though clients will require an instance of ICreateReadUpdateDelete<TEntity> per entity, there could still only be one implementation of ICreateReadUpdateDelete<TEntity> that would suffice for all concrete TEntity types. This implementation would also be generic.


Every operation of CRUD is performed by each implementation of the ICreateReadUpdate-Delete interface—including any decorator. This would be acceptable for decorators such as logging or transaction handling, as Listing 8-2 shows.

LISTING 8-2 Some decorators apply to all methods.


public class CrudLogging<TEntity> : ICreateReadUpdateDelete<TEntity>
{
private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud;
private readonly ILog log;
public CrudLogging(ICreateReadUpdateDelete<TEntity> decoratedCrud, ILog log)
{
this.decoratedCrud = decoratedCrud;
this.log = log;
}

public void Create(TEntity entity)
{
log.InfoFormat("Creating entity of type {0}", typeof(TEntity).Name);
decoratedCrud.Create(entity);
}

public TEntity ReadOne(Guid identity)
{
log.InfoFormat("Reading entity of type {0} with identity {1}",
typeof(TEntity).Name, identity);
return decoratedCrud.ReadOne(identity);
}

public IEnumerable<TEntity> ReadAll()
{
log.InfoFormat("Reading all entities of type {0}", typeof(TEntity).Name);
return decoratedCrud.ReadAll();
}

public void Update(TEntity entity)
{
log.InfoFormat("Updating entity of type {0}", typeof(TEntity).Name);
decoratedCrud.Update(entity);
}

public void Delete(TEntity entity)
{
log.InfoFormat("Deleting entity of type {0}", typeof(TEntity).Name);
decoratedCrud.Delete(entity);
}

}
// . . .
public class CrudTransactional<TEntity> : ICreateReadUpdateDelete<TEntity>
{
private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud;
public CrudTransactional(ICreateReadUpdateDelete<TEntity> decoratedCrud)
{
this.decoratedCrud = decoratedCrud;
}

public void Create(TEntity entity)
{
using (var transaction = new TransactionScope())
{
decoratedCrud.Create(entity);

transaction.Complete();
}
}

public TEntity ReadOne(Guid identity)
{
TEntity entity;
using (var transaction = new TransactionScope())
{
entity = decoratedCrud.ReadOne(identity);

transaction.Complete();
}
return entity;
}

public IEnumerable<TEntity> ReadAll()
{
IEnumerable<TEntity> allEntities;
using (var transaction = new TransactionScope())
{
allEntities = decoratedCrud.ReadAll();

transaction.Complete();
}
return allEntities;
}

public void Update(TEntity entity)
{
using (var transaction = new TransactionScope())
{
decoratedCrud.Update(entity);

transaction.Complete();
}
}

public void Delete(TEntity entity)
{
using (var transaction = new TransactionScope())
{
decoratedCrud.Delete(entity);

transaction.Complete();
}
}

}


The decorators for logging and transaction management are cross-cutting concerns. Irrespective of the method on the interface and, in many cases, irrespective of the interface itself, logging and transaction management could be applied. Thus, to avoid repetitive implementations for multiple interfaces, you can decorate all implementations by using aspect-oriented programming.

Some other decorators apply only to a subset of the methods of a single interface, rather than to all of them. For example, you might want to prompt the user before you permanently delete an entity from persistent storage—a common requirement. Remember that you do not want to edit an existing class, which would violate the open/closed principle. Instead, you should create a new implementation of an existing interface that clients are already using to perform the delete action. This is the Delete method of the ICreateReadUpdateDelete<TEntity> interface. Such an implementation would look like Listing 8-3.

LISTING 8-3 If a decorator targets part of an interface, segregation is an option.


public class DeleteConfirmation<TEntity> : ICrud<TEntity>
{
private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud;
public DeleteConfirmation(ICreateReadUpdateDelete<TEntity> decoratedCrud)
{
this.decoratedCrud = decoratedCrud;
}

public void Create(TEntity entity)
{
decoratedCrud.Create(entity);
}

public TEntity ReadOne(Guid identity)
{
return decoratedCrud.ReadOne(identity);
}

public IEnumerable<TEntity> ReadAll()
{
return decoratedCrud.ReadAll();
}

public void Update(TEntity entity)
{
decoratedCrud.Update(entity);
}

public void Delete(TEntity entity)
{
Console.WriteLine("Are you sure you want to delete the entity? [y/N]");
var keyInfo = Console.ReadKey();
if (keyInfo.Key == ConsoleKey.Y)
{
decoratedCrud.Delete(entity);
}
}
}


The DeleteConfirmation<TEntity> class decorates only the Delete method, as its name suggests. The other methods are implemented with pass-through delegation to the wrapped interface. Pass-through means that there is no decoration for that method: the call is merelypassed through the decorator to the underlying implementation, almost as if it had been called directly. Despite the fact that these pass-through methods apparently do nothing special, in order to maintain unit test coverage and ensure that they are delegating properly, test methods should still be written to verify that their behavior is correct. This is laborious when compared to the alternative: interface segregation.

By separating the Delete method from the rest of the ICreateReadUpdateDelete<TEntity> interface, you have two interfaces, as shown in Listing 8-4.

LISTING 8-4 The ICreateReadUpdateDelete interface is split in two.


public interface ICreateReadUpdate<TEntity>
{
void Create(TEntity entity);

TEntity ReadOne(Guid identity);

IEnumerable<TEntity> ReadAll();

void Update(TEntity entity);
}
// . . .
public interface IDelete<TEntity>
{
void Delete(TEntity entity);
}


This allows the confirmation decorator to be replaced with an implementation only for the IDelete<TEntity> interface, as shown in Listing 8-5.

LISTING 8-5 The confirmation decorator is applied only to the interface to which it pertains.


public class DeleteConfirmation<TEntity> : IDelete<TEntity>
{
private readonly IDelete<TEntity> decoratedDelete;

public DeleteConfirmation(IDelete<TEntity> decoratedDelete)
{
this.decoratedDelete = decoratedDelete;
}

public void Delete(TEntity entity)
{
Console.WriteLine("Are you sure you want to delete the entity? [y/N]");
var keyInfo = Console.ReadKey();
if (keyInfo.Key == ConsoleKey.Y)
{
decoratedDelete.Delete(entity);
}
}
}


This is an improvement, because there is less code overall, without the pass-through decoration methods, so the intent is much clearer. Also, less code means less testing.

Before moving on to the next decorator, consider the following refactor that is available for the DeleteConfirmation decorator. You should encapsulate the user interrogation into a simple interface. This way, you could write multiple different implementations of this new interface—one for each user interface type (for example, console, Windows Forms, and Windows Presentation Foundation)—and the decorator would not need to change. You should do this because the DeleteConfirmation class does not currently adhere to the single responsibility principle. As it is now, it contains two reasons to change: the interface that it delegates to has changed, and you want to elicit confirmation from the user in a different manner. Asking users whether they want to delete an entity requires a very simple predicate-like interface, as shown in Listing 8-6.

LISTING 8-6 A very simple interface for asking the user to confirm something.


public interface IUserInteraction
{
bool Confirm(string message);
}


Caching

The next decorator that you could implement is for the read methods: ReadOne and ReadAll. For both of these methods, you want to cache the returned value from the decorated implementation and return the contents of the cache in all subsequent requests. Again, with no equivalent analogy for caching the Create or Update methods, the first decorator contains needless methods, as in Listing 8-7.

LISTING 8-7 The caching decorator includes redundant, pass-through methods.


public class CrudCaching<TEntity> : ICreateReadUpdate<TEntity>
{
private TEntity cachedEntity;
private IEnumerable<TEntity> allCachedEntities;
private readonly ICreateReadUpdate<TEntity> decorated;

public CrudCaching(ICreateReadUpdate<TEntity> decorated)
{
this.decorated = decorated;
}

public void Create(TEntity entity)
{
decorated.Create(entity);
}

public TEntity ReadOne(Guid identity)
{
if(cachedEntity == null)
{
cachedEntity = decorated.ReadOne(identity);
}
return cachedEntity;
}

public IEnumerable<TEntity> ReadAll()
{
if (allCachedEntities == null)
{
allCachedEntities = decorated.ReadAll();
}
return allCachedEntities;
}

public void Update(TEntity entity)
{
decorated.Update(entity);
}

}


By applying interface segregation a second time, you can factor out the two methods used for reading data into their own interface, and they can now be decorated separately. The new IRead interface, and its accompanying caching decorator, is shown in Listing 8-8.

LISTING 8-8 The IRead interface is targeted specifically by the ReadCaching decorator.


public interface IRead<TEntity>
{
TEntity ReadOne(Guid identity);

IEnumerable<TEntity> ReadAll();
}
// . . .
public class ReadCaching<TEntity> : IRead<TEntity>
{
private TEntity cachedEntity;
private IEnumerable<TEntity> allCachedEntities;

private readonly IRead<TEntity> decorated;
public ReadCaching(IRead<TEntity> decorated)
{
this.decorated = decorated;
}

public TEntity ReadOne(Guid identity)
{
if(cachedEntity == null)
{
cachedEntity = decorated.ReadOne(identity);
}
return cachedEntity;
}

public IEnumerable<TEntity> ReadAll()
{
if (allCachedEntities == null)
{
allCachedEntities = decorated.ReadAll();
}
return allCachedEntities;
}

}


Before you implement the final decorator, the remaining interface contains only two methods, as Listing 8-9 shows.

LISTING 8-9 The remaining methods can probably be unified.


public interface ICreateUpdate<TEntity>
{
void Create(TEntity entity);

void Update(TEntity entity);
}


The Create and Update methods have identical signatures. Not only that, they serve very similar purposes: the former saves a new entity, and the latter saves an existing entity. You could unify these methods into one Save method, which acknowledges that the distinction between creating and updating is an implementation detail that clients don’t need to know about. After all, a client is likely to want to both save and update an entity, so requiring two interfaces that are so similar seems needless when there is a viable alternative. All that clients of the interface want to do is persist an entity. The refactored interface looks like the one in Listing 8-10.

LISTING 8-10 ISave implementations will either create or update an entity, as appropriate.


public interface ISave<TEntity>
{
void Save(TEntity entity);
}


After this refactor, you can add a new decorator that is specific to this interface—audit tracking. Every time a user saves an entity, you want to add some metadata to persistent storage. Specifically, you want to know which user enacted the save and at what time. Listing 8-11 shows theSaveAuditing decorator.

LISTING 8-11 Two ISave interfaces are used by the audit decorator.


public class SaveAuditing<TEntity> : ISave<TEntity>
{
private readonly ISave<TEntity> decorated;
private readonly ISave<AuditInfo> auditSave;
public SaveAuditing(ISave<TEntity> decorated, ISave<AuditInfo> auditSave)
{
this.decorated = decorated;
this.auditSave = auditSave;
}

public void Save(TEntity entity)
{
decorated.Save(entity);
var auditInfo = new AuditInfo
{
UserName = Thread.CurrentPrincipal.Identity.Name,
TimeStamp = DateTime.Now
};
auditSave.Save(auditInfo);
}

}


The SaveAuditing decorator implements the ISave interface, but it also needs to be constructed with two further ISave implementations. The first must match the TEntity generic type parameter of the decorator and is used to do the real work of saving (or, of course, to provide further decoration on the way to doing the real work of saving). The second is an ISave implementation that is specifically for saving AuditInfo types. This class is not shown, but it can be inferred to contain string UserName and DateTime TimeStamp properties. When clients call the Save method, a new AuditInfo instance is created and its properties are set. The real implementation responsible for saving this instance will then persist this new record to storage.

Again, it is worth reiterating that client code has no idea that this is happening; it is entirely unaware that auditing is occurring and does not need to change as a result of its implementation. Similarly, the leaf implementation of the ISave<TEntity> interface—that is, the nondecorator version that is responsible for the actual work of saving—is also unaware of the decorator and does not need to change to accommodate any specific decoration.

You now have three different interfaces where before you had one, and each has a decorator that provides some different, meaningful, real-world function. Figure 8-2 shows a UML class diagram of the new interfaces and their decorators after segregation.

Image

FIGURE 8-2 Interface segregation allows you to target methods for decoration without redundancy.

Multiple interface decoration

Each decorator so far has maintained a one-to-one relationship with the interface that it enhances. This is true because each decorator implements the interface that it is decorating. But you can use the Adapter pattern in conjunction with the Decorator pattern to produce multiple decorators while minimizing the code you must write.

The next decorator you will create is intended to publish an event whenever a record is saved or deleted. This notification will allow disparate subscribers to act upon any change to persistent storage. Note that there is no analogous event for reading any records, so the IRead interface will not be targeted in this instance.

For this, you first need a mechanism for publishing and subscribing events. Continuing the theme of interface segregation, this is split into the two interfaces shown in Listing 8-12.

LISTING 8-12 Two interfaces for publishing and subscribing to events.


public interface IEventPublisher
{
void Publish<TEvent>(TEvent @event)
where TEvent : IEvent;
}
// . . .
public interface IEventSubscriber
{
void Subscribe<TEvent>(TEvent @event)
where TEvent : IEvent;
}


The IEvent interface is extremely simple, containing just a string Name property. By using these two interfaces, a decorator can be created, as in Listing 8-13, that publishes a specific event when an entity is deleted.

LISTING 8-13 This decorator publishes an event when an entity is deleted.


public class DeleteEventPublishing<TEntity> : IDelete<TEntity>
{
private readonly IDelete<TEntity> decorated;
private readonly IEventPublisher eventPublisher;

public DeleteEventPublishing(IDelete<TEntity> decorated, IEventPublisher
eventPublisher)
{
this.decorated = decorated;
this.eventPublisher = eventPublisher;
}

public void Delete(TEntity entity)
{
decorated.Delete(entity);
var entityDeleted = new EntityDeletedEvent<TEntity>(entity);
eventPublisher.Publish(entityDeleted);
}

}


From here, you have two choices: implementing the equivalent ISave decorator for publishing events on the same class or implementing the ISave decorator in a new class. Listing 8-14 shows the former option, which involves renaming the existing class and adding a new Savemethod.

LISTING 8-14 Two decorators can be implemented in one class.


public class ModificationEventPublishing<TEntity> : IDelete<TEntity>, ISave<TEntity>
{
private readonly IDelete<TEntity> decoratedDelete;
private readonly ISave<TEntity> decoratedSave;
private readonly IEventPublisher eventPublisher;

public ModificationEventPublishing(IDelete<TEntity> decoratedDelete, ISave<TEntity>
decoratedSave, IEventPublisher eventPublisher)
{
this.decoratedDelete = decoratedDelete;
this.decoratedSave = decoratedSave;
this.eventPublisher = eventPublisher;
}

public void Delete(TEntity entity)
{
decoratedDelete.Delete(entity);
var entityDeleted = new EntityDeletedEvent<TEntity>(entity);
eventPublisher.Publish(entityDeleted);
}

public void Save(TEntity entity)
{
decoratedSave.Save(entity);
var entitySaved = new EntitySavedEvent<TEntity>(entity);
eventPublisher.Publish(entitySaved);
}

}


A single class can be the implementation for multiple decorators—but only when the context of the decorator is shared, as in this example. The ModificationEventPublishing decorator is implementing the same functionality—event publication—for both of the interfaces that it implements. It would be unwise, however, to combine decorators for event publishing with those for auditing, for example. This is due to the relative dependencies involved. One decorator depends on the IEventPublisher interface, whereas the other depends on the AuditInfo class. It would be better instead to separate those implementations into their own assemblies with their own dependency chains.

Client construction

The design of interfaces—segregated or otherwise—affects the classes that implement the interfaces and also the clients that use the interfaces. If clients are to use interfaces, they must in some way be supplied them. This chapter will continue, for the most part, to manually construct the implementations and provide them to clients via constructor parameters. For an alternative option, see the next chapter, which covers dependency injection.

The manner in which you supply the implementations to clients is partly dictated by the number of implementations of the segregated interfaces. If each interface is given its own implementation, each of those implementations needs to be constructed and supplied to the client. Alternatively, if all of the interfaces are implemented in a single class, a single instance is sufficient for all of the related dependencies on the client.

Multiple implementations, multiple instances

Continuing the CRUD example, assume that the IRead, ISave, and IDelete interfaces have all been implemented by different, distinct classes. A client needing to use these interfaces will, because of segregation, require three interfaces whereas it previously only required one. Such a client is shown in Listing 8-15.

LISTING 8-15 The order-specific controller requires each facet of CRUD as a separate dependency.


public class OrderController
{
private readonly IRead<Order> reader;
private readonly ISave<Order> saver;
private readonly IDelete<Order> deleter;

public OrderController(IRead<Order> orderReader, ISave<Order> orderSaver,
IDelete<Order> orderDeleter)
{
reader = orderReader;
saver = orderSaver;
deleter = orderDeleter;
}

public void CreateOrder(Order order)
{
saver.Save(order);
}

public Order GetSingleOrder(Guid identity)
{
return reader.ReadOne(identity);
}

public void UpdateOrder(Order order)
{
saver.Save(order);
}

public void DeleteOrder(Order order)
{
deleter.Delete(order);
}
}


This controller works specifically with order entities. This means that each of the interfaces supplied contains the Order class as the generic parameter. If you were to alter any of those declarations to use a different type, the operations provided by that interface would then require that type. For example, if you decided to change the delete interface parameter to IDelete<Customer>, the DeleteOrder method of the OrderController would complain that you were trying to delete an Order with a method that only accepts Customers. This is simply strong typing and generics in action.

Each method of this controller class requires a different interface to perform its function. For clarity, each method maps one to one with the operations on the respective interfaces. It is quite likely that this will not always be the case, of course.

As its name suggests, the OrderController deals only with Order classes. You can make use of the fact that the service interfaces are each generic by implementing a controller that is similarly generic. This is shown in Listing 8-16.

LISTING 8-16 An entity-generic version of the controller class requires an entity-generic version of each CRUD interface.


public class GenericController<TEntity>
{
private readonly IRead<TEntity> reader;
private readonly ISave<TEntity> saver;
private readonly IDelete<TEntity> deleter;

public GenericController(IRead<TEntity> entityReader, ISave<TEntity> entitySaver,
IDelete<TEntity> entityDeleter)
{
reader = entityReader;
saver = entitySaver;
deleter = entityDeleter;
}

public void CreateEntity(TEntity entity)
{
saver.Save(entity);
}

public TEntity GetSingleEntity(Guid identity)
{
return reader.ReadOne(identity);
}

public void UpdateEntity(TEntity entity)
{
saver.Save(entity);
}

public void DeleteEntity(TEntity entity)
{
deleter.Delete(entity);
}
}


There is little difference between this version of the controller and the prior one, but the impact on the amount of code that you might have to write could be significant. This controller can be instantiated to operate on any entity, and the service interfaces that are required are all forced to agree on the same operation. No longer can you supply different types for each one—such as ISave<Customer>, IRead<Order>, IDelete<LineItem>.

Either version of the controller can be created in much the same way. Listing 8-17 shows how you must instantiate an instance of each class that implements the required interfaces before passing them in to the controller’s constructor.

LISTING 8-17 Creating the OrderController with separate instances of the dependencies.


static OrderController CreateSeparateServices()
{
var reader = new Reader<Order>();
var saver = new Saver<Order>();
var deleter = new Deleter<Order>();

return new OrderController(reader, saver, deleter);
}


By creating classes for each individual segregated interface, the segregation has, in effect, permeated the implementations. The key point to note is that the three parameters to the OrderController class—reader, saver, and delete—are not just distinct instances, they are also distinct types.

Single implementation, single instance

A second approach to implementing segregated interfaces is to inherit all of them into one single class. This might at first appear somewhat counterintuitive (after all, what is the point of segregating interfaces just to unify them all again in the implementation?), but be patient. Listing 8-18shows all three interfaces on a single class.

LISTING 8-18 All interfaces can be implemented in a single class.


public class CreateReadUpdateDelete<TEntity> :
IRead<TEntity>, ISave<TEntity>, IDelete<TEntity>
{
public TEntity ReadOne(Guid identity)
{
return default(TEntity);
}

public IEnumerable<TEntity> ReadAll()
{
return new List<TEntity>();
}

public void Save(TEntity entity)
{

}

public void Delete(TEntity entity)
{

}
}


Remember, clients are not aware of the existence of this class. At compile time, they are only aware of the individual interfaces, which it requires one by one. To the client, each interface will still only have the members declared on that interface, regardless of the fact that the underlying implementation has other operations available. This is how interfaces are used for encapsulation and information hiding—they are analogous to a small window onto the implementing class, masking out what it does not allow the client to see.

Even with this change, the controller from the multiple implementation example is still sufficient: it correctly asks for each interface as a separate constructor parameter. What needs to change is how you construct the controller and supply it with those parameters. This is shown in Listing 8-19.

LISTING 8-19 Although it might look unusual, this is an expected side effect of interface segregation.


public OrderController CreateSingleService()
{
var crud = new CreateReadUpdateDelete<Order>();

return new OrderController(crud, crud, crud);
}


First, you only need a single instance of the CreateReadUpdateDelete class. It implements all three interfaces, so it suffices for all three constructor parameters.

As unusual as that might look—passing in the same instance three times—it makes sense because each parameter requires a different facet of the class. This is a common side effect of the interface segregation principle.

Of the two variations explored, this single implementation for a suite of related—but segregated—interfaces is not as versatile as having multiple implementations. It is most commonly used for the leaf implementation of the interfaces—that is, the implementation that is neither decorator nor adapter. It is the one that does the actual work. The reason for this is that the context is the same across all implementations. Whether you are using NHibernate, ADO.NET, Entity Framework, or some other persistence framework, the leaf implementation is the one that directly uses these libraries. In each case—reading, saving, or deleting—that library will be used to do the main work.

Some decorators and adapters also apply to the full suite of segregated interfaces, but it is more common for these to be implemented individually only on the appropriate interface.

The Interface Soup anti-pattern

A common mistake is to take the segregated interfaces and reunify them in an aggregate for some reason, as Listing 8-20 demonstrates. This is usually done to avoid the odd-looking multiple injection that you saw previously.

LISTING 8-20 Interface segregation is wrongly circumvented when all interfaces are thrown together to form a soup.


interface IInterfaceSoupAntiPattern<TEntity> : IRead<TEntity>, ISave<TEntity>,
IDelete<TEntity>
{
}


This creates an “interface soup” that is made from constituent interfaces but undermines the benefits of interface segregation. Implementers will again be required to provide implementations of all operations and so there is no scope for targeted decoration.

Splitting interfaces

The ability—or requirement—to decorate interfaces is only one reason that you might want to split a large interface into smaller constituents. However, I view this as a good enough reason for the practice.

Two more utilitarian reasons for interface segregation are based on client need and architectural design.

Client need

Different developers work on different parts of code. Therefore, it is likely that two or more developers will converge at some point, with one using the interface of another. Having detailed, step-by-step instructions for an interface is not only unlikely, but also impractical. Writing any code—especially code that is sufficiently unit tested—takes time. Writing extensive documentation, even for the end user, is tedious and time consuming. Instead, it is better to program as defensively as is possible, to prevent other developers—or even yourself in the future—from inadvertently doing something they shouldn’t with your interface.

It helps to remember that clients need only what they need. Monolithic interfaces tend to hand too much control to clients. Interfaces with a large number of members allow clients to do more than they perhaps should, clouding the intent and misdirecting the focus. All classes shouldalways have a single responsibility.

Listing 8-21 shows an interface that allows clients to interact with a user’s settings—specifically, the user interface theme that the clients have set for the application. This example is surprising—it is an interface with a single property that, in this particular scenario, is still exposing too much to its client. How can you possibly segregate this further?

LISTING 8-21 The user settings interface allows access to the application’s current theme.


public interface IUserSettings
{
string Theme
{
get;
set;
}
}


First, see the implementation in Listing 8-22, which uses the ConfigurationManager class to read and write to the AppSettings section of the configuration files.

LISTING 8-22 An implementation that loads settings from the configuration file.


public class UserSettingsConfig : IUserSettings
{
private const string ThemeSetting = "Theme";

private readonly Configuration config;

public UserSettingsConfig()
{
config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
}

public string Theme
{
get
{
return config.AppSettings.Settings[ThemeSetting].Value;
}
set
{
config.AppSettings.Settings[ThemeSetting].Value = value;
config.Save();
ConfigurationManager.RefreshSection("appSettings");
}
}
}


So far, so what? Well, there are two clients to this interface. One is focused only on reading the data and the other is focused on writing the data. Herein lies the problem, as shown in Listing 8-23.

LISTING 8-23 The clients of the interface use the property for different purposes.


public class ReadingController
{
private readonly IUserSettings settings;

public ReadingController(IUserSettings settings)
{
this.settings = settings;
}

public string GetTheme()
{
return settings.Theme;
}
}
// . . .
public class WritingController
{
private readonly IUserSettings settings;

public WritingController(IUserSettings settings)
{
this.settings = settings;
}

public void SetTheme(string theme)
{
settings.Theme = theme;
}
}


As is to be expected, the ReadingController only uses the getter of the Theme property, whereas the WritingController only uses the setter of the Theme property. However, due to a lack of segregation, there is nothing to stop the writer from retrieving the theme nor, which is more problematic, the reader from modifying the theme.

In order to be truly defensive and eliminate the possibility of interface misuse, you can segregate the read and write portions of the interface, as shown in Listing 8-24.

LISTING 8-24 The interface is split into two parts: one for reading the theme, and one for writing it.


public interface IUserSettingsReader
{
string Theme
{
get;
}
}
// . . .
public interface IUserSettingsWriter
{
string Theme
{
set;
}
}


Although this might look a little odd, it is absolutely valid C#. It is perhaps not unusual that an interface can dictate that implementers only supply a getter for a property, but it is slightly more unusual that it require only a setter.

Each controller is now able to depend only on the interface that it truly requires. As Listing 8-25 shows, the ReadingController is paired with the IUserSettingsReader, and the Writing-Controller is paired with the IUserSettingsWriter.

LISTING 8-25 Each of the two controllers now depends only on the interface that it requires.


public class ReadingController
{
private readonly IUserSettingsReader settings;

public ReadingController(IUserSettingsReader settings)
{
this.settings = settings;
}

public string GetTheme()
{
return settings.Theme;
}
}
// . . .
public class WritingController
{
private readonly IUserSettingsWriter settings;

public WritingController(IUserSettingsWriter settings)
{
this.settings = settings;
}

public void SetTheme(string theme)
{
settings.Theme = theme;
}
}


Via interface segregation, you have prevented the reader from being able to write the user settings, and you have prevented the writer from being able to read the user settings. Developers are thus not able to accidently dilute the purpose of the controller by mistakenly performing an operation that they should not.

The implementing class, which uses the ConfigurationManager, changes only very subtly, as shown in Listing 8-26.

LISTING 8-26 The UsersSettingsConfig class now implements both interfaces, but clients are unaware.


public class UserSettingsConfig : IUserSettingsReader, IUserSettingsWriter
{
private const string ThemeSetting = "Theme";

private readonly Configuration config;

public UserSettingsConfig()
{
config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
}

public string Theme
{
get
{
return config.AppSettings.Settings[ThemeSetting].Value;
}
set
{
config.AppSettings.Settings[ThemeSetting].Value = value;
config.Save();
ConfigurationManager.RefreshSection("appSettings");
}
}
}


Other than in the fact that it inherits from both reader and writer interfaces, this class is identical to the previous version. Remember that this same implementation can easily be passed to both the ReadingController and the WritingController, yet the window provided by the interface means that the set and get operations, respectively, will not be available.

The requirement that some clients should be able to read without writing is particularly likely. The other scenario, where writers are not allowed to read, is less likely. In this case, instead of total segregation, you can segregate and inherit the interfaces, as shown in Listing 8-27.

LISTING 8-27 Now using methods, the writer inherits from the reader.


public interface IUserSettingsReader
{
string GetTheme();
}
// . . .
public interface IUserSettingsWriter : IUserSettingsReader
{
void SetTheme(string theme);
}


In order to do this, the Theme property had to be converted to GetTheme and SetTheme methods. This is because the language doesn’t quite support property inheritance cleanly. The Theme property is present on both interfaces. Although classes are able to cleanly implement the getand set parts of an interface from two different interfaces, this is unfortunately not the case with interface inheritance. When property names clash through interface inheritance, the compiler warns that the base class property is hidden by the subclass property. This would not achieve the result that you want, and the compiler’s suggestion that you replace the base class property with the new keyword is not a solution, either—the getter would still not be inherited.

Instead, you can change from properties to methods with the same semantic function. The GetTheme method is the same as Theme.get, and the SetTheme method is the same as Theme.set. Now the inheritance works as expected—implementers and clients of the reader interface will only have access to the GetTheme method, and implementers and clients of the writer interface will have access to both the GetTheme and SetTheme methods. Additionally, any implementation of the IUserSettingsWriter interface is automatically an implementation of theIUserSettingsReader interface.

Listing 8-28 shows a change to the writing controller: it first checks whether the theme has been changed before it tries to set a new theme. This is now acceptable because the user settings writer service is also the user settings reader service. In this case, the two interfaces do not need to be supplied separately in order to be used.

LISTING 8-28 The writing controller has access to both the getter and setter through one interface.


public class WritingController
{
private readonly IUserSettingsWriter settings;

public WritingController(IUserSettingsWriter settings)
{
this.settings = settings;
}

public void SetTheme(string theme)
{
if (settings.GetTheme() != theme)
{
settings.SetTheme(theme);
}
}
}


Authorization

Another example of segregation by client need is when a certain set of operations is only available when the application is in a specific state. For example, the operations that a user can perform are typically different depending on whether that user is logged in or not.

The unauthorized interface shown in Listing 8-29 contains operations that can be done by an anonymous, unauthenticated user.

LISTING 8-29 This interface only contains operations that anonymous users can perform.


public interface IUnauthorized
{
IAuthorized Login(string username, string password);

void RequestPasswordReminder(string emailAddress);
}


Note that the Login method returns an interface. It is only returned when the credentials are correct, and it allows clients to perform authorized actions, as shown in Listing 8-30.

LISTING 8-30 After logging in, the user will have access to privileged operations.


public interface IAuthorized
{
void ChangePassword(string oldPassword, string newPassword);

void AddToBasket(Guid itemID);

void Checkout();

void Logout();
}


The operations on this interface are only available to a user who has entered his credentials and is logged in as authenticated.

Segregating interfaces by client need prevents programmers from doing something they should not. In this case, it prevents them from executing a privileged action with an anonymous user. Of course, there are ways around this, but it is hoped that developers will realize that they are making a very fundamental change to the application in order to do something that they should not.

Architectural need

A second driver of the interface segregation principle is architectural design. High-level decisions can have a large impact on the low-level organization of the code.

In this example, the decision has been made to have an asymmetric architecture. Similar to the read/write split shown earlier, the IPersistence interface shown in Listing 8-31 contains a combination of queries and commands.

LISTING 8-31 This persistence-layer interface contains both commands and queries.


public interface IPersistence
{
IEnumerable<Item> GetAll();

Item GetByID(Guid identity);

IEnumerable<Item> FindByCriteria(string criteria);

void Save(Item item);

void Delete(Item item);
}


The asymmetric architecture that this interface is part of is specifically CQRS: Command/Query Responsibility Segregation. The recurrence of the word segregation is no accident here, because this architectural pattern is about to cause you to perform some interface segregation.

A first implementation of the IPersistence interface is shown in Listing 8-32.

LISTING 8-32 When commands and queries are handled asymmetrically, the implementation is muddled.


public class Persistence : IPersistence
{
private readonly ISession session;
private readonly MongoDatabase mongo;

public Persistence(ISession session, MongoDatabase mongo)
{
this.session = session;
this.mongo = mongo;
}

public IEnumerable<Item> GetAll()
{
return mongo.GetCollection<Item>("items").FindAll();
}

public Item GetByID(Guid identity)
{
return mongo.GetCollection<Item>("items").FindOneById(identity.ToBson());
}

public IEnumerable<Item> FindByCriteria(string criteria)
{
var query = BsonSerializer.Deserialize<QueryDocument>(criteria);
return mongo.GetCollection<Item>("Items").Find(query);
}

public void Save(Item item)
{
using(var transaction = session.BeginTransaction())
{
session.Save(item);

transaction.Commit();
}
}

public void Delete(Item item)
{
using(var transaction = session.BeginTransaction())
{
session.Delete(item);

transaction.Commit();
}
}
}


There are two very different dependencies here: NHibernate is used for commands, and MongoDB is used for queries. The former is an Object/Relational Mapper for use with a domain model. The latter is a document storage library for fast querying. This class has two disparate dependencies and therefore two reasons to change. With such differing dependencies, it is very likely that their respective decorators will similarly be different. Rather than split the entire interface into very small operations, as was done with the previous CRUD interface, this interface will only be split into two parts: commands and queries. Figure 8-3 shows a UML class diagram of how this will be orchestrated.

With the commands and queries split between two interfaces, the implementations can then depend on totally different packages. The commands implementation will depend only on NHibernate, and the queries implementation will depend only on MongoDB.

Image

FIGURE 8-3 Splitting interfaces by architectural need allows implementations to have very different dependencies.

Ideally, these two implementations will not only be different classes, but those classes will reside in different packages—assemblies—too. If not, the problem is only partially alleviated because it will be impossible to reuse one implementation without depending on the other, plus the chain of dependencies that comes with it.

Listing 8-33 shows the interfaces after they have been split. The two can now be implemented separately.

LISTING 8-33 The interface has been split into query and command methods.


public interface IPersistenceQueries
{
IEnumerable<Item> GetAll();

Item GetByID(Guid identity);

IEnumerable<Item> FindByCriteria(string criteria);
}
// . . .
public interface IPersistenceCommands
{
void Save(Item item);

void Delete(Item item);
}


As shown in Listing 8-34, the queries class is the same implementation as before, except for the commands—and the dependency on NHibernate—being completely excised.

LISTING 8-34 The query implementation depends only on MongoDB.


public class PersistenceQueries : IPersistenceQueries
{
private readonly MongoDatabase mongo;

public Persistence(MongoDatabase mongo)
{
this.mongo = mongo;
}

public IEnumerable<Item> GetAll()
{
return mongo.GetCollection<Item>("items").FindAll();
}

public Item GetByID(Guid identity)
{
return mongo.GetCollection<Item>("items").FindOneById(identity.ToBson());
}

public IEnumerable<Item> FindByCriteria(string criteria)
{
var query = BsonSerializer.Deserialize<QueryDocument>(criteria);
return mongo.GetCollection<Item>("Items").Find(query);
}
}


In exactly the same manner, the commands class contains no queries, nor any reference to MongoDB, as shown in Listing 8-35.

LISTING 8-35 The command implementation depends only on NHibernate.


public class PersistenceCommands : IPersistenceCommands
{
private readonly ISession session;
public PersistenceCommands(ISession session)
{
this.session = session;
}

public void Save(Item item)
{
using(var transaction = session.BeginTransaction())
{
session.Save(item);

transaction.Commit();
}
}

public void Delete(Item item)
{
using(var transaction = session.BeginTransaction())
{
session.Delete(item);

transaction.Commit();
}
}
}


Single-method interfaces

Interface segregation taken to its logical conclusion results in very small interfaces. The smaller the interface, the more versatile it becomes. Such interfaces have analogies in the framework: Action, Func, and Predicate. However, delegates are not as versatile as interfaces. Though delegates certainly have their uses, the fact that interfaces can be decorated, adapted, and composed in all kinds of different ways sets them apart. Because interfaces must be implemented, there can also be extra context provided through other interfaces implemented on the same class, or via constructor parameters.

The simplest interface available has a single method. The simplest method available accepts no parameters and returns no value, as shown in Listing 8-36.

LISTING 8-36 ITask is the simplest interface possible.


public interface ITask
{
void Do();
}


This interface is extremely decoratable. Because it returns no value, it can even have an asynchronous fire-and-forget decorator. It can be used whenever a client needs to send a message but does not have any context to provide it, nor does it require any response to be returned.

A step up from this is the action interface, which is analogous to the Action delegate in the framework. It takes a generic parameter that dictates the type of its context. The IAction interface is shown in Listing 8-37.

LISTING 8-37 The IAction interface adds a context parameter.


public interface IAction<TContext>
{
void Do(TContext context);
}


This is only slightly more complex than the task. If you introduce a return value, instead of a parameter, you create a function, as shown in Listing 8-38.

LISTING 8-38 IFunction interfaces have return values.


public interface IFunction<TReturn>
{
TReturn Do();
}


A further specialization of this interface is to require that the function return a Boolean value. This creates a predicate, as shown in Listing 8-39.

LISTING 8-39 IPredicate is a function that returns a Boolean value.


public interface IPredicate
{
bool Test();
}


The predicate can be used to encapsulate a branching test, such as an if statement or the clause of a loop.

Although these interfaces look unassuming, a lot can be achieved by decorating, adapting, and composing a number of different instances of these interfaces.

Conclusion

This chapter has been dedicated to the art of good interface design. Too often, interfaces are large facades behind which huge subsystems are hidden. At a certain critical mass, interfaces lose the adaptability that makes them so fundamental to developing solid code.

There are plenty of reasons why interfaces should be segregated—to aid decoration, to correctly hide functionality from clients, as self-documentation for other developers, or as a side effect of architectural design. Whatever the reason, it is a technique that should be kept at the forefront of your mind whenever you are creating an interface. As with most programming tools, it is easier to start out on the right path than to go back and heavily refactor.