The Interface Segregation Principle with Lambdas
How to implement the ISP using simple functions to reduce coupling and complexity at the same time.
The Interface Segregation Principle (ISP) is the “I” in the SOLID acronym and states that clients should not be forced to depend on methods they don’t use.
It attempts to decouple clients from unnecessary details.
The Problem
Consider a typical violation of the ISP (in Java):
class UserService { User register(String username) { … } User find(String username) { … } void lock(User user) { … } } class UserRegistrationClient { private UserService userService; /* constructor */ void registerUser() { String username = … User user = userService .register(username); … } }
The UserRegistrationClient
class doesn’t need anything else than the register
method from the UserService
, but it does depend on the whole API.
A solution according the ISP would be to create an explicit contract only for the needs of the particular client:
interface RegisterUser { User register(String username); } class UserService implements RegisterUser { /* as above */ } class UserRegistrationClient { private RegisterUser registerUser; /* constructor */ void registerUser() { String username = … User user = registerUser .register(username); … } }
This already looks much better. The benefits are pretty obvious: the client code is decoupled from unnecessary details and an explicit contract between the service provider and client is established. This is a great way for the client to express her needs and to ensure it is implemented by the provider.
However, there are drawbacks, too. Any new interface brings certain complexity and following the approach strictly ends up writing more code, in extreme cases it could lead to an explosion of interfaces. Another problem appears when we don’t have the provider’s code in the hand.
The Solution
A solution would be to use lambdas (anonymous functions):
class UserService { /* as above */ } class UserRegistrationClient { private Function<String,User> registerUser; /* constructor */ void registerUser() { String username = … User user = registerUser .apply(username); … } }
The functionality is injected into the client using the provider:
new UserRegistrationClient( userService::register );
Again, this solution doesn’t come without drawbacks: we have lost the explicit and named contract and the code is less type-safety. On the other hand, we have much less code to maintain and gained a great flexibility as any function can be injected to implement the client’s needs, not just derivations of RegisterUser
. And of course, third-parties are no problem anymore.
The technique can be used even in languages without interfaces, such as JavaScript:
class UserRegistrationClient { constructor(registerUser) { this._registerUser = registerUser; } registerUser() { let username = … this._registerUser(username); … } }
Rather than:
class UserRegistrationClient { constructor(userService) { this._userService = userService; } registerUser() { let username = … this._userService.register(username); … } }
Conclusion
Using lambdas to decouple the client code according to the ISP works pretty well depending on the specific context, but it doesn’t come without drawbacks.
Moreover, the technique is not very common in languages where lambdas were introduced later than interfaces, such as Java or C#.