If you missed the previous post from this series check it out SOLID Principles: LSP (3 of 4).
Interface segregation principle: ISP
“No client should be forced to depend on methods it does not use.”
In other words what the principle says is that components should be discrete: if you ask someone its name you don’t want to know that he is divorced four times and likes Italian food.
Imaging you are working with an input mapped to a Camera, the stream of data coming from that source is Read Only, and Non Seekable. You have no way to “write” to the camera feed, or to seek the camera to a specific time; you only get to read or loose the data. But because you are using a general purpose IO framework you end up using an InputSream with reset() and mark() / seek() methods.
What the ISP says for cases like this, is to split the interface into as many smaller ones as needed by the clients. The division is done looking at the “roles” the initial interface must play. In our case the InputStream is playing a both the Read Only and Non Seekable roles. The first one is implicit by the name prefix “Input”, so we don’t need to create another one to indicate that is an Read Only stream. But for the second role we need a way to say that our stream does not allows seek operations. So we could split the InputStream in two components: InputStream, andSeekableStream. The first one stays as is, with the seek-related methods removed. The second one will only have the seek-only methods on it.
Related Concepts
1 2 3 4 5 |
CameraOrMediaInputStream camInput = new CameraOrMediaInputStream(...); CameraReader camReader = new CameraReader((InputStream)camInput); CameraOrMediaInputStream mediaInput = new CameraOrMediaInputStream(...); MediaReader mediaReader = new MediaReader((SeekableStream)mediaInput); |
Casts are redundant in many OO languages but were added to imply the signature of both constructors expecting the steams.
Be aware that “Role Interfaces”, does not mean “Single Method Interfaces”, a role may be fulfilled by a single method, or by sequence of calls to related methods.
Code sample
Code samples are on github repo lsolano/blog.solid.demo
The code was written using Scala 2.11.7 using the JVM 1.8.0u20. The example for this principle is an Automatic Teller Machine (ATM). Our ATM is located in an international airport so it must handle withdraws with currency conversions. The ATM also supports deposits using both cash and cheques.
Here you have a summary of the use cases:
The initial class diagram for these use cases are:
Fat Interface |
Here is the code for the “fat” ATMInteractor component:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
class ATMInteractor(securityService: SecurityService, withdrawalService: WithdrawalService, depositService: AnyRef, currencyRatesService: AnyRef) { require(securityService != null) require(withdrawalService != null) require(depositService != null) require(currencyRatesService != null) def validate(request: CustomerValidationRequest): CustomerValidationResponse = { val validation = securityService.validateCustomer(PlasticInfo(request.secret, request.pin)) CustomerValidationResponse(validation.valid) } def withdrawal(request: WithdrawalRequest): TransactionResponse = { val response = withdrawalService.withdrawal(com.malpeza.solid.isp.model.Withdrawal(request.pin, request.amount)) val failReason = response.reason match { case Some(r) => r match { case com.malpeza.solid.isp.model.InsufficientBalance => InsufficientBalance case _ => CallBank } case _ => CallBank } TransactionResponse(response.done, Option(failReason)) } def deposit(request: AnyRef) = ??? } object ATMInteractor { def apply(securityService: SecurityService, withdrawalService: WithdrawalService, depositService: AnyRef, currencyRatesService: AnyRef): ATMInteractor = { new ATMInteractor(securityService, withdrawalService, depositService, currencyRatesService) } } |
What is wrong with this code: ISP?
Simply put, this component is doing to much. It handles all use cases: Customer Validation, Withdrawal, and Deposit. Because of that, it has to many dependencies (services).
If you are validating a customer you don’t need to know anything about Deposits or Withdrawals.
Cleaning the code
Honoring the ISP
Clean Interfaces |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class CustomerValidation(securityService: SecurityService) { require(securityService != null) def validate(request: CustomerValidationRequest): CustomerValidationResponse = { val validation = securityService.validateCustomer(PlasticInfo(request.secret, request.pin)) CustomerValidationResponse(validation.valid) } } object CustomerValidation { def apply(securityService: SecurityService) = new CustomerValidation(securityService) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Withdrawal(withdrawalService: WithdrawalService) { require (withdrawalService != null) def withdrawal(request: WithdrawalRequest): TransactionResponse = { val response = withdrawalService.withdrawal(com.malpeza.solid.isp.model.Withdrawal(request.pin, request.amount)) val failReason = response.reason match { case Some(r) => r match { case com.malpeza.solid.isp.model.InsufficientBalance => InsufficientBalance case _ => CallBank } case _ => CallBank } TransactionResponse(response.done, Option(failReason)) } } object Withdrawal { def apply(withdrawalService: WithdrawalService) = new Withdrawal(withdrawalService) } |
Agile Link
With the ISP we gain Orthogonality. For that concept I mean the following meanings
statistically independent
or
non-overlapping, uncorrelated, or independent objects of some kind
In other words, we get components with none or little relation. If one must change the other(s) remain untouched. This minimizes the impact of changes.
Orthogonality can be applied to architectural layers such as IO, Persistence, Transactions, Logging, etc.; or to behaviors like the sample project presented here. The last version has a clear separation of Use Cases. We could implement all those behaviors in a single component but that will make the code harder to maintain.
With such a reduced amount of use cases is difficult to see the need for this (over)engineering, but try to imagine the following:
You are working in an online retailing product’s back-end API. It’s the fifth year since the API initial version, and you have about 500 different use cases, each one with some pretty complicated scenarios such as:
- Checkout with mixed payment methods: Coupons + Prepaid Card + Credit Card + PayPal, etc.
- Manage upper bounds for customers by category to protect then from errors or malicious actions
- Single purchase with multiple items with arbitrary groups allowing for shipment to different addresses
- Manufacturers promotions by random distribution for customers matching certain profiles
- …
How many times you think you would expend each time you need to add a new feature or change an existing one if all these cases are implemented into a single component, or distributed within a small group of components?
Series links
Previous, SOLID Principles: LSP (3 of 4)
Next: none, this is the last one.