The single responsibility principle - 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 5 The single responsibility principle

CHAPTER 6 The open/closed principle

CHAPTER 7 The Liskov substitution principle

CHAPTER 8 Interface segregation

CHAPTER 9 Dependency injection

SOLID is the acronym for a set of practices that, when implemented together, make code adaptive to change. The SOLID practices were introduced by Bob Martin almost 15 years ago. Even so, these practices are not as widely known as they could be—and perhaps should be.

In this part, a chapter is devoted to each of the SOLID principles:

Image S The single responsibility principle

Image O The open/closed principle

Image L The Liskov substitution principle

Image I Interface segregation

Image D Dependency injection

Even taken in isolation, each of these principles is a worthy practice that any software developer would do well to learn. When used in collaboration, these patterns give code a completely different structure—one that lends itself to change.

However, take note that these patterns and practices, just like all others, are merely tools for you to use. Deciding when and where to apply any pattern or practice is part of the art of software development. Overuse leads to code that is adaptive, but on too fine-grained a level to be appreciated or useful. Overuse also affects another key facet of code quality: readability. It is far more common for software to be developed in teams than as an individual pursuit. Thus, judiciously selecting when and where to apply each pattern, practice, or SOLID principle is imperative to ensure that the code remains comprehensible in the future.

Chapter 5. The single responsibility principle

After completing this chapter, you will be able to

Image Understand the importance of the single responsibility principle.

Image Identify classes that have too many responsibilities.

Image Write modules, classes, and methods that have a single responsibility.

Image Refactor monolithic classes into smaller classes with single responsibilities.

Image Use design patterns to separate responsibilities.

The single responsibility principle (SRP) instructs developers to write code that has one and only one reason to change. If a class has more than one reason to change, it has more than one responsibility. Classes with more than a single responsibility should be broken down into smaller classes, each of which should have only one responsibility and reason to change.

This chapter explains that process and shows you how to create classes that only have a single responsibility but are still useful. Through a process of delegation and abstraction, a class that contains too many reasons to change should delegate one or more responsibilities to other classes.

It is difficult to overstate the importance of delegating to abstractions. It is the lynchpin of adaptive code and, without it, developers would struggle to adapt to changing requirements in the way that Scrum and other Agile processes demand.

Problem statement

To better explain the problem with having classes that hold too many responsibilities, this section explores an example. Listing 5-1 shows a simple batch processor class that reads records from a file and updates a database. Despite its small size, you need to continually add features to this batch processor so that it meets the needs of your business.

LISTING 5-1 An example of a class with too many responsibilities.


