The Decorator Pattern - Part 1: DRY (ExceptionLoggerCodeFileReader
)
At fist glance, the decorator pattern is nice in that it gives the appearance of very easy adherence to the open/closed principle (OCP) and the single responsibility principle (SRP), but it does have its limitations.
The most obvious one is that any decorator is inevitably tightly coupled to its interface. If you find yourself writing code in a decorator that actually doesn't really have anything to do with the specific interface it's implementing, that should set off alarm bells because you're inevitably setting yourself up for a future violation of another important programming principle- Don't Repeat Yourself (DRY).
Your ExceptionLoggerCodeFileReader
is a great example of this. The name is the first sign of this- instead of being one thing, that's two totally unrelated things stuck together, an ExceptionLogger
and a CodeFileReader
. Does exception logging have anything to do with reading code files? And if not then why is an exception logger being forced to implement the ICodeFileReader
interface?
Nothing in that class is specific to the interface it implements apart from the method that is wrapped in a try/catch. So the next time you write a Foo
class, and want an ExceptionLoggerFoo
decorator for that class, you will find that almost all of the code in ExceptionLoggerCodeFileReader
will need to be duplicated, hence the DRY violation.
The Decorator Pattern - Part 2: Separate and Dynamic Responsibilities (FileHeaderValidatorCodeFileReader
)
In your answer, you quote a nice list of situations in which the decorator pattern is appropriate. What's interesting is what is and isn't on this list, compared to why you've employed it in this situation.
On the list:
- Add responsibilities dynamically
- Add responsibilities which can be withdrawn
Not on the list:
- Write code which adheres to the open/close principle.
So, let's take FileHeaderValidatorCodeFileReader
as an example here. Is this a responsibility that only sometimes applies to reading code files? Is it a function that you want to add or withdraw based on some condition that can't be known at compile-time? I don't know the full workings of your program, but from how you've described it, I understand the answer to that to be no.
So given that, you now actually have a single responsibility (reading a code file, which must involve validating the header) split into three places: CodeFileReader
, FileHeaderValidatorCodeFileReader
and wherever you compose your objects together, which in the case of an IoC container is probably all the way up at the application route. The logic for when the file header validation is applied (which in this case, as far as I can tell, is simply that it is always applied) has to live far away from the class which should actually be responsible for it, instead sitting up in the object graph composition route.
The Alternatives - Part 1: Aspect-Oriented Programming (ExceptionLoggerCodeFileReader
)
So what about OCP? Because despite any other problems, it's still the case that the decorator pattern does give very good adherence to that pattern. Can that be maintained without using decorators?
First, to deal with the ExceptionLoggerCodeFileReader
. This kind of logging is generally difficult to do in a way that adheres to good design principles. A decorator allows it- and any dependencies it brings like your ILogger
- to at least be separated out from the rest of the body of the class, but with or without a decorator, you will inevitably find yourself repeating a lot of code in repeatedly writing the try...catch{log,rethrow} pattern.
The reason for this is that exception logging is a cross-cutting concern- a concern which is involved with many other concerns in a way which is difficult to separate out. And this is precisely the key use case for Aspect-oriented programming.
When working with certain frameworks like ASP.NET's MVC, there's some built in support for aspect-oriented programming (in that case, using action filter attributes). But the general way to apply it is through an IoC container which supports interceptors. I won't go too much into the implementation details because that will vary from container to container, but to give an overview, here's an example using Ninject's Interception extension:
public abstract class BaseInterceptor : IInterceptor
{
public void Intercept(IInvocation invocation)
{
OnStart();
try
{
invocation.Proceed(); //The actual method call is done here
OnSuccess();
}
catch(Exception ex)
{
OnError(ex);
}
finally
{
OnExit();
}
}
protected abstract void OnStart();
protected abstract void OnSuccess();
protected abstract void OnError(Exception exception);
protected abstract void OnExit();
}
This can now be bound (I won't go into the details of how) to any class/method, so that when the class is retrieved from the container and the method called, that interceptor is used. You don't need to use that exact format of course, but those are the basic places at which you can perform some action like logging.
The ExceptionLoggerCodeFileReader
can now be replaced with an ExceptionInterceptor
, which would essentially be an implementation of the OnError
method above. Notice that it's completely decoupled from the actual method that's being called- no knowledge of the method, class or interface corresponding to the invocation. So now this cross-cutting concern, instead of having to be sprayed all over your project in repetitive decorators, has its own single class which is appropriate for what it's trying to do. This is a specific example, but because of the decorator's coupling to the interface, AOP is pretty much always preferred over the decorator pattern for cross-cutting concerns.
The Alternatives - Part 2: Perspective Shift (FileHeaderValidatorCodeFileReader
)
That's all well and good for exception logging, but file header validation certainly isn't a cross-cutting concern. And to work out how to best deal with responsibilities like that, I'm going to suggest a bit of a perspective shift.
To start, let's look at the only public member on CodeFileReader
:
public string[] ReadFile(string path)
{
return File.ReadAllLines(path);
}
And here's the initial shift in perspective: don't call that a partially-implemented CodeFileReader
. Call it a fully implemented FileReader
.
So what does that achieve? Well, it means the class you just wrote is definitely going to adhere to the OCP- you can be as confident as it's ever reasonable to expect to be that you're not going to have to go in and change- or even decorate- that class. Fine. But it also seems like an exercise in rug-sweeping: what happens when you need some code somewhere to handle additional responsibilities relating to reading code files?
Well, here's where the second perspective shift comes in. You should not be trying to create a high-level design in advance to meet all possible future requirements. As an example, let's take the FileHeaderValidatorCodeFileReader
again. Now imagine I gave you the following new requirement:
Entire source control repositories will be processed by this application, so it will be the application's responsibility to recognise and avoid attempting to process files which are not VBA.
(Potentially the best way to deal with this would be by looking at file extension, but since this is only supposed to be an illustrative example and since we're already talking about a file header validator, I'll work with the somewhat contrived assumption that you're additionally required to treat header validation as the only reliable way of determining whether or not a file is valid code which needs to be processed)
So, this causes you two big problems regarding the OCP:
- Your
FileHeaderValidatorCodeFileReader
treats an invalid header as exceptional, and this can't be fixed without modifications to the class itself
- This will require changes that can't be accomodated without also making a modification to the consumer of this class. Your main options are:
- Change your method signature to a "
Try...
" pattern, like bool TryReadFile(string path, out string[] fileLines);
- Keep the existing method signature the same but add a
CheckValid
method.
- Return
null
for invalid files.
The relative merits of those options don't really matter- the first two would mean an interface change and the last would mean any consuming code would need to add null checking logic. None of them comply with OCP.
In trying to give yourself better adherence to OCP, with this particular example requirement you'd actually have locked yourself into a design that would need bigger modifications. That particular requirement was just the first one off the top of my head, it may not have been realistic, or perhaps you can think of workarounds, but in general you should work on the assumption that developers are bad at guessing future requirements.
So once you realise that header validation of some kind was a requirement, what should you write? Something like:
public class CodeFileHeaderValidator : IFileValidator
{
public bool IsValid(string[] fileContent)
{
//Your actual implementation goes here
}
}
And at that point, you're done. Again, what you've written now definitely adheres to OCP- there's no situation in which you'll have to change that class. Unknown requirements are kept in the realm of code you haven't written yet, and design you're not locked into:
- Will there be more validation rules that should be pulled together into a compound validator (like in your answer)?
- Will a file that fails validation be an exceptional situation or not?
- Will this always be called at the same time as the file is first loaded, or might it be required at other times?
- Will the file contents always be read from disk or may there be other data sources like a source control API or database which provide code files which require the same validation?
- Will this- and other validation rules if there are any- always be required or will there be some condition determining which validation rules are relevant, and if the latter, which class's responsibility will it be to decide which conditions are relevant?
The decorator pattern has opinions about almost all of those questions (and probably more I haven't thought of!), whereas writing a minimal implementation, decoupled from any interface like ICodeFileReader
does not. That's adherence to OCP- writing code that won't have to change no matter what the answers to those above questions turn out to be.
Pulling it together
At some point, you will have to write code that handles all the responsibilities associated with reading code files. Hopefully the examples in the previous section have convinced you that trying to come up with a design which can handle arbitrary changes in responsibilities is impossible. But by having a design that:
- is loosely coupled, with nothing depending on interfaces or classes it shouldn't have to care about,
- uses dependency injection where appropriate to allow concrete implementations to be swapped without requiring modification to the classes using them,
- makes as few assumptions about unknown requirements as possible, and when it does make assumptions, chooses sensible and minimal over trying to be all-encompasing,
- adheres to the SRP
You will minimize the likelihood of having to make changes, and minimize the magnitude and risk of the changes you have to make.
With just the information from the OP, I wouldn't create an actual CodeFileReader
class, because all I know is that code files need to be read and they need to be validated, and without answers to some of the previous questions, I would have no idea whether it's appropriate to group those two pieces of functionality into a file reader class. Potentially it might turn out to be more appropriate for the validator to be used by the code that calls IFileReader.ReadFile
, rather than an actual IFileReader
implementation. But if I did find out more and that seemed appropriate, and it turned out that invalid files were exceptional, the design would probably be:
public class ValidatingFileReader : FileReader
{
private IFileValidator _fileValidator;
public ValidatingFileReader(IFileValidator fileValidator)
{
_fileValidator = fileValidator;
}
public override string[] ReadFile(string path)
{
var content = base.ReadFile(path);
if(!_fileValidator.IsValid(content))
//Throw some kind of exception here
return content;
}
}
(Logging would be left to interceptors as described previously.)
Is it impossible that this class, or code consuming it, would have to change? No. But by calling it a ValidatingFileReader
rather than a CodeFileReader
and adhering to the SRP, I've made it more likely that a new requirement would be an extension of the class rather than a modification. By implementing an interface, I've made it more likely that if this class needs to be extended or swapped out, the consuming code won't need to be modified. By completely decoupling the validation implementation from this class, I've ensured that any change in validation rules will almost certainly not impact this class. And I believe that that's about as close as you should reasonably strive to get to ideally OCP-compliant code.