- Software Architecture: Meta and SOLID Principles in C#
- Introduction to SOLID Principles
- SOLID Principles: Single Responsibility Principle (SRP)
- SOLID Principles: The Open/Closed Principle (OCP)
- SOLID Principles: Liskov Substitution Principle (LSP) In Practice
- SOLID Principles: Interface Segregation Principle (ISP)
In this part of the SOLID series, we’re going to revisit the ISP or Interface Segregation Principle. Yep, this is a very long article. If you want an easier way of learning SOLID principles in depth, then I would recommend you taking my SOLID tutorial just for 10.99$.
Interface Segregation Principle Definition
Before giving a definition, I want to say a couple of words about what we mean here by the word “interface”. Of course, “interface” is a reserved keyword in C# which allows to declare a non-implementable type consisting of member signatures. Such a construction defines an API, a shape which has to be implemented by inheritors of that interface.
At the same time, it’s not necessary to implement an interface to expose an interface. What I mean is that classes which don’t implement any interfaces have their own interfaces comprised of publicly visible members. The set of public members of a class represents the interface of that class. So, simply put, an interface is what clients see and use.
A great simple definition of the Interface Segregation Principle was given in the book you have already heard of, “Agile Principles, Patterns, and Practices in C#”. So, the definition is:
“The Interface Segregation Principle states that Clients should not be forced to depend on methods they do not use.”
A simple conclusion that we can draw from this definition is that you should prefer small, cohesive interfaces to “fat” interfaces. Just in case, I’ll remind you that “cohesive” means that all the API members are logically related to each other.
It’s always useful to look at a picture which illustrates the problem. And of course, this is also a chance to get some fun. Look at this illustration.
This funny monster wants to eat and that’s why it says that if IRequireFood, then it means that I want to Eat some food, not light candelabra or layout cutlery. This obviously implies that if you have a public interface named IRequireFood, it would be silly to have methods like LaunchAircraft or KillCharacter exposed by that interface.
A bit of History
Here is an interesting historical note about the ISP. I’m pretty sure that ISP was first used long ago before Robert Martin, but the first public formulation belongs to Robert C. Martin. He applied the ISP first time while consulting for Xerox. Xerox had created a new printer system that could perform a variety of tasks such as stapling and faxing. The software for this system was created from the ground up. As the software grew, making modifications became more and more difficult so that even the smallest change would take a redeployment cycle of an hour, which made development nearly impossible. The redeployment cycle took so much time because at that time there were no C# or Java, these languages compile very fast. What we can’t say about C++ for example. Bad design of a C++ program can lead to significant compilation time. The consequences are dramatic.
Let’ get back to the story. The design problem was that a single Job class was used by almost all of the tasks.
Whenever a print job or a stapling job needed to be performed, a call was made to the Job class. This resulted in a ‘fat’ class with multitudes of methods specific to a variety of different clients. Because of this design, a staple job would know about all the methods of the print job, even though there was no use for them.
To solve the problem, Uncle Bob came up with an idea which is called “Interface Segregation Principle” today. Uncle Bob created an interface layer between the Job class and its clients using the Dependency Inversion Principle (that we’re going to cover later).
Instead of having one large Job class, a Staple Job interface or a Print Job interface was created that would be used by the Staple or Print classes, respectively, calling methods of the Job class. Therefore, one interface was created for each job type, which were all implemented by the Job class.
So, Interface Segregation Principle violation results in classes that depend on things they do not need, increasing coupling and reducing flexibility and maintainability.
Interface Segregation Principle Violation in C#
I want to show you a real case from my practice which was related to the problem of interface segregation. I’ve been working with devices a lot and once I faced the following case.
Before continuing I want to say that I like to give examples to students which reflect the whole picture of a real-world case, so I’ll describe all the details related to the problem because I think that this helps to understand the material much better.
So, on the top level, we have a monolithic WPF application which works in POS terminals. Those terminals allow people to buy tickets on suburban trains. That application allows users to buy tickets via credit cards. Thus, the application has to work with bank terminals somehow. Due to some business reasons, there were several bank terminal models integrated to POS-terminals, so all of them had to be supported. The interoperation with bank terminals is not direct, bank terminal producers provide their own applications through which our application can work with their bank terminals. Their applications are implemented as COM-servers. If you don’t know what is a COM-server, just think of it as of a standalone executable service. The following diagram reflects the flow of operations:
Let’s look at the code:
[code lang=”csharp”]
public interface IBankTerminal
{
void Start();
void Stop();
void Ping();
void BankHostTest();
void Purchase(decimal amount, string checkId);
void CancelPayment(string checkId, decimal amount);
void InterruptTransaction();
event EventHandler<PaymentOperationCompletedEventArgs> PaymentCompleted;
event EventHandler<PaymentOperationCompletedEventArgs> CancellationCompleted;
event EventHandler<TransactionCompletedEventArgs> TransactionCompleted;
}
[/code]
There is a bare minimum of operations which are supported by any possible bank terminal. That’s why I defined the IBankTerminal interface which reflects the API provided by services which interoperate with bank terminals directly.
Now I have three implementations of the IBankTerminal interface: ZapTerminal, ZonTerminal and PdqTerminal.
[code lang=”csharp”]
public class PdqTerminal : IBankTerminal
{
private PdqTerminalServiceCommunicator _service = new PdqTerminalServiceCommunicator();
public void Start() { }
public void Stop() { }
public void Ping() { }
public void BankHostTest() { }
public void Purchase(decimal amount, string checkId) { }
public void CancelPayment(string checkId, decimal amount) { }
public void InterruptTransaction() { }
public event EventHandler<PaymentOperationCompletedEventArgs> PaymentCompleted;
public event EventHandler<PaymentOperationCompletedEventArgs> CancellationCompleted;
public event EventHandler<TransactionCompletedEventArgs> TransactionCompleted;
}
[/code]
I omitted the real implementation since it would complicate the example too much and there is no meaning to show you the guts and low-level details. I also omitted the code for Zap and Zon terminal which in their turn use corresponding communication objects as well.
And now I’ll describe the problem which arose. The thing is that bank terminals are different. The ZonTerminal is a black-box solution which physically is a box which on its own works with credit card readers. Credit card readers are those devices which accept your card, read the chip or a magnetic stripe and dispense back a card back to you. So, ZonTerminal doesn’t expose API for interoperating with card readers, because the service application automatically sets all the necessary things up. At the same time, the other two bank terminals, “PdqTerminal” and “ZapTerminal” don’t take responsibility for interoperating with card readers automatically. On the contrary, they delegate this responsibility to a client, exposing API for interoperating with card readers.
So, in the first case (ZonTerminal), our application doesn’t need to do anything at all, while in the other two cases, our application has to show a window to POS-terminal maintenance engineers to provide the ability to set up the bank terminal devices properly.
Let’s look at the ViewModel class which is a presenter for that window which allows maintenance engineers to set up the bank terminal devices. The window has four buttons which allow to test if contact or contactless readers are on a certain port and to find them by scanning through all the available ports in the system.
[code lang=”csharp”]
public class CardReadersCommunicatorViewModel
{
public CardReadersCommunicatorViewModel()
{
}
public bool TestContactReaderOnPort(string port)
{
return false;
}
public bool TestNonContactReaderOnPort(string port)
{
return false;
}
public string FindContactReader()
{
return null;
}
public string FindNonContactReader()
{
return null;
}
}
[/code]
We need to pass in the CardReadersCommunicatorViewModel’s constructor a dependency which is capable of testing and searching for card readers on ports. Now ask yourself what would you do to solve the problem?
The straightforward way is to add four method signatures right into the IBankTerminal. Let’s do this and look at what will happen.
[code lang=”csharp”]
public interface IBankTerminal
{
…
bool IsContactReaderOnPort(string comPort);
bool IsNonContactReaderOnPort(string comPort);
string FindContactReader();
string FindNonContactReader();
…
}
[/code]
Ok, since we have three implementers we need to implement the just added API members. Everything will be fine with ZapTerminal and PdqTerminal.
[code lang=”csharp”]
public class PdqTerminal : IBankTerminal
{
private PdqTerminalServiceCommunicator _service = new PdqTerminalServiceCommunicator();
public void Start() { }
public void Stop() { }
public void Ping() { }
public void BankHostTest() { }
public void Purchase(decimal amount, string checkId) { }
public void CancelPayment(string checkId, decimal amount) { }
public void InterruptTransaction() { }
public event EventHandler<PaymentOperationCompletedEventArgs> PaymentCompleted;
public event EventHandler<PaymentOperationCompletedEventArgs> CancellationCompleted;
public event EventHandler<TransactionCompletedEventArgs> TransactionCompleted;
public bool IsContactReaderOnPort(string comPort)
{
return _service.IsContactReaderOnPort(comPort);
}
public bool IsNonContactReaderOnPort(string comPort)
{
return _service.IsNonContactReaderOnPort(comPort);
}
public string FindContactReader()
{
return _service.FindContactReader();
}
public string FindNonContactReader()
{
return _service.FindNonContactReader();
}
}
public class ZapTerminal : IBankTerminal
{
private ZapTerminalServiceCommunicator _service = new ZapTerminalServiceCommunicator();
public void Start() { }
public void Stop() { }
public void Ping() { }
public void BankHostTest() { }
public void Purchase(decimal amount, string checkId) { }
public void CancelPayment(string checkId, decimal amount) { }
public void InterruptTransaction() { }
public event EventHandler<PaymentOperationCompletedEventArgs> PaymentCompleted;
public event EventHandler<PaymentOperationCompletedEventArgs> CancellationCompleted;
public event EventHandler<TransactionCompletedEventArgs> TransactionCompleted;
public bool IsContactReaderOnPort(string comPort)
{
return _service.IsContactReaderOnPort(comPort);
}
public bool IsNonContactReaderOnPort(string comPort)
{
return _service.IsNonContactReaderOnPort(comPort);
}
public string FindContactReader()
{
return _service.FindContactReader();
}
public string FindNonContactReader()
{
return _service.FindNonContactReader();
}
}
[/code]
But what about ZonTerminal? ZonTerminal’s service doesn’t provide an API for communicating with card readers. Seemingly, we need to throw NotSupportedExceptions from the implemented members.
[code lang=”csharp”]
public class ZonTerminal : IBankTerminal
{
private ZonTerminalServiceCommunicator _service = new ZonTerminalServiceCommunicator();
public void Start() { }
public void Stop() { }
public void Ping() { }
public void BankHostTest() { }
public void Purchase(decimal amount, string checkId) { }
public void CancelPayment(string checkId, decimal amount) { }
public void InterruptTransaction() { }
public event EventHandler<PaymentOperationCompletedEventArgs> PaymentCompleted;
public event EventHandler<PaymentOperationCompletedEventArgs> CancellationCompleted;
public event EventHandler<TransactionCompletedEventArgs> TransactionCompleted;
public bool IsContactReaderOnPort(string comPort)
{
throw new NotImplementedException();
}
public bool IsNonContactReaderOnPort(string comPort)
{
throw new NotImplementedException();
}
public string FindContactReader()
{
throw new NotImplementedException();
}
public string FindNonContactReader()
{
throw new NotImplementedException();
}
}
[/code]
Doesn’t this case remind you something? Something we’ve seen previously. Of course, this is a violation of the Liskov Substitution Principle. But what, we’re talking about the Interface Segregation Principle, aren’t we? Yes, we are, the thing is that as I told you earlier, all the principles are related to each other. Sometimes they have hidden relationships. In this particular case, we end up with the LSP violation as a consequence of ISP violation. If we stick with the current solution and request IBankTerminal, we will end up with the following code:
[code lang=”csharp”]
public class CardReadersCommunicatorViewModel
{
private readonly IBankTerminal _bankTerminal;
public CardReadersCommunicatorViewModel(IBankTerminal bankTerminal)
{
_bankTerminal = bankTerminal;
}
public bool TestContactReaderOnPort(string port)
{
return _bankTerminal.IsContactReaderOnPort(port);
}
public bool TestNonContactReaderOnPort(string port)
{
return _bankTerminal.IsNonContactReaderOnPort(port);
}
public string FindContactReader()
{
return _bankTerminal.FindContactReader();
}
public string FindNonContactReader()
{
return _bankTerminal.FindNonContactReader();
}
}
[/code]
we require the IBankTerminal interface as a parameter of the CardReadersCommunicatorViewModel’c constructor, one day, someone will pass the ZonTerminal into the constructor and the end user will get an exception after clicking on the testing or search button in the window.
So, if we want to avoid such unfortunate consequences, we need to acknowledge the fact that the IBankTerminal interface is too fat. It contains excessive API members.
Fixing the ISP Violation
The major and obvious refactoring technique which we usually apply to adhere to the ISP is that we create small isolated interfaces which represent well-defined concrete responsibilities. In this particular case, we need to segregate the responsibility which concerns the interoperating with card readers.
So, I’ll do that.
[code lang=”csharp”]
public interface IBankTerminal
{
void Start();
void Stop();
void Ping();
void BankHostTest();
void Purchase(decimal amount, string checkId);
void CancelPayment(string checkId, decimal amount);
void InterruptTransaction();
event EventHandler<PaymentOperationCompletedEventArgs> PaymentCompleted;
event EventHandler<PaymentOperationCompletedEventArgs> CancellationCompleted;
event EventHandler<TransactionCompletedEventArgs> TransactionCompleted;
}
public interface IReadersCommunicable
{
bool IsContactReaderOnPort(string comPort);
bool IsNonContactReaderOnPort(string comPort);
string FindContactReader();
string FindNonContactReader();
}
[/code]
Now we should implement this new interface on our two models of bank terminal which actually can interoperate with card readers.
[code lang=”csharp”]
public class PdqTerminal : IBankTerminal, IReadersCommunicable
{
private PdqTerminalServiceCommunicator _service = new PdqTerminalServiceCommunicator();
public void Start() { }
public void Stop() { }
public void Ping() { }
public void BankHostTest() { }
public void Purchase(decimal amount, string checkId) { }
public void CancelPayment(string checkId, decimal amount) { }
public void InterruptTransaction() { }
public event EventHandler<PaymentOperationCompletedEventArgs> PaymentCompleted;
public event EventHandler<PaymentOperationCompletedEventArgs> CancellationCompleted;
public event EventHandler<TransactionCompletedEventArgs> TransactionCompleted;
public bool IsContactReaderOnPort(string comPort)
{
return _service.IsContactReaderOnPort(comPort);
}
public bool IsNonContactReaderOnPort(string comPort)
{
return _service.IsNonContactReaderOnPort(comPort);
}
public string FindContactReader()
{
return _service.FindContactReader();
}
public string FindNonContactReader()
{
return _service.FindNonContactReader();
}
}
public class ZapTerminal : IBankTerminal, IReadersCommunicable
{
private ZapTerminalServiceCommunicator _service = new ZapTerminalServiceCommunicator();
public void Start() { }
public void Stop() { }
public void Ping() { }
public void BankHostTest() { }
public void Purchase(decimal amount, string checkId) { }
public void CancelPayment(string checkId, decimal amount) { }
public void InterruptTransaction() { }
public event EventHandler<PaymentOperationCompletedEventArgs> PaymentCompleted;
public event EventHandler<PaymentOperationCompletedEventArgs> CancellationCompleted;
public event EventHandler<TransactionCompletedEventArgs> TransactionCompleted;
public bool IsContactReaderOnPort(string comPort)
{
return _service.IsContactReaderOnPort(comPort);
}
public bool IsNonContactReaderOnPort(string comPort)
{
return _service.IsNonContactReaderOnPort(comPort);
}
public string FindContactReader()
{
return _service.FindContactReader();
}
public string FindNonContactReader()
{
return _service.FindNonContactReader();
}
}
[/code]
Great. Now we can require the ICardReadersCommunicable interface in the ViewModel’s constructor.
[code lang=”csharp”]
public class CardReadersCommunicatorViewModel
{
private readonly IReadersCommunicable _readersCommunicable;
public CardReadersCommunicatorViewModel(IReadersCommunicable readersCommunicable)
{
_readersCommunicable = readersCommunicable;
}
public bool TestContactReaderOnPort(string port)
{
return _readersCommunicable.IsContactReaderOnPort(port);
}
public bool TestNonContactReaderOnPort(string port)
{
return _readersCommunicable.IsNonContactReaderOnPort(port);
}
public string FindContactReader()
{
return _readersCommunicable.FindContactReader();
}
public string FindNonContactReader()
{
return _readersCommunicable.FindNonContactReader();
}
}
[/code]
Great! There is no chance anymore that someone will pass an inappropriate instance which can’t work with card readers. And by the way, now it’s much more understandable what API members to use here, since we have a one-purpose interface here. Now a client of the interfaces should not be bothered by excessive API members thinking of their role. Now everything stands on its shelve.
By applying the ISP, we achieve the low coupling between methods which are different by their meaning and thus we achieve a high cohesion between them in segregated interfaces.
ISP Violation Example 2
In the previous case, the implementer of an interface itself knew too much, it knew what it should not that’s why it could not implement some members appropriately. So, the existence of a problem has already been obvious at the level of interface implementer.
Very often the problem goes deeper when the problem shows itself only at the implementer’s client level. Consider the following example.
[code lang=”csharp”]
[DataContract(Namespace = "")]
public class AppConfig
{
private AppConfig()
{
}
[DataMember(IsRequired = true)]
public string ServerId { get; set; }
[DataMember(IsRequired = true)]
public string ServerIP { get; set; }
[DataMember(IsRequired = true)]
public string ServerPort { get; set; }
[DataMember(IsRequired = true)]
public int LoggingSwitch { get; set; }
[DataMember(IsRequired = true)]
public int AppSkinId { get; set; }
[DataMember(IsRequired = true)]
public decimal Income { get; set; }
[DataMember(IsRequired = true)]
public decimal Outcome { get; set; }
[DataMember(IsRequired = true)]
public decimal TotalRevenue { get; set; }
public static AppConfig Config { get; private set; }
public static void Initialize()
{
using (Stream s = File.OpenRead("config.xml"))
{
Config = (AppConfig) new DataContractSerializer(typeof(AppConfig)).ReadObject(s);
}
}
}
[/code]
We have a configuration class which is implemented as sort of a singleton. Its constructor is closed and it only can be created via the Initialize method which deserializes an XML configuration file.
Not surprisingly we have a class which utilized that configuration file. Here we have the Report class which implements some business logic of reports generation:
[code lang=”csharp”]
public class Report
{
public string Generate()
{
return $"Income:{AppConfig.Config.Income}" + "\n" +
$"Outcome:{AppConfig.Config.Outcome}" + "\n" +
$"Total Revenue:{AppConfig.Config.TotalRevenue}";
}
}
[/code]
Currently it directly depends on the AppConfig class. Because of that, we can’t easily write a unit test for the Report class. What we only can do is to write an integration test which deals with xml configuration file, setting it up properly.
To fix the problem, we can abstract away the configuration class by introducing an interface. Let’s extract an interface:
[code lang=”csharp”]
public interface IAppConfig
{
string ServerId { get; set; }
string ServerIP { get; set; }
string ServerPort { get; set; }
int LoggingSwitch { get; set; }
int AppSkinId { get; set; }
decimal Income { get; set; }
decimal Outcome { get; set; }
decimal TotalRevenue { get; set; }
}
[/code]
Now we can request this interface to be passed in the constructor of the Report class.
[code lang=”csharp”]
public class Report
{
private readonly IAppConfig _appConfig;
public Report(IAppConfig reportsConfig)
{
_appConfig = appConfig;
}
public string Generate()
{
return $"Income:{_appConfig.Income}" + "\n" +
$"Outcome:{_appConfig.Outcome}" + "\n" +
$"Total Revenue:{_appConfig.TotalRevenue}";
}
}
[/code]
Great! Now we can write a unit test.
[code lang=”csharp”]
[TestFixture]
public class ReportTests
{
[Test]
public void Generate_ValidInput_GeneratesReport()
{
IAppConfig appConfig = new TestableAppConfig()
{
AppSkinId = 0,
Income = 10,
LoggingSwitch = 1,
Outcome = 100,
ServerIP = "192.168.0.1",
ServerId = "120888",
ServerPort = "8080",
TotalRevenue = 1000
};
Report sut = new Report(appConfig);
string report = sut.Generate();
Assert.AreEqual(
$"Income:10" + "\n" +
$"Outcome:100" + "\n" +
$"Total Revenue:1000",
report);
}
}
public class TestableAppConfig : IAppConfig
{
public string ServerId { get; set; }
public string ServerIP { get; set; }
public string ServerPort { get; set; }
public int LoggingSwitch { get; set; }
public int AppSkinId { get; set; }
public decimal Income { get; set; }
public decimal Outcome { get; set; }
public decimal TotalRevenue { get; set; }
}
[/code]
What’s wrong with this test? I set up all the properties, even those which are not required by the internal logic of the Report class. You could ask me why I indeed set all the properties, I could set only those which are required. Partly, you’re right, this would solve the problem of excessive code.
But imagine that there are tons of properties like in the real-world configuration file. Some of them even have similar names. Writing a unit test, would you be so sure that you have set all the properties required by the class under test? No, you wouldn’t. You’ll constantly see all that mess in the IntelliSense, asking yourself, whether that class requires that or another property to be set. The only way to get rid of that mess and make the code clearer and cleaner is to apply the Interface Segregation Principle.
Refactoring to Fix ISP Violation
To cure the disease, we should segregate the configuration interface. The Reports class should know only the part of configuration related to generating reports. Let’s extract that part into a separate interface.
[code lang=”csharp”]
public interface IReportsConfig
{
public decimal Income { get; set; }
public decimal Outcome { get; set; }
public decimal TotalRevenue { get; set; }
}
public interface IAppConfig
{
public string ServerId { get; set; }
public string ServerIP { get; set; }
public string ServerPort { get; set; }
public int LoggingSwitch { get; set; }
public int AppSkinId { get; set; }
}
public class TestableReportsAppConfig : IReportsConfig
{
public decimal Income { get; set; }
public decimal Outcome { get; set; }
public decimal TotalRevenue { get; set; }
}
public class TestableAppConfig : IAppConfig
{
public string ServerId { get; set; }
public string ServerIP { get; set; }
public string ServerPort { get; set; }
public int LoggingSwitch { get; set; }
public int AppSkinId { get; set; }
}
[/code]
Now we can request the IReportsConfig interface to be passed in the Report’s class constructor.
[code lang=”csharp”]
public class Report
{
private readonly IReportsConfig _reportsConfig;
public Report(IReportsConfig reportsConfig)
{
_reportsConfig = reportsConfig;
}
public string Generate()
{
return $"Income:{_reportsConfig.Income}" + "\n" +
$"Outcome:{_reportsConfig.Outcome}" + "\n" +
$"Total Revenue:{_reportsConfig.TotalRevenue}";
}
}
[/code]
Now, the Report class is aware of only those things which are relevant to the Report’s business logic. Let’s implement the unit test now:
[code lang=”csharp”]
[TestFixture]
public class ReportTests
{
[Test]
public void Generate_ValidInput_GeneratesReport()
{
IReportsConfig appConfig = new TestableReportsAppConfig()
{
Income = 10,
Outcome = 100,
TotalRevenue = 1000
};
Report sut = new Report(appConfig);
string report = sut.Generate();
Assert.AreEqual(
$"Income:10" + "\n" +
$"Outcome:100" + "\n" +
$"Total Revenue:1000",
report);
}
}
public class TestableReportsAppConfig : IReportsConfig
{
public decimal Income { get; set; }
public decimal Outcome { get; set; }
public decimal TotalRevenue { get; set; }
}
[/code]
Great! Now we don’t need to think of irrelevant stuff. The test is simple, clean and understandable.
Common Smells of Interface Segregation Principle Violation. Fixes and Related Patterns
ISP violation can be the root cause of LSP violation
In the first example, you’ve seen a smell which was similar to the violation of the Liskov Substitution Principle (LSP). We can refer to this smell as to “degenerate implementation of interface methods”. If a method which either overrides a method of a base class or implements a method inherited from an interface throws an exception, most likely NotSupportedException or it just does nothing what is called “degenerative implementation” that indicates that ISP may be violated. This is logical since there is a very high chance that a client of such a class doesn’t want to know about that method and doesn’t want to use it. At the same time, this is an indication of LSP violation, since such a class can’t act as a substitution for a base class or an interface. Sometimes, in such cases, the LSP violation is the root cause of the problem, but often the root cause is the violation of the ISP. So, you should study your particular case and figure out what causes the problem.
Too fat Interface
Another smell, which you’ve seen in the second demo can be described as the case when a client’s code references a class but only uses a small portion of its API. It obviously indicates that there is a low cohesion between such classes. This should suggest you that something is wrong here and you need to start the investigation of the problem. Often, you’ll find that the problem hides behind the too fat interfaces. To fix the problem, you have to properly segregate such an interface. Another sub-case here is that when you see a fat interface, but you don’t own it, you can’t change the interface because you can’t modify its source code. In that case, it might be useful to apply a Façade pattern. You’ve seen the application of this pattern in the SRP section. In this case, we can use Façade to narrow down the “width” of a fat class API.
If you have a fat interface and its API is not compatible with an interface on the client’s side when you try to use it, then you may find that there is also a possibility to apply the Adapter pattern. Firstly, let’s look at the Adapter pattern generally. What is the Adapter pattern?
According to the Gang of Four book, adapter pattern is intended to “Convert the interface of a class into another interface clients expect. Adapter lets classes work together that couldn’t otherwise because of incompatible interfaces”. The picture above demonstrates a case when an adapter can be applied. Imagine that at first, we have the Client which depends on the Fighter interface. On the other side, we have the Wizard class which exposes three members incompatible with the Fighter interface. If we want to use the Wizard through the Fighter interface, we need to introduce a WizardAdapter which implements the Fighter interface and internally uses the Wizard, it adapts it.
In our case, when we talk about the Interface Segregation Principle, we don’t have a classic problem when an adapter is usually applied, because we don’t deal with incompatible interfaces. In our case, interfaces are not incompatible, one is just too fat for another. Nevertheless, the adapter pattern can also be applied in our case as well.
Let’s look at the code which demonstrates the case of applying the Adapter pattern in order to adhere to the ISP. I didn’t come out with a real case, so I’ll just show you a completely synthetic example.
[code lang=”csharp”]
public interface IWideInterface
{
void A();
void B();
void C();
void D();
}
public interface INarrowInterface
{
void A();
void B();
}
[/code]
Here is the case. At first, you see that we have the IWideInterface which defines four methods, A, B, C, and D. The client’s code wants to use only methods A and B. To apply the Adapter pattern, we can define a separate INarrowInterface which defines only A and B methods. The adapter itself is going to implement that INarrowInterface while it accepts the IWideInterface instance in the constructor.
[code lang=”csharp”]
class Adapter : INarrowInterface
{
private readonly IWideInterface _wide;
public Adapter(IWideInterface wide)
{
_wide = wide;
}
public void A()
{
_wide.A();
}
public void B()
{
_wide.B();
}
}
[/code]
As the implementation of A and B methods, adapter just delegates the responsibility to the IWideInterface instance. The client’s code now can rely on the INarrowInterface, taking the instance in the constructor. We can pass in the adapter instance and the client will get the access only to methods A and B.
[code lang=”csharp”]
//needs to use only A and B
class Client
{
private readonly INarrowInterface _narrow;
public Client(INarrowInterface narrow)
{
_narrow = narrow;
}
}
[/code]
This is how the Adapter pattern can be applied to adhere to the Interface Segregation Principle.
There is another tricky case related to the Adapter pattern. Let’s say we have a class Persister which is directly used by many clients:
[code lang=”csharp”]
public class Persister
{
public void SaveToFile(string file, string content)
{
}
public void SaveToDb(string connectionString, string content)
{
}
}
[/code]
And now let’s say new clients require a new method in the Persister and they will depend only on that method. Then we can do the following:
[code lang=”csharp”]
public interface INewPersister
{
void SaveToCloud(string connectionString, string content);
}
public class Persister : INewPersister
{
public void SaveToFile(string file, string content)
{
}
public void SaveToDb(string connectionString, string content)
{
}
void INewPersister.SaveToCloud(string connectionString, string content)
{
}
}
[/code]
Now old clients can use the Persister as they used before and they will not see the new method. While new clients will see only the new method. This implementation of the interface is called – “the explicit implementation”. You can call such a method only through the interface:
[code lang=”csharp”]
class PersisterClient
{
void Save()
{
Persister p = new Persister();
//doesn’t compile
//p.SaveToCloud("", "")
INewPersister np = new Persister();
np.SaveToCloud("", ""); // compiles
}
}
[/code]
That’s all about it.
Don’t forget that the general way of fixing a problem with a fat interface is to create a narrower interface with only absolutely required methods in it, then have the fat interface implement your new interface and then use that new interface in the client’s code which don’t need to know about irrelevant API members of a fat interface. This is exactly what we did in the second refactoring demo while fixing the problem with a configuration.
And the last point here I want to address is that you shouldn’t crush a whole class-level interface into multiple single-method interfaces just because this will make the ISP violation impossible. Apply the refactoring techniques only when you feel that there is a technical debt starts to appear here and there or if a technical debt hasn’t started to spread yet but you know for sure that it will if you don’t fix the ISP violation.
The last tip I want to say about, concerns the dependency management at a binary level. So, the tip is that it is better whenever possible to keep the interface within the client’s assembly. It makes easier to change the interface if something goes wrong. A client will be able to change the interface exactly how it wants instead of rolling out any adapters.
Conclusion
The principle we discussed throughout this post was the Interface Segregation Principle or ISP in short. The ISP has a simple definition: “Clients should not be forced to depend on methods they do not use.” So, this definition implies that you have to strive to create small, cohesive and focused interfaces.
Of course, different clients can use only a subset of API members they depend upon, unless that API start to grow transforming into a very fat interface with unrelated members to each other or when similar but different methods appear to satisfy the requirements of different clients.
A typical way of violation you saw was when an interface was too broad what led to some smells like:
- implementers throw NotImplementedException
- clients have to see all the mess in the IntelliSense thinking of what they actually need to use.
As we discussed, there are several ways of dealing with ISP violations: - sometimes it is just enough to extract a separate interface from a fat one and use it where appropriate
- sometimes you need to come out with a Façade which hides irrelevant API members
- sometimes you need to apply an Adapter pattern especially if you don’t own the fat interface and thus you can’t modify its source code
Intelligent adhering to the Interface Segregation Principle makes an application easier to maintain what is extremely valuable especially in the long term.
Abusing ISP, you can end up with tons of too small interfaces what makes them harder to use by all clients. This is a smell known as Anti-ISP.
That’s all for the ISP. That was an interesting investigation. But there is another principle waiting for us, the “Dependency Inversion Principle”.