Today, we discuss principle #4 of SOLID principles to help write clean code in Python.
Here are the first , second , and third principles if you missed them.
And let’s dive into the fourth design principle (the ‘I’ in SOLID).
We will explain the concept with a real example implemented in Python and shown in a UML diagram to better illustrate the design of the classes and the relationships between them and have a visual understanding of what’s going on.
So what is ISP?
Definition:
Clients of a class should not be forced to depend on those of its methods that they don’t use.
In the preceding blog
post
covering LSP, we split one interface (in our case ISettings
) into
two IReadableSettings
and IWritableSettings
interfaces. That was
intended so that one could choose a suitable interface based on the
functionality.
Do you know that this solution was based on the Interface Segregation Principle; the 4th SOLID principle?
The client, mentioned in the definition, can be any part of the system that uses the class. Let’s see ISP in action with a more specific example than the previous one.
Example: E-Commerce Payment Design
Suppose you are developing an e-commerce website. You have a class that
represents the customer’s shopping cart and associated order processing
methods. Let’s define the IOrderProcessor
interface as shown below:
<img src=“https://drive.google.com/uc?export=view&id=1ZzVhnvK2bxulpOh65P26dgl5OyUdabQ0”,alt=“IOrderProcessor class with three methods: validate_card_info(), validate_shipping_address(), and process_order(). Two classes inherit from it: OnlineOrderProcessor and CashOnDeliveryOrderProcessor.",width=“50%">
IOrderProcessor violates ISP. (Designed by Plantuml)
validate_card_info()
to validate the credit card information, such as card number, card verification code, and expiration date.validate_shipping_address()
to validate the shipping physical address. This might be necessary to make sure that the destination is in the area that the company can cover.process_order()
to initiate processing the order to be delivered to the destination.
The question now: Why does the above UML show a bad design?
Remember the assumption that you only accept credit card payments. What if that assumption is no longer valid? and the company decides to accept cash-on-delivery payments in some areas.
At first glance, fixing this design might be straightforward as once you
implement the CashOnDeliveryOrderProcessor
class you can just raise
a NotImplementedError
for the credit card functionality validate_card_info()
.
def validate_card_info(obj: CardInfo):
raise NotImplementedError()
At this stage, your app may work as expected, but a potential problem may arise in the future.
Let’s assume that for some reason the online-based payments need extra
functionalities. This will force you to modify the IOrderProcessor
to include the extra methods. You will also need to implement these
newly added methods in the OnlineOrderProcessor
child. Not just
that, you will also need to modify the CashOnDeliveryOrderProcessor
child to define the same methods and throw NotImplementedError
.
In other words, the CashOnDeliveryOrderProcessor
will be changed
because of methods it doesn’t need in the first place which is violating
ISP. Also notice that for the implementation of the newly added methods,
you just threw NotImplementedError
which makes the CashOnDeliveryOrderProcessor
violates LSP.
Now, the new design (shown in the figure below) splits the required operations into two separate interfaces:
IOrderProcessor
which includes only two methods:validate_shipping_address()
andprocess_order()
. These two methods are consumed by theOnlineOrderProcessor
class as well asCashOnDeliveryOrderProcessor
.IOnlineOrderProcessor
which has only one method:validate_card_info()
. This interface is implemented only byOnlineOrderProcessor
.
Now, if you want to add specific functionalities to how online payments
are processed, you only need to change the IOnlineOrderProcessor
interface which will affect the OnlineOrderProcessor
, not the CashOnDeliveryOrderProcessor
. Thus, the new design conforms to ISP.
<img src=“https://drive.google.com/uc?export=view&id=1IwevolrZQiVgfpOUhD0di3_xwV-GaB6v”,alt=“Two interfaces: IOnlineOrderProcessor with validate_card_info() method, and IOrderProcessor with validate_shipping_address() and process_order() method. There are two classes beneath them. Both CashOnDeliveryOrderProcessor and OnlineOrderProcessor inherit from IOrderProcessor while OnlineOrderProcessor inherit from IOnlineOrderProcessor as well.",width=“50%">
Corrected design of the system. (Designed by Plantuml)
Final thoughts:
The Interface Segregation principle is applied when you notice that
there is a client of a class that implements a method that is not used.
In our example, we noticed that issue with the CashOnDeliveryOrderProcessor
class which implemented validate_card_info()
that was not needed.
See you in principle #5
Credit
- Beginning SOLID Principles and Design Patterns for ASP.NET Developers by Bipin Joshi