public class TradeProcessor
{
public void ProcessTrades(System.IO.Stream stream)
{
// read rows
var lines = new List<string>();
using(var reader = new System.IO.StreamReader(stream))
{
string line;
while((line = reader.ReadLine()) != null)
{
lines.Add(line);
}
}

var trades = new List<TradeRecord>();

var lineCount = 1;
foreach(var line in lines)
{
var fields = line.Split(new char[] { ',' });

if(fields.Length != 3)
{
Console.WriteLine("WARN: Line {0} malformed. Only {1} field(s) found.",
lineCount, fields.Length);
continue;
}

if(fields[0].Length != 6)
{
Console.WriteLine("WARN: Trade currencies on line {0} malformed: '{1}'",
lineCount, fields[0]);
continue;
}

int tradeAmount;
if(!int.TryParse(fields[1], out tradeAmount))
{
Console.WriteLine("WARN: Trade amount on line {0} not a valid integer:
'{1}'", lineCount, fields[1]);
}

decimal tradePrice;
if (!decimal.TryParse(fields[2], out tradePrice))
{
Console.WriteLine("WARN: Trade price on line {0} not a valid decimal:
'{1}'", lineCount, fields[2]);
}

var sourceCurrencyCode = fields[0].Substring(0, 3);
var destinationCurrencyCode = fields[0].Substring(3, 3);

// calculate values
var trade = new TradeRecord
{
SourceCurrency = sourceCurrencyCode,
DestinationCurrency = destinationCurrencyCode,
Lots = tradeAmount / LotSize,
Price = tradePrice
};

trades.Add(trade);

lineCount++;
}

using (var connection = new System.Data.SqlClient.SqlConnection("Data
Source=(local);Initial Catalog=TradeDatabase;Integrated Security=True"))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
foreach(var trade in trades)
{
var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandType = System.Data.CommandType.StoredProcedure;
command.CommandText = "dbo.insert_trade";
command.Parameters.AddWithValue("@sourceCurrency", trade.
SourceCurrency);
command.Parameters.AddWithValue("@destinationCurrency", trade.
DestinationCurrency);
command.Parameters.AddWithValue("@lots", trade.Lots);
command.Parameters.AddWithValue("@price", trade.Price);

command.ExecuteNonQuery();
}

transaction.Commit();
}
connection.Close();
}

Console.WriteLine("INFO: {0} trades processed", trades.Count);
}

private static float LotSize = 100000f;
}


This is more than an example of a class that has too many responsibilities; it is also an example of a single method that has too many responsibilities. By reading the code carefully, you can discern what this class is trying to achieve:

1. It reads every line from a Stream parameter, storing each line in a list of strings.

2. It parses out individual fields from each line and stores them in a more structured list of Trade-Record instances.

3. The parsing includes some validation and some logging to the console.

4. Each TradeRecord is enumerated, and a stored procedure is called to insert the trades into a database.

The responsibilities of the TradeProcessor are reading streams, parsing strings, validating fields, logging, and database insertion. The single responsibility principle states that this class, like all others, should only have a single reason to change. However, the reality of theTradeProcessor is that it will change under the following circumstances:

Image When you decide not to use a Stream for input but instead read the trades from a remote call to a web service.

Image When the format of the input data changes, perhaps with the addition of an extra field indicating the broker for the transaction.

Image When the validation rules of the input data change.

Image When the way in which you log warnings, errors, and information changes. If you are using a hosted web service, writing to the console would not be a viable option.

Image When the database changes in some way—perhaps the insert_trade stored procedure requires a new parameter for the broker, too, or you decide not to store the data in a relational database and opt for document storage, or the database is moved behind a web service that you must call.

For each of these changes, this class would have to be modified. Furthermore, unless you maintain a variety of versions, there is no possibility of adapting the TradeProcessor so that it is able to read from a different input source, for example. Imagine the maintenance headache when you are asked to add the ability to store the trades in a web service, but only if a certain command-line argument was supplied.

Refactoring for clarity

The first task on the road to refactoring the TradeProcessor so that it has one reason to change is to split the ProcessTrades method into smaller pieces so that each one focuses on a single responsibility. Each of the following listings shows a single method from the refactoredTradeProcessor class, followed by an explanation of the changes.

First, Listing 5-2 shows the ProcessTrades method, which now does nothing more than delegate to other methods.

LISTING 5-2 The ProcessTrades method is very minimal because it delegates work to other methods.


public void ProcessTrades(System.IO.Stream stream)
{
var lines = ReadTradeData(stream);
var trades = ParseTrades(lines);
StoreTrades(trades);
}


The original code was characterized by three distinct parts of a process—reading the trade data from a stream, converting the string data in the stream to TradeRecord instances, and writing the trades to persistent storage. Note that the output from one method feeds into the input to the next method. You cannot call StoreTrades until you have the trade records returned from the Parse-Trades method, and you cannot call ParseTrades until you have the lines returned from the ReadTradeData method.

Taking each of these methods in order, let’s look at ReadTradeData, in Listing 5-3.

LISTING 5-3 ReadTradeData encapsulates the original code.


private IEnumerable<string> ReadTradeData(System.IO.Stream stream)
{
var tradeData = new List<string>();
using (var reader = new System.IO.StreamReader(stream))
{
string line;
while ((line = reader.ReadLine()) != null)
{
tradeData.Add(line);
}
}
return tradeData;
}


This code is preserved from the original implementation of the ProcessTrades method. It has simply been encapsulated in a method that returns the resultant string data as a string enumeration. Note that this makes the return value read-only, whereas the original implementation unnecessarily allowed subsequent parts of the process to add further lines.

The ParseTrades method, shown in Listing 5-4, is next. It has changed somewhat from the original implementation because it, too, delegates some tasks to other methods.

LISTING 5-4 ParseTrades delegates to other methods to limit its complexity.


private IEnumerable<TradeRecord> ParseTrades(IEnumerable<string> tradeData)
{
var trades = new List<TradeRecord>();
var lineCount = 1;
foreach (var line in tradeData)
{
var fields = line.Split(new char[] { ',' });

if(!ValidateTradeData(fields, lineCount))
{
continue;
}

var trade = MapTradeDataToTradeRecord(fields);

trades.Add(trade);

lineCount++;
}
return trades;
}


This method delegates validation and mapping responsibilities to other methods. Without this delegation, this section of the process would still be too complex and it would retain too many responsibilities. The ValidateTradeData method, shown in Listing 5-5, returns a Boolean value to indicate whether any of the fields for a trade line are invalid.

LISTING 5-5 All of the validation code is in a single method.


private bool ValidateTradeData(string[] fields, int currentLine)
{
if (fields.Length != 3)
{
LogMessage("WARN: Line {0} malformed. Only {1} field(s) found.", currentLine,
fields.Length);

return false;
}

if (fields[0].Length != 6)
{
LogMessage("WARN: Trade currencies on line {0} malformed: '{1}'", currentLine,
fields[0]);

return false;
}

int tradeAmount;
if (!int.TryParse(fields[1], out tradeAmount))
{
LogMessage("WARN: Trade amount on line {0} not a valid integer: '{1}'",
currentLine, fields[1]);

return false;
}

decimal tradePrice;
if (!decimal.TryParse(fields[2], out tradePrice))
{
LogMessage("WARN: Trade price on line {0} not a valid decimal: '{1}'",
currentLine, fields[2]);

return false;
}

return true;
}


The only change made to the original validation code is that it now delegates to yet another method for logging messages. Rather than embedding calls to Console.WriteLine where needed, the LogMessage method is used, shown in Listing 5-6.

LISTING 5-6 The LogMessage method is currently just a synonym for Console.WriteLine.


private void LogMessage(string message, params object[] args)
{
Console.WriteLine(message, args);
}


Returning up the stack to the ParseTrades method, Listing 5-7 shows the other method to which it delegates. This method maps an array of strings representing the individual fields from the stream to an instance of the TradeRecord class.

LISTING 5-7 Mapping from one type to another is a separate responsibility.


private TradeRecord MapTradeDataToTradeRecord(string[] fields)
{
var sourceCurrencyCode = fields[0].Substring(0, 3);
var destinationCurrencyCode = fields[0].Substring(3, 3);
var tradeAmount = int.Parse(fields[1]);
var tradePrice = decimal.Parse(fields[2]);

var tradeRecord = new TradeRecord
{
SourceCurrency = sourceCurrencyCode,
DestinationCurrency = destinationCurrencyCode,
Lots = tradeAmount / LotSize,
Price = tradePrice
};

return tradeRecord;
}


The sixth and final new method introduced by this refactor is StoreTrades, shown in Listing 5-8. This method wraps the code for interacting with the database. It also delegates the informational log message to the aforementioned LogMessage method.

LISTING 5-8 With the StoreTrades method in place, the responsibilities in this class are clearly demarcated.


private void StoreTrades(IEnumerable<TradeRecord> trades)
{
using (var connection = new System.Data.SqlClient.SqlConnection("Data
Source=(local);Initial Catalog=TradeDatabase;Integrated Security=True"))
{
connection.Open();
using (var transaction = connection.BeginTransaction())
{
foreach (var trade in trades)
{
var command = connection.CreateCommand();
command.Transaction = transaction;
command.CommandType = System.Data.CommandType.StoredProcedure;
command.CommandText = "dbo.insert_trade";
command.Parameters.AddWithValue("@sourceCurrency", trade.SourceCurrency);
command.Parameters.AddWithValue("@destinationCurrency",
trade.DestinationCurrency);
command.Parameters.AddWithValue("@lots", trade.Lots);
command.Parameters.AddWithValue("@price", trade.Price);

command.ExecuteNonQuery();
}

transaction.Commit();
}
connection.Close();
}

LogMessage("INFO: {0} trades processed", trades.Count());
}


Looking back at this refactor, it is a clear improvement on the original implementation. However, what have you really achieved? Although the new ProcessTrades method is indisputably smaller than the monolithic original, and the code is definitely more readable, you have gained very little by way of adaptability. You can change the implementation of the LogMessage method so that it, for example, writes to a file instead of to the console, but that involves a change to the TradeProcessor class, which is precisely what you wanted to avoid.

This refactor has been an important stepping stone on the path to truly separating the responsibilities of this class. It has been a refactor for clarity, not for adaptability. The next task is to split each responsibility into different classes and place them behind interfaces. What you need is true abstraction to achieve useful adaptability.

Refactoring for abstraction

Building on the new TradeProcessor implementation, the next refactor introduces several abstractions that will allow you to handle almost any change request for this class. Although this running example might seem very small, perhaps even insignificant, it is a workable contrivance for the purposes of this tutorial. Also, it is very common for a small application such as this to grow into something much larger. When a few people start to use it, the feature requests begin to increase.

Often, the terms prototype and proof of concept are applied to such allegedly small applications, and the conversion from prototype to production application is relatively seamless. This is why the ability to refactor toward abstraction is such a touchstone of adaptive development. Without it, the myriad requests devolve into a “big ball of mud”—a class, or a group of classes in an assembly, with little delineation of responsibility and no discernible abstractions. The result is an application that has no unit tests and that is difficult to maintain and enhance, and yet that could be a critical piece of the line of business.

The first step in refactoring the TradeProcessor for abstraction is to design the interface or interfaces that it will use to perform the three high-level tasks of reading, processing, and storing the trade data. Figure 5-1 shows the first set of abstractions.

Image

FIGURE 5-1 The TradeProcessor will now depend on three new interfaces.

Because you moved all of the code from ProcessTrades into separate methods in the first refactor, you should have a good idea of where the first abstractions should be applied. As prescribed by the single responsibility principle, the three main responsibilities will be handled by different classes. As you know from previous chapters, you should not have direct dependencies from one class to another but should instead work via interfaces. Therefore, the three responsibilities are factored out into three separate interfaces. Listing 5-9 shows how theTradeProcessor class looks after this change.

LISTING 5-9 The TradeProcessor is now the encapsulation of a process, and nothing more.


public class TradeProcessor
{
public TradeProcessor(ITradeDataProvider tradeDataProvider, ITradeParser tradeParser,
ITradeStorage tradeStorage)
{
this.tradeDataProvider = tradeDataProvider;
this.tradeParser = tradeParser;
this.tradeStorage = tradeStorage;
}

public void ProcessTrades()
{
var lines = tradeDataProvider.GetTradeData();
var trades = tradeParser.Parse(lines);
tradeStorage.Persist(trades);
}

private readonly ITradeDataProvider tradeDataProvider;
private readonly ITradeParser tradeParser;
private readonly ITradeStorage tradeStorage;
}


The class is now significantly different from its previous incarnation. It no longer contains the implementation details for the whole process but instead contains the blueprint for the process. The class models the process of transferring trade data from one format to another. This is its only responsibility, its only concern, and the only reason that this class should change. If the process itself changes, this class will change to reflect it. But if you decide you no longer want to retrieve data from a Stream, log on to the console, or store the trades in a database, this class remains as is.

As prescribed by the Stairway pattern (introduced in Chapter 2, “Dependencies and layering”), the interfaces that the TradeProcessor now depends on all live in a separate assembly. This ensures that neither the client nor the implementation assemblies reference each other. Separated into another assembly are the three classes that implement these interfaces, the StreamTradeDataProvider, SimpleTradeParser, and AdoNetTradeStorage classes. Note that there is a naming convention used for these classes. First, the prefixed I was removed from the interface name and replaced with the implementation-specific context that is required of the class. So StreamTradeDataProvider allows you to infer that it is an implementation of the ITradeDataProvider interface that retrieves its data from a Stream object. TheAdoNetTradeStorage class uses ADO.NET to persist the trade data. I have prefixed the ITradeParser implementation with the word Simple to indicate that it has no dependency context.

All three of these implementations are able to live in a single assembly due to their shared dependencies—core assemblies of the Microsoft .NET Framework. If you were to introduce an implementation that required a third-party dependency, a first-party dependency of your own, or a dependency from a non-core .NET Framework class, you should put these implementations into their own assemblies. For example, if you were to use the Dapper mapping library instead of ADO.NET, you would create an assembly called Services.Dapper, inside of which would be anITradeStorage implementation called DapperTradeStorage.

The ITradeDataProvider interface does not depend on the Stream class. The previous version of the method for retrieving trade data required a Stream instance as a parameter, but this artificially tied the method to a dependency. When you are creating interfaces and refactoring toward abstractions, it is important that you do not retain dependencies where doing so would affect the adaptability of the code. The possibility of retrieving the trade data from sources other than a Stream has already been discussed, so the refactoring has ensured that this dependency is removed from the interface. Instead, the StreamTradeDataProvider requires a Stream as a constructor parameter, instead of a method parameter. By using the constructor, you can depend on almost anything without polluting the interface. Listing 5-10 shows theStreamTradeDataProvider implementation.

LISTING 5-10 Context can be passed into classes via constructor parameters, keeping the interface clean.


public class StreamTradeDataProvider : ITradeDataProvider
{
public StreamTradeDataProvider(Stream stream)
{
this.stream = stream;
}

public IEnumerable<string> GetTradeData()
{
var tradeData = new List<string>();
using (var reader = new StreamReader(stream))
{
string line;
while ((line = reader.ReadLine()) != null)
{
tradeData.Add(line);
}
}
return tradeData;
}

private Stream stream;
}


Remember that the TradeProcessor class, which is the client of this code, is aware of nothing other than the GetTradeData method’s signature via the ITradeDataProvider. It has no knowledge whatsoever of how the real implementation retrieves the data—nor should it.

There are more abstractions that can be extracted from this example. Remember that the original ParseTrades method delegated responsibility for validation and for mapping. You can repeat the process of refactoring so that the SimpleTradeParser class does not have more than one responsibility. Figure 5-2 shows in Unified Markup Language (UML) how this can be achieved.

Image

FIGURE 5-2 The SimpleTradeParser is also refactored to ensure that each class has a single responsibility.

This process of abstracting responsibilities into interfaces (and their accompanying implementations) is recursive. As you inspect each class, you must determine the responsibilities that it has and factor them out until the class has only one. Listing 5-11 shows the SimpleTradeParserclass, which delegates to interfaces where appropriate. Its single reason for change is if the overall structure of the trade data changes—for instance, if the data no longer uses comma-separated values and changes to using tabs, or perhaps XML.

LISTING 5-11 The algorithm for parsing trade data is encapsulated in ITradeParser implementations.


public class SimpleTradeParser : ITradeParser
{
public SimpleTradeParser(ITradeValidator tradeValidator, ITradeMapper tradeMapper)
{
this.tradeValidator = tradeValidator;
this.tradeMapper = tradeMapper;
}

public IEnumerable<TradeRecord> Parse(IEnumerable<string> tradeData)
{
var trades = new List<TradeRecord>();
var lineCount = 1;
foreach (var line in tradeData)
{
var fields = line.Split(new char[] { ',' });

if (!tradeValidator.Validate(fields))
{
continue;
}

var trade = tradeMapper.Map(fields);

trades.Add(trade);

lineCount++;
}
return trades;
}

private readonly ITradeValidator tradeValidator;
private readonly ITradeMapper tradeMapper;
}


The final refactor aims to abstract logging from two classes. Both the ITradeValidator and ITradeStorage implementations are still logging directly to the console. This time, instead of implementing your own logging class, you will create an adapter for the popular logging library, Log4Net. The UML class diagram in Figure 5-3 shows how this all fits together.

Image

FIGURE 5-3 By implementing an adapter for Log4Net, you need not reference it in every assembly.

The net benefit of creating an adapter class such as Log4NetLoggerAdapter is that you can convert a third-party reference into a first-party reference. Notice that both AdoNetTradeStorage and SimpleTradeValidator both depend on the first-party ILogger interface. But, at run time, both will actually use Log4Net. The only references needed to Log4Net are in the entry point of the application (see Chapter 9, “Dependency injection,” for more information) and the newly created Service.Log4Net assembly. Any code that has a dependency on Log4Net, such as custom appenders, should live in the Service.Log4Net assembly. For now, only the adapter resides in this new assembly.

The refactored validator class is shown in Listing 5-12. It now has no reference whatsoever to the console. Because of Log4Net’s flexibility, you can actually log to almost anywhere now. Total adaptability has been achieved as far as logging is concerned.

LISTING 5-12 The SimpleTradeValidator class after refactoring.


public class SimpleTradeValidator : ITradeValidator
{
private readonly ILogger logger; public SimpleTradeValidator(ILogger logger)
{
this.logger = logger;
}


public bool Validate(string[] tradeData)
{
if (tradeData.Length != 3)
{
logger.LogWarning("Line malformed. Only {1} field(s) found.",
tradeData.Length)
;
return false;
}

if (tradeData[0].Length != 6)
{
logger.LogWarning("Trade currencies malformed: '{1}'", tradeData[0]);
return false;
}

int tradeAmount;
if (!int.TryParse(tradeData[1], out tradeAmount))
{
logger.LogWarning("Trade amount not a valid integer: '{1}'", tradeData[1]);
return false;
}

decimal tradePrice;
if (!decimal.TryParse(tradeData[2], out tradePrice))
{
logger.LogWarning("WARN: Trade price not a valid decimal: '{1}'",
tradeData[2]);

return false;
}

return true;
}
}


At this point, a quick recap is in order. Bear in mind that you have altered nothing as far as the functionality of the code is concerned. Functionally, this code does exactly what it used to do. However, if you wanted to enhance it in any way, you could do so with ease. The added ability to adapt this code to a new purpose more than justifies the effort expended to refactor it.

Referring back to the original list of potential enhancements to this code, this new version allows you to implement each one without touching the existing classes.

Image Request: You decide not to use a Stream for input but instead read the trades from a remote call to a web service.

• Solution: Create a new ITradeDataProvider implementation that supplies the data from the service.

Image Request: The format of the input data changes, perhaps with the addition of an extra field indicating the broker for the transaction.

• Solution: Alter the implementations for the ITradeDataValidator, ITradeDataMapper, and ITradeStorage interfaces, which handle the new broker field.

Image Request: The validation rules of the input data change.

• Solution: Edit the ITradeDataValidator implementation to reflect the rule changes.

Image Request: The way in which you log warnings, errors, and information changes. If you are using a hosted web service, writing to the console would not be a viable option.

• Solution: As discussed, Log4Net provides you with infinite options for logging, by virtue of the adapter.

Image Request: The database changes in some way—perhaps the insert_trade stored procedure requires a new parameter for the broker, too, or you decide not to store the data in a relational database and opt for document storage, or the database is moved behind a web service that you must call.

• Solution: If the stored procedure changes, you would need to edit the AdoNetTradeStorage class to include the broker field. For the other two options, you could create a MongoTradeStorage class that uses MongoDB to store the trades, and you could create aServiceTradeStorage class to hide the implementation behind a web service.

I hope you are now fully convinced that a combination of abstracting via interfaces, decoupling assemblies to follow the Stairway pattern, aggressive refactoring, and adhering to the single responsibility principle are the foundation of adaptive code.

When you arrive at a scenario in which your code is neatly delegating to abstractions, the possibilities are endless. The rest of this chapter concentrates on other ways in which you can focus on a single responsibility per class.

SRP and the Decorator pattern

The Decorator pattern is excellent for ensuring that each class has a single responsibility. Classes can often do too many things without an obvious way of splitting the responsibilities into other classes. The responsibilities seem too closely linked.

The Decorator pattern’s basic premise is that each decorator class fulfills the contract of a type and also accepts one or more of those types as constructor parameters. This is beneficial because functionality can be added to an existing class that implements a certain interface, and the decorator also acts—unbeknownst to clients—as an implementation of the required interface. Figure 5-4 shows a UML diagram of the Decorator design pattern.

Image

FIGURE 5-4 A UML diagram showing an implementation of the Decorator pattern.

A simple example of the pattern is shown in Listing 5-13, which does not pertain to a specific use of the pattern but provides a canonical example.

LISTING 5-13 A template example of the decorator pattern.


public interface IComponent
{
void Something();
}
// . . .
public class ConcreteComponent : IComponent
{
public void Something()
{

}
}
// . . .
public class DecoratorComponent : IComponent
{
public DecoratorComponent(IComponent decoratedComponent)
{
this.decoratedComponent = decoratedComponent;
}

public void Something()
{
SomethingElse();
decoratedComponent.Something();
}

private void SomethingElse()
{

}

private readonly IComponent decoratedComponent;
}
// . . .
class Program
{
static IComponent component;

static void Main(string[] args)
{
component = new DecoratorComponent(new ConcreteComponent());
component.Something();
}
}


Because a client accepts the interface shown in the listing as a method parameter, you can provide either the original, undecorated type to that client or you can provide the decorated version. Note that the client will be oblivious: it will not have to change depending on which version it is being provided.

The Composite pattern

The Composite pattern is a specialization of the Decorator pattern and is one of the more common uses of that pattern. A UML diagram describing the Composite pattern’s collaborators is shown in Figure 5-5.

Image

FIGURE 5-5 The Composite pattern closely resembles the Decorator pattern.

The Composite pattern’s purpose is to allow you to treat many instances of an interface as if they were just one instance. Therefore, clients can accept just one instance of an interface, but they can be implicitly provided with many instances, without requiring the client to change. Listing 5-14 shows a composite decorator in practice.

LISTING 5-14 The composite implementation of an interface.


public interface IComponent
{
void Something();
}
// . . .
public class Leaf : IComponent
{
public void Something()
{

}
}
// . . .
public class CompositeComponent : IComponent
{
public CompositeComponent()
{
children = new List<IComponent>();
}

public void AddComponent(IComponent component)
{
children.Add(component);
}

public void RemoveComponent(IComponent component)
{
children.Remove(component);
}

public void Something()
{
foreach(var child in children)
{
child.Something();
}
}

private ICollection<IComponent> children;
}
// . . .
class Program
{
static void Main(string[] args)
{
var composite = new CompositeComponent();
composite.AddComponent(new Leaf());
composite.AddComponent(new Leaf());
composite.AddComponent(new Leaf());

component = composite;
component.Something();
}

static IComponent component;
}


In the CompositeComponent class, there are methods for adding and removing other instances of the IComponent. These methods do not form part of the interface and are for clients of the CompositeComponent class, directly. Whichever factory method or class is tasked with creating instances of the CompositeComponent class will also have to create the decorated instances and pass them into the Add method; otherwise, the clients of the IComponent would have to change in order to cope with compositions.

Whenever the Something method is called by the IComponent clients, the list of composed instances is enumerated, and their respective Something is called. This is how you reroute the call to a single instance of IComponent—of type CompositeComponent—to many other types.

Each instance that you supply to the CompositeComponent class must implement the IComponent interface—and this is enforced by the compiler due to C#’s strong typing—but the instances need not all be of the same concrete type. Because of the advantages of polymorphism, you can treat all implementations of an interface as instances of that interface. In the example shown in Listing 5-15, the CompositeComponent instances provided are of different types, further enhancing this pattern’s utility.

LISTING 5-15 Instances provided to the composite can be of different types.


public class SecondTypeOfLeaf : IComponent
{
public void Something()
{

}
}
// . . .
public class AThirdLeafType : IComponent
{
public void Something()
{

}
}
// . . .
public void AlternativeComposite()
{
var composite = new CompositeComponent();
composite.AddComponent(new Leaf());
composite.AddComponent(new SecondTypeOfLeaf());
composite.AddComponent(new AThirdLeafType());

component = composite;
composite.Something();
}


Taking this pattern to its logical conclusion, you can even pass in one or more instances of the CompositeComponent interface to the Add method, forming a chain of composite instances in a hierarchical tree structure.


Where should the composite live?

Chapter 2 introduced the Entourage anti-pattern, which states that implementations should not live in the same assemblies as their interfaces. However, there is an exception to that rule: implementations whose dependencies are a subset of their interface’s dependencies.

Depending on how the composite is implemented, it is likely that no further dependencies will be introduced. If this is true, the assembly in which the interface resides could also include the composite implementation.


In Chapter 2, classes were shown to be modeled as object graphs. That theme continues here, to further demonstrate how the Composite pattern works. In Figure 5-6, the nodes of the graph represent object instances, and the edges represent method calls.

Image

FIGURE 5-6 The object graph notation helps to visualize the runtime structure of the program.

Predicate decorators

The predicate decorator is a useful construct for hiding the conditional execution of code from clients. Listing 5-16 shows an example.

LISTING 5-16 This client will only execute the Something method on even days of the month.


public class DateTester
{
public bool TodayIsAnEvenDayOfTheMonth
{
get
{
return DateTime.Now.Day % 2 == 0;
}
}
}
// . . .
class PredicatedDecoratorExample
{
public PredicatedDecoratorExample(IComponent component)
{
this.component = component;
}

public void Run()
{
DateTester dateTester = new DateTester();
if (dateTester.TodayIsAnEvenDayOfTheMonth)
{
component.Something();
}
}

private readonly IComponent component;
}


The presence of the DateTester class in this example is a dependency that does not belong in this class. The initial temptation is to alter the code toward that of Listing 5-17. However, that is only a partial solution.

LISTING 5-17 An improvement is to require the dependency to be passed into the class.


class PredicatedDecoratorExample
{
public PredicatedDecoratorExample(IComponent component)
{
this.component = component;
}

public void Run(DateTester dateTester)
{
if (dateTester.TodayIsAnEvenDayOfTheMonth)
{
component.Something();
}
}

private readonly IComponent component;
}


You now require a parameter of the Run method, breaking the client’s public interface and burdening its clients with providing an implementation of the DateTester class. By using the Decorator pattern, you are able to keep the client’s interface the same, yet retain the conditional-execution functionality. Listing 5-18 proves that this is not too good to be true.

LISTING 5-18 The predicate decoration contains the dependency, and the client is much cleaner.


public class PredicatedComponent : IComponent
{
public PredicatedComponent(IComponent decoratedComponent, DateTester dateTester)
{
this.decoratedComponent = decoratedComponent;
this.dateTester = dateTester;
}

public void Something()
{
if(dateTester.TodayIsAnEvenDayOfTheMonth)
{
decoratedComponent.Something();
}
}

private readonly IComponent decoratedComponent;
private readonly DateTester dateTester;
}
// . . .
class PredicatedDecoratorExample
{
public PredicatedDecoratorExample(IComponent component)
{
this.component = component;
}

public void Run()
{
component.Something();
}

private readonly IComponent component;
}


Note that this listing has added conditional branching to the code without modifying either the client code or the original implementing class. Also, this example has accepted the DateTester class as a dependency, but you could take this one step further by defining your own predicate interface for handling this scenario generically. After a few changes, the code looks like Listing 5-19.

LISTING 5-19 Defining a dedicated IPredicate interface makes the solution more general.


public interface IPredicate
{
bool Test();
}
// . . .
public class PredicatedComponent : IComponent
{
public PredicatedComponent(IComponent decoratedComponent, IPredicate predicate)
{
this.decoratedComponent = decoratedComponent;
this.predicate = predicate;
}

public void Something()
{
if (predicate.Test())
{
decoratedComponent.Something();
}
}

private readonly IComponent decoratedComponent;
private readonly IPredicate predicate;
}
// . . .
public class TodayIsAnEvenDayOfTheMonthPredicate : IPredicate
{
public TodayIsAnEvenDayOfTheMonthPredicate(DateTester dateTester)
{
this.dateTester = dateTester;
}

public bool Test()
{
return dateTester.TodayIsAnEvenDayOfTheMonth;
}

private readonly DateTester dateTester;
}


The TodayIsAnEvenDayOfTheMonthPredicate class converts the original dependent class, DateTester, to that of an IPredicate. This is an example of the Adapter pattern that was discussed earlier, in the “Refactoring for abstraction” section.


Image Note

The .NET Framework, as of version 2.0, contains a Predicate<T> delegate, which models a predicate that accepts a single, generic parameter as context. I did not choose the Predicate<T> delegate for this example for two reasons: First, no context needs to be provided, because the original conditional test accepted no arguments. However, I could have used a Func<bool> delegate to model a context-free predicate, which brings me to the second reason: delegates are not as versatile as interfaces. By modeling an IPredicate, I will be able to decorate that interface just the same as any other in the future. In other words, I have defined another extension point that is infinitely decoratable.


Branching decorators

You can extend the predicate decorator further by accepting a decorated instance of the interface to execute something on the false branch of the conditional test, as shown in Listing 5-20.

LISTING 5-20 The branching decorator accepts two components and a predicate.


public class BranchedComponent : IComponent
{
public BranchedComponent(IComponent trueComponent, IComponent falseComponent,
IPredicate predicate)
{
this.trueComponent = trueComponent;
this.falseComponent = falseComponent;
this.predicate = predicate;
}

public void Something()
{
if (predicate.Test())
{
trueComponent.Something();
}
else
{
falseComponent.Something();
}
}

private readonly IComponent trueComponent;
private readonly IComponent falseComponent;
private readonly IPredicate predicate;
}


Whenever the predicate is tested, if it returns true, you call the equivalent interface method on the trueComponent instance. If it returns false, you instead call the interface method on the falseComponent instance.

Lazy decorators

The lazy decorator allows clients to be provided with a reference to an interface that will not be instantiated until its first use. Typically, and erroneously, clients are made aware of the presence of a lazy instance because a Lazy<T> is passed to them, as in Listing 5-21.

LISTING 5-21 This client has been given a Lazy<T>.


public class ComponentClient
{
public ComponentClient(Lazy<IComponent> component)
{
this.component = component;
}

public void Run()
{
component.Value.Something();
}

private readonly Lazy<IComponent> component;
}


This client has no option but to accept that all instances of IComponent that it is provided with will be lazy. However, if you return to a more standard use of the interface, you can create a lazy decorator that prevents the client from knowing that it is dealing with a Lazy<T>, and allows some ComponentClient objects to accept IComponent instances that are not lazy. Listing 5-22 shows this decorator.

LISTING 5-22 LazyComponent implements a lazily instantiated IComponent, but ComponentClient is unaware of this.


public class LazyComponent : IComponent
{
public LazyComponent(Lazy<IComponent> lazyComponent)
{
this.lazyComponent = lazyComponent;
}

public void Something()
{
lazyComponent.Value.Something();
}

private readonly Lazy<IComponent> lazyComponent;
}
// . . .
public class ComponentClient
{

public ComponentClient(IComponent component)
{
this.component = component;
}

public void Run()
{
component.Something();
}

private readonly IComponent component;
}


Logging decorators

Listing 5-23 shows a common pattern that occurs whenever code contains extensive logging. The logging code becomes ubiquitous throughout the application, and the signal-to-noise ratio suffers.

LISTING 5-23 Logging code clouds the intent of methods.


public class ConcreteCalculator : ICalculator
{
public int Add(int x, int y)
{
Console.WriteLine("Add(x={0}, y={1})", x, y);

var addition = x + y;

Console.WriteLine("result={0}", addition);

return addition;
}
}


Instead of proliferating the logging code throughout the application, you can limit it to one assembly that implements logging decorators, as shown in Listing 5-24.

LISTING 5-24 Logging decorators factor out the logging code, simplifying the main implementation.


public class LoggingCalculator : ICalculator
{
public LoggingCalculator(ICalculator calculator)
{
this.calculator = calculator;
}

public int Add(int x, int y)
{
Console.WriteLine("Add(x={0}, y={1})", x, y);

var result = calculator.Add(x, y);

Console.WriteLine("result={0}", result);

return result;
}

private readonly ICalculator calculator;
}
// . . .
public class ConcreteCalculator : ICalculator
{
public int Add(int x, int y)
{
return x + y;
}
}


Clients of the ICalculator interface will pass in various parameters, and some of the methods themselves will return values, too. Because the LoggingCalculator is in a position to intercept both of these artifacts, it can interrogate them directly. There are limitations to using logging decorators that should be considered. First, any private state contained in the decorated class remains unavailable to the logging decorator, which cannot access this state and write it to a log. Second, a logging decorator would need to be created for every interface in the application—a significant undertaking. For something so common, logging is better implemented as a logging aspect. Aspect-oriented programming (AOP) was covered in Chapter 2.

Profiling decorators

One of the major reasons to choose the .NET Framework as the target platform for developing an application is that it lends itself well to Rapid Application Development (RAD). A working application can be developed in a far shorter time frame than in a lower-level language like, for example, C++. This is for several reasons, including the .NET Framework’s automatic memory management, the rich and varied list of libraries that can be used, and the .NET Framework itself. C# is often deemed fast at development time but slow at run time. C++, on the other hand, is considered to be slow at development time and fast at run time.

Although the .NET Framework can also be fast, bottlenecks do occur. How can you tell which part of the code is slow? By profiling the methods of the application, you gather statistics on which parts of the code are slower than others. See the code in Listing 5-25.

LISTING 5-25 This code is (intentionally and artificially) slow.


public class SlowComponent : IComponent
{
public SlowComponent()
{
random = new Random((int)DateTime.Now.Ticks);
}

public void Something()
{
for(var i = 0; i<100; ++i)
{
Thread.Sleep(random.Next(i) * 10);
}
};

private readonly Random random
}


The component’s Something() method in this example is slow. Slow and fast, of course, mean different things to different people at different times. In this case, a slow method is defined as one that takes one second or more to execute. How can you tell that a method is slow? You can time the method to find out how long it took to execute from start to finish, much like in Listing 5-26.

LISTING 5-26 The System.Diagnostics.Stopwatch class can time how long a method takes to execute.


public class SlowComponent : IComponent
{
public SlowComponent()
{
random = new Random((int)DateTime.Now.Ticks);
stopwatch = new Stopwatch();
}

public void Something()
{
stopwatch.Start();
for(var i = 0; i<100; ++i)
{
System.Threading.Thread.Sleep(random.Next(i) * 10);
}
stopwatch.Stop();
Console.WriteLine("The method took {0} seconds to complete",
stopwatch.ElapsedMilliseconds / 1000);
}

private readonly Random random;
private readonly Stopwatch;
}


Here the Stopwatch class from the System.Diagnostics assembly is used to time each method from start to finish. Note that the Something method in the class starts the stopwatch on entry and then stops it on exit.

Of course, this can be factored out into a profiling decorator. The interface as a whole is decorated and, before delegating to the decorated instance, you start the stopwatch. When the delegated method returns, you stop the stopwatch before returning to the calling client. The stopwatch decorator code is shown in Listing 5-27.

LISTING 5-27 The profiling decorator code.


public class ProfilingComponent : IComponent
{
public ProfilingComponent(IComponent decoratedComponent)
{
this.decoratedComponent = decoratedComponent;
stopwatch = new Stopwatch();
}

public void Something()
{
stopwatch.Start();
decoratedComponent.Something();
stopwatch.Stop();
Console.WriteLine("The method took {0} seconds to complete",
stopwatch.ElapsedMilliseconds / 1000);
}

private readonly IComponent decoratedComponent;
private readonly Stopwatch stopwatch;
}


There is one further change that you could make to the ProfilingComponent class: make it transparently log the profiling. First, you need to factor out the stopwatch code behind an interface, so that you can provide multiple implementations, including decorators. This is a common first step when refactoring toward a better separation of responsibilities. Listing 5-28 shows this intermediate step.

LISTING 5-28 Before you can implement a decorator, you must replace concrete implementations with interfaces.


public class ProfilingComponent : IComponent
{
public ProfilingComponent(IComponent decoratedComponent, IStopwatch stopwatch)
{
this.decoratedComponent = decoratedComponent;
this.stopwatch = stopwatch;
}

public void Something()
{
stopwatch.Start();
decoratedComponent.Something();
var elapsedMilliseconds = stopwatch.Stop();
Console.WriteLine("The method took {0} seconds to complete", elapsedMilliseconds /
1000);
}

private readonly IComponent decoratedComponent;
private readonly IStopwatch stopwatch;
}


Now that the ProfilingComponent class does not depend directly on the System.Diagnostics.Stopwatch class, you can vary the implementation of the IStopwatch class. A LoggingStopwatch decorator is created, as shown in Listing 5-29, to enhance any furtherIStopwatch implementations with logging facilities.

LISTING 5-29 The LoggingStopwatch decorator is an IStopwatch implementation that logs and delegates.


public class LoggingStopwatch : IStopwatch
{
public LoggingStopwatch(IStopwatch decoratedStopwatch)
{
this.decoratedStopwatch = decoratedStopwatch;
}

public void Start()
{
decoratedStopwatch.Start();
Console.WriteLine("Stopwatch started...");
}

public long Stop()
{
var elapsedMilliseconds = decoratedStopwatch.Stop();
Console.WriteLine("Stopwatch stopped after {0} seconds",
TimeSpan.FromMilliseconds(elapsedMilliseconds).TotalSeconds);
return elapsedMilliseconds;
}

private readonly IStopwatch decoratedStopwatch;
}


Of course, you need a non-decorator implementation of the IStopwatch interface—one that acts as a real stopwatch. This is just a case of delegating to the .NET Framework’s System.Diagnostics.Stopwatch class, as Listing 5-30 shows.

LISTING 5-30 The primary IStopwatch implementation uses the Stopwatch class.


public class StopwatchAdapter : IStopwatch
{
public StopwatchAdapter(Stopwatch stopwatch)
{
this.stopwatch = stopwatch;
}

public void Start()
{
stopwatch.Start();
}

public long Stop()
{
stopwatch.Stop();
var elapsedMilliseconds = stopwatch.ElapsedMilliseconds;
stopwatch.Reset();
return elapsedMilliseconds;
}

private readonly Stopwatch stopwatch;
}


Note that you could have chosen to implement IStopwatch as a subclass of the System.Diagnostics.Stopwatch class and used the existing Start and Stop methods. However, the Start method acts to resume functionality when a stopwatch is stopped, but what you need to do is to call Reset after you call Stop, and retrieve the ElapsedMilliseconds property value in between. This is another example of the Adapter pattern.

Asynchronous decorators

Asynchronous methods are those that run on a different thread than the client. This is useful when a method takes a long time to execute because, during synchronous execution, the client is blocked while waiting for a called method to return. In a desktop application using Windows Presentation Foundation (WPF) and the Model-View-ViewModel (MVVM) pattern, for example, the ViewModels are bound to the View, and any commands that are executed are handled synchronously by those ViewModels on the user interface thread. In practice, this means that a long-running command will block the user interface from executing for as long as the command takes to finish its work. Listing 5-31 shows a snippet of this behavior.

LISTING 5-31 Commands handled on the UI thread will block it, making the UI unresponsive.


public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel(IComponent component)
{
this.component = component;
calculateCommand = new RelayCommand(Calculate);
}

public string Result
{
get
{
return result;
}
private set
{
if (result != value)
{
result = value;
PropertyChanged(this, new PropertyChangedEventArgs("Result"));
}
}
}

public ICommand CalculateCommand
{
get
{
return calculateCommand;
}
}

public event PropertyChangedEventHandler PropertyChanged = delegate { };

private void Calculate(object parameter)
{
Result = "Processing...";
component.Process();
Result = "Finished!";
}

private string result;
private IComponent component;
private RelayCommand calculateCommand;
}


By creating an asynchronous decorator, you can instruct the called method to execute on a separate thread. This can be accomplished by delegating the work to a Task class, which becomes a dependency of your decorator, as Listing 5-32 shows.

LISTING 5-32 An asynchronous decorator for WPF that uses the Dispatcher class.


public class AsyncComponent : IComponent
{
public AsyncComponent(IComponent decoratedComponent)
{
this.decoratedComponent = decoratedComponent;
}

public void Process()
{
Task.Run((Action)decoratedComponent.Process);
}

private readonly IComponent decoratedComponent;
}


There is a problem with the AsyncComponent class: its dependency on the Task class is implicit, meaning that it is hard to test this class. Unit testing code with static dependencies is difficult, so you would be better off replacing this skyhook with a crane.

The limitations of asynchronous decorators

Not all conceivable methods can make use of the Decorator pattern to create asynchronous versions that clients do not know about. In fact, the only asynchronous methods to which this approach is applicable are fire-and-forget methods.

A fire-and-forget method has no return value, and clients do not need to know when such a method returns. When it is implemented as an asynchronous decorator, clients cannot know when a method call truly was completed because the call returns immediately—while the real work being performed is probably still in progress.

Request-response methods are common data-retrieval methods that are often very usefully implemented asynchronously, because they tend to take a while and block the UI thread. Clients need to know that the method is asynchronous, so that they can be coded explicitly to accept a callback when the asynchronous method is complete. Therefore, request-response methods cannot be implemented by using asynchronous decorators.

Decorating properties and events

So far, you have learned how to decorate the methods of an interface, but what about properties and events? Both of those syntactic elements can also be decorated, as long as you do not use auto-properties or auto-events: you need to explicitly define both in order to decorate them properly.

Listing 5-33 shows the manual creation of a property, but rather than having a backing field, for both the getter and the setter this code delegates to the decorated instance of the interface.

LISTING 5-33 Properties can also use the Decorator pattern, just like methods.


public class ComponentDecorator : IComponent
{
public ComponentDecorator(IComponent decoratedComponent)
{
this.decoratedComponent = decoratedComponent;
}

public string Property
{
get
{
// We can do some mutation here after retrieving the value
return decoratedComponent.Property;
}
set
{
// And/or here, before we set the value
decoratedComponent.Property = value;
}
}

private readonly IComponent decoratedComponent;
}


Listing 5-34 shows the manual creation of an event, but rather than having a backing field, for both the adder and remover this code delegates to the decorated instance of the interface.

LISTING 5-34 Events can also use the Decorator pattern, just like methods.


public class ComponentDecorator : IComponent
{
public ComponentDecorator(IComponent decoratedComponent)
{
this.decoratedComponent = decoratedComponent;
}

public event EventHandler Event
{
add
{
// We can do something here, when the event handler is registered
decoratedComponent.Event += value;
}
remove
{
// And/or here, when the event handler is deregistered
decoratedComponent.Event -= value;
}
}


private readonly IComponent decoratedComponent;
}


Using the Strategy pattern instead of switch

To understand when the Strategy pattern is best applied, you can look at instances of conditional branching. Whenever you use switch statements, you can use the Strategy pattern to simplify the client so that it delegates complexity to dependent interfaces. Listing 5-35 shows an example of a switch statement that could be replaced with the Strategy pattern.

LISTING 5-35 This method uses a switch statement, but the Strategy pattern would be more adaptive.


public class OnlineCart
{
public void CheckOut(PaymentType paymentType)
{
switch(paymentType)
{
case PaymentType.CreditCard:
ProcessCreditCardPayment();
break;
case PaymentType.Paypal:
ProcessPaypalPayment();
break;
case PaymentType.GoogleCheckout:
ProcessGooglePayment();
break;
case PaymentType.AmazonPayments:
ProcessAmazonPayment();
break;
}
}

private void ProcessCreditCardPayment()
{
Console.WriteLine("Credit card payment chosen");
}

private void ProcessPaypalPayment()
{
Console.WriteLine("Paypal payment chosen");
}

private void ProcessGooglePayment()
{
Console.WriteLine("Google payment chosen");
}

private void ProcessAmazonPayment()
{
Console.WriteLine("Amazon payment chosen");
}
}


In the example, for each case of the switch statement, the behavior of the class changes. This presents a code maintenance problem because the addition of a new case option requires a change to this class. If, instead, you replace each case statement with a new implementation of an interface, further implementations can be created to encapsulate new functionality—and the client would not need to change. This is shown in Listing 5-36.

LISTING 5-36 After the switch statement is replaced, the client looks far more adaptive to change.


public class OnlineCart
{
public OnlineCart()
{
paymentStrategies = new Dictionary<PaymentType, IPaymentStrategy>();
paymentStrategies.Add(PaymentType.CreditCard, new PaypalPaymentStrategy());
paymentStrategies.Add(PaymentType.GoogleCheckout, new
GoogleCheckoutPaymentStrategy());
paymentStrategies.Add(PaymentType.AmazonPayments, new
AmazonPaymentsPaymentStrategy());
paymentStrategies.Add(PaymentType.Paypal, new PaypalPaymentStrategy());
}

public void CheckOut(PaymentType paymentType)
{
paymentStrategies[paymentType].ProcessPayment();
}

private IDictionary<PaymentType, IPaymentStrategy> paymentStrategies;
}


In the tradition of object-oriented programming, this code has objectified the different payment types into various classes, each of which implements the IPaymentStrategy interface. In this example, the OnlineCart class has a private dictionary that maps each value of thePaymentType enumeration onto an instance of the strategy interface. This simplifies the CheckOut method significantly. The switch statement has been removed and, along with it, the knowledge of how to process each different type of payment. The OnlineCart class did not need to know how to process the payments, which could vary greatly and introduce many unnecessary dependencies on this class. Now its job is to select the right payment strategy and delegate the processing to it.

There is still a maintenance burden here for adding new payment strategy implementations. If you want to add support for WePay, for example, the constructor will need to be updated to map the new WePayPaymentStrategy class to the associated WePay enumeration value.

Conclusion

The single responsibility principle has a hugely positive impact on the adaptability of code. Compared to equivalent code that does not adhere to the principle, SRP-compliant code leads to a greater number of classes that are smaller and more directed in scope. Where there would otherwise have been a single class or suite of classes with interdependencies and a confusion of responsibility, the SRP introduces order and clarity.

The SRP is primarily achieved through abstracting code behind interfaces and delegating responsibility for unrelated functionality to whichever implementation happens to be behind the interface at run time. Some design patterns are excellent at supporting efforts to strictly regiment the SRP—in particular, the Adapter pattern and the Decorator pattern. The former enables much of your code to maintain first-party references to interfaces under your direct control, although in reality utilizing a third-party library. The latter can be applied whenever some of a class’s functionality needs to be removed but it is too tightly coupled with the intent of the class to stand alone.

What this chapter did not cover is how all of these classes are orchestrated at run time. Passing interfaces into constructors was taken for granted in this chapter, but Chapter 9 describes a variety of ways in which this can be accomplished.