Abstract
This
article is the 6th of a series of seven articles I wrote (described
later on) about The Principles of Object Oriented Design, also known as SOLID.
This
post describes the Interface Segregation Principle (ISP).
As
a prerequisite, please read the first 2 articles:
As
architects and solution designers we’d like to design and write robust &
clean solutions.
We’d
like our code to be stable, maintainable, testable and scalable for as long as
our product lives.
We’d
like to implement and integrate new features as smoothly as possible in our
existing (already tested and deployed) code, without any regression issues, and
by maintaining dead-lines and production deployments.
In
addition, we’d like our code to perform according to standard performance KPIs,
and eventually, to serves its purpose.
Such
desire has many obstacles, especially as our application evolves and our
product become more complex.
More
often than we’d like, we are experiencing these obstacles which could be
described as software design smells.
The
purpose of the SOLID principles is to achieve these desires and to prevent
software design smells.
In
this article series we’ll emphasize the importance of well-designed
applications.
We’ll
show how, by maintaining the SOLID principles in our code, we’ll create better
robust solutions, provide easier code maintenance, faster bug fixing and the
overall impact on the application performance, especially while adding new
features to existing design, as the application evolves with new requirements.
The complete series:
Intro:
S.O.L.I.D
Content
- Introduction
- Case Study
-
Requirements
- A design that does NOT conform to the ISP
1.
The IConfigManager interface
2.
The ConfigManager class
3.
A simple
‘Tasks’ design
- A design that conforms to the ISP
1.
The ICustomerConfigManager interface, and
CustomerConfigManager class
2.
The ISalesOrderConfigManager interface,
and SalesOrderConfigManager class
3.
The ISupplierConfigManager interface,
and SupplierConfigManager class
4.
A simple
‘Tasks’ design
- Summary
Introduction
The interface segregation principle
states that:
Clients should not
be forced to depend on methods they do not use.
When we are designing interfaces that
other classes could implement, sometimes with the aim to adhere to generic
behavior, we add needles methods, properties etc., that are not needed
by all clients of that interface.
In other words, the natural tendency
is to include all ‘relevant’ methods on that interface, in order to guide the
clients what to implement, according to our requirements.
This means that those clients would
have to implement methods (or set default implementation) that they do not
actually use, since not all clients would need all these methods.
Such design would cause undesired
coupling between all clients of that interface, since, in case one client
would change a method signature (as a result of new requirement), all clients
would also have to be modified.
In addition, if some of the
interface’s clients are in different modules (assemblies), such a change would
require the developers to retest, rebuild and redeploy
those modules as well.
This undesired coupling could
eventually cause our application to suffer from software design smells, mainly rigidity, fragility and viscosity.
In the following case-study, we’ll
examine how a ‘big’ interface could lead to those disadvantages.
Case
study
I’ll illustrate how to properly
design a solution that conforms to the ISP using a configuration management
example.
Requirements:
1.
We need to create a Configuration Manager that
receives configuration parameters from different sources/providers, and is used
in the entire codebase.
2.
The Providers could export configuration
parameters from: Database, External services, web.config files, Custom
configuration sections etc.
3.
Each Business Domain expects different
configuration parameters.
-
Meaning, we have different business domains, such as:
SalesOrders scenarios, Customers scenarios, Suppliers scenarios, etc.
-
Each business domain needs access to the configuration
manager with respects to its scenarios, and invokes different operations.
-
Remark:
For the
business logic implementation, I’ve added a simple BaseTask class that every business domain
could inherit and implement with relevant logic, and invoke the proper
configuration operations.
A
design that does NOT conform to the ISP
With the aim to get all the
configurations in one place, developers tend to encapsulate all operations in
one interface.
This way, they could have one entry
point to all configuration parameters, meaning they could invoke the same class
in every business domain, to retrieve the relevant configurations.
This is, of course, a violation of
the Interface Segregation Principle, since we’ll get one ‘big’ interface
with methods that are not relevant for all its clients.
Meaning, classes that would implement
this interface will only need some of the interface’s methods. (The rest of the
methods would get default implementation or throw an exception)
In addition, with every new business
domain class, we’ll have to add methods to the interface and to all clients,
which also violates the Open-Closed Principle.
More importantly, we’ll get undesired
coupling between the interface’s clients.
As we know, sometimes we need to
modify the interface API, since some of the client implementation was changed.
For instance, we have an interface
that comprises the following method signature:
CustomerConfigItems GetVIPCustomersConfiguration();
|
New requirements instructed us to
modify it as follows:
CustomerConfigItems GetVIPCustomersConfiguration(DateTime fromDate);
|
This would mean that other clients of
that interface would also have to be modified respectively, and we’ll need to rebuild,
retest and redeploy them as well.
We’ll examine the following design
which violates the Interface Segregation Principle:
click the image to enlarge |
Description:
1. The IConfigManager interface
I’ve created the IConfigManager
interface with all the relevant methods for all the business domains.
namespace
Configuration.Manager.Config.Manager
{
public interface IConfigManager
{
CustomerConfigItems
GetCustomersConfiguration();
CustomerConfigItems
GetVIPCustomersConfiguration();
SalesOrderConfigItems GetSalesOrderConfigId();
SuppliersConfigItems
GetSuppliersConfiguration();
}
}
|
2. The ConfigManager class
The ConfigManager
class implements the IConfigManager
interface.
Remark:
For our example and for simplicity I didn’t implement those methods.
using System;
namespace
Configuration.Manager.Config.Manager
{
public class ConfigManager : IConfigManager
{
public CustomerConfigItems GetCustomersConfiguration()
{
throw new NotImplementedException();
}
public SalesOrderConfigItems GetSalesOrderConfigId()
{
throw new NotImplementedException();
}
public SuppliersConfigItems GetSuppliersConfiguration()
{
throw new NotImplementedException();
}
public CustomerConfigItems GetVIPCustomersConfiguration()
{
throw new NotImplementedException();
}
}
}
|
Remark:
Different Clients
Instead of creating one ConfigManager
class, we could create separate classes for each business domain that would
implement the IConfigManager
interface.
E.g.
CustomerConfigManager,
SalesOrderConfigManager
and SupplierConfigManager.
However,
the problem still remains, since the IConfigManager interface is still ‘big’ and
contains methods that are implemented in different clients, thus the undesired
coupling issue would just get worse (as described above).
3. A simple ‘Tasks’ design
I’ve created a simple ‘Tasks’ design to
support the business domain logic.
We have an abstract BaseTask
class that every business domain could inherit and implement respectively.
The BaseTask
class contains a reference to the IConfigManager
interface, thus every business domain class could use it to get its relevant
configuration items.
using
Configuration.Manager.Config.Manager;
namespace
Configuration.Manager.Tasks.Base
{
public abstract class BaseTask
{
// Protected variables
protected IConfigManager _configManager;
// Constructors
public BaseTask(IConfigManager configManager = null)
{
// Setting the default
ConfigManager
// implementation in case
it wasn't provided.
_configManager = configManager ??
new ConfigManager();
}
// Public abstract methods
public abstract void Execute();
}
}
|
using
Configuration.Manager.Tasks.Base;
namespace
Configuration.Manager.Tasks.Customers
{
public class CustomerTask : BaseTask
{
public override void Execute()
{
var customerConfiguration =
_configManager.GetCustomersConfiguration();
// TODO:
Implementation...!
}
}
}
|
using Configuration.Manager.Tasks.Base;
namespace
Configuration.Manager.Tasks.SalesOrders
{
public class SalesOrderTask : BaseTask
{
public override void Execute()
{
var salesOrderConfigId =
_configManager.GetSalesOrderConfigId();
// TODO:
Implementation...!
}
}
}
|
using
Configuration.Manager.Tasks.Base;
namespace
Configuration.Manager.Tasks.Suppliers
{
public class SupplierTask : BaseTask
{
public override void Execute()
{
var suppliersConfiguration =
_configManager.GetSuppliersConfiguration();
// TODO:
Implementation...!
}
}
}
|
Remark:
Business domain tasks in different modules
We could see that each business
domain task uses the same IConfigManager
instance to retrieve its own relevant configuration methods.
Seemingly, since we’ve implemented
only one client to the IConfigManager
interface, the ConfigManager
class, this design operates well.
However,
in case we’ll need to modify the interface, and we have many business domains
tasks, in different modules (assemblies), we’ll have to rebuild, retest
and redeploy them as well.
This, of course, burden the system
and eventually would cause software design smells.
A
design that conforms to the ISP
There is more than one proper design
for these requirements. For simplicity and for our purpose, I’ve created the
following examples.
The basic idea is to divide the IConfigManager
interface into different small interfaces that contains the relevant domain
methods, and every business domain task would use its corresponding interface,
instead of using the ‘big’ IConfigManager
interface.
As a best practice it’s advisable to
design small interfaces with as little methods as possible, to prevent ‘client coupling’, even if it’s for the same domain.
Meaning, if for example we have
several configuration methods for the same business domain, we should
create several ‘little interfaces’ in order to prevent clients-coupling as
requirements would grow.
For our example, I’ve created the
following design:
|
Description:
1. The ICustomerConfigManager interface, and CustomerConfigManager class
using
Configuration.Manager.Config.Items;
namespace Configuration.Manager.Config.Manager.Customer
{
public interface ICustomerConfigManager
{
CustomerConfigItems
GetCustomersConfiguration();
CustomerConfigItems
GetVIPCustomersConfiguration();
}
}
|
using
Configuration.Manager.Config.Items;
using System;
namespace
Configuration.Manager.Config.Manager.Customer
{
public class CustomerConfigManager : ICustomerConfigManager
{
public CustomerConfigItems GetCustomersConfiguration()
{
throw new NotImplementedException();
}
public CustomerConfigItems GetVIPCustomersConfiguration()
{
throw new NotImplementedException();
}
}
}
|
2. The ISalesOrderConfigManager interface, and SalesOrderConfigManager class
using Configuration.Manager.Config.Items;
namespace
Configuration.Manager.Config.Manager.SalesOrder
{
public interface ISalesOrderConfigManager
{
SalesOrderConfigItems GetSalesOrderConfigId();
}
}
|
using
Configuration.Manager.Config.Items;
using System;
namespace
Configuration.Manager.Config.Manager.SalesOrder
{
public class SalesOrderConfigManager : ISalesOrderConfigManager
{
public SalesOrderConfigItems GetSalesOrderConfigId()
{
throw new NotImplementedException();
}
}
}
|
3. The ISupplierConfigManager interface, and SupplierConfigManager class
using
Configuration.Manager.Config.Items;
namespace
Configuration.Manager.Config.Manager.Supplier
{
public interface ISupplierConfigManager
{
SupplierConfigItems
GetSuppliersConfiguration();
}
}
|
using
Configuration.Manager.Config.Items;
using System;
namespace
Configuration.Manager.Config.Manager.Supplier
{
public class SupplierConfigManager : ISupplierConfigManager
{
public SupplierConfigItems GetSuppliersConfiguration()
{
throw new NotImplementedException();
}
}
}
|
4.
A
simple ‘Tasks’ design
namespace
Configuration.Manager.Tasks.Base
{
public abstract class BaseTask
{
public abstract void Execute();
}
}
|
using
Configuration.Manager.Config.Manager.Customer;
using
Configuration.Manager.Tasks.Base;
namespace
Configuration.Manager.Tasks.Customers
{
public class CustomerTask : BaseTask
{
// Private variables
private ICustomerConfigManager _customerConfigManager;
// Constructors
public CustomerTask(ICustomerConfigManager customerConfigManager = null)
{
_customerConfigManager =
customerConfigManager ?? new CustomerConfigManager();
}
// Public override methods
public override void Execute()
{
var customerConfiguration =
_customerConfigManager.GetCustomersConfiguration();
// TODO:
Implementation...!
}
}
}
|
using
Configuration.Manager.Config.Manager.SalesOrder;
using
Configuration.Manager.Tasks.Base;
namespace
Configuration.Manager.Tasks.SalesOrders
{
public class SalesOrderTask : BaseTask
{
// Private variables
private ISalesOrderConfigManager _salesOrderConfigManager;
// Constructors
public SalesOrderTask(ISalesOrderConfigManager salesOrderConfigManager = null)
{
_salesOrderConfigManager =
salesOrderConfigManager ?? new SalesOrderConfigManager();
}
// Public override methods
public override void Execute()
{
var salesOrderConfigId =
_salesOrderConfigManager.GetSalesOrderConfigId();
// TODO:
Implementation...!
}
}
}
|
using
Configuration.Manager.Config.Manager.Supplier;
using
Configuration.Manager.Tasks.Base;
namespace
Configuration.Manager.Tasks.Suppliers
{
public class SupplierTask : BaseTask
{
// Private variables
private ISupplierConfigManager _supplierConfigManager;
// Constructors
public SupplierTask(ISupplierConfigManager supplierConfigManager = null)
{
_supplierConfigManager =
supplierConfigManager ?? new SupplierConfigManager();
}
// Public override methods
public override void Execute()
{
var suppliersConfiguration =
_supplierConfigManager.GetSuppliersConfiguration();
// TODO:
Implementation...!
}
}
}
|
Description:
In this design we’ve separated the
configuration methods into 3 relatively small interfaces, based on the business
domain requirements.
This design prevents the coupling
between the interface clients, meaning in case we’ll have to modify one method
signature, other clients won’t be affected by this change.
In other words, we managed to design
these interfaces with respect to the Interface Segregation Principle.
Summary
We
managed to illustrate a simple use of the Interface Segregation Principle and
to understand the importance of ‘small’ designed
interfaces.
We
understood that, by creating ‘big’ interfaces, most times, some of the
interface methods won’t be needed by all the interface’s clients.
These
clients would have to provide default implementation, and this would naturally
burden the entire codebase, with unnecessary maintenance, testing, bugs and so
forth.
In
addition, this eventually would create undesired coupling between the
interface’s clients, and would lead to software design smells, mainly rigidity,
fragility and viscosity.
To
conclude I would say that, whenever a ‘big’ interface is required, we should
remember that clients could implement more than one interface.
Thus,
by dividing ‘big’ interface into few ‘small’ interfaces, we won’t affect the
client’s implementation, on the contrary, we would provide a ‘healthier’ and much
more tolerant to changes code.
---
Next in
the SOLID series is the Dependency Inversion Principle, which describes
the importance of loosely-coupled code, and how to prevent dependencies
in our classes, by using abstractions.
We’ll
design a better robust, easy maintainable and scalable solutions, which would
naturally prevent software design smells.
The End
Hope you enjoyed!
Appreciate your comments…
Yonatan Fedaeli
No comments:
Post a Comment