Preamble: Introduction to interface segregation
(Skip this if you’re aware of what the Interface Segration Principle is.)
The Interface Segration
Principle is the
I in the SOLID mnemonic. It’s
conceptually one of the simplest, but is often overlooked, which is a shame as
it’s a highly beneficial principle to follow in software design.
The Interface Segration Principle is defined as “no client should be forced to
depend on methods it does not use”. More simply, it suggests that a client
should be able to depend on the smallest possible interface, i.e. exactly what
it needs and nothing more.
It’s common to see interfaces like this:
interface AccountManager {
createAccount(accountDetails)
getAccount(accountId)
updateAccount(accountId, accountDetails)
deleteAccount(accountId)
}
Client code then depends on that interface, as in this example:
class RegistrationService {
constructor(private accountManager: AccountManager, private emailService: EmailService) {}
registerUser(registrationRequest) {
this.accountManager.createAccount(someFactory.makeAccountDetails(registrationRequest))
this.emailService.sendWelcomeEmailToUser(registrationRequest.emailAddress)
}
}
This seems fine, and it’s not a big problem in this imaginary codebase. It does
have some drawbacks though:
Firstly, if we want to change the RegistrationService to use some other
dependency to create the account, we’d have to implement all the methods of
AccountManager on that dependency. We could change the dependency in
RegistrationService too, but it can get tricky if you have to identify exactly
which methods it’s using and how.
This situation would be even harder to resolve if we have multiple
implementations of AccountManager that get used in different situations, with
dependency injection choosing which implementation to give to
RegistrationService . In this part of the codebase, we’re only using the
createAccount method, but we’d have to implement all of the other methods for
each implementation, even if they’re just dummy methods. This won’t be easy to
reason about, and gets tiresome.
Those other methods exist on the AccountManager interface because other client
code is using them. What if some of that other client code needs a new method on
its dependency on AccountManager ? Now all of the other clients get their
dependency changed whether they need it or not. It’s easy for this to snowball,
with key interfaces accumulating more and more methods to satisfy the varied
needs of their clients.
This can turn into a vicious cycle: the more methods an interface offers, the
more attractive it is to new client code, and the more client code that depends
on it, the greater the temptation to add new methods to it.
Finally, if we’re writing unit tests for the RegistrationService with the
AccountManager mocked out, we have to mock out the other methods even though
they’re not being used in this instance. The same problem with new methods being
added will also occur – we have to make sure all the mock implementations also
get that new method.
Considering all of that, the dependency that RegistrationService has on
AccountManager doesn’t seem ideal. Wouldn’t it be better for it to depend only
on the methods it actually uses, and nothing more?
interface AccountCreator {
createAccount(accountDetails)
}
class DatabaseAccountManager implements AccountCreator, AccountUpdater, AccountDeleter {}
class RegistrationService {
constructor(private accountCreator: AccountCreator, private emailService: EmailService) {}
registerUser(registrationRequest) {
this.accountCreator.createAccount(someFactory.makeAccountDetails(registrationRequest))
this.emailService.sendWelcomeEmailToUser(registrationRequest.emailAddress)
}
}
In the example above, we’ve pulled out a smaller AccountCreator interface that
does one thing – creates accounts. The RegistrationService only needs that,
so that’s the dependency it defines. The DatabaseAccountManager implementation
happens to also offer other things, but that doesn’t matter – the
RegistrationService doesn’t know about that. This makes the refactoring and
mocking problems described above go away.
The catch is that, realistically, this often doesn’t happen in living codebases.
These situations aren’t as obvious as the contrived examples here, and it’s hard
to change habits to try and segregate interfaces more.
The trick is define interfaces that clients need, not interfaces that match
the concrete implementations you’re writing. One client needs a Countable ,
another needs an Iterable . If you’ve written a nice ArrayIterator
implementation, it’s tempting to make that the interface, but it should be the
other way round. One implementation can implement both Countable and
Iterable , but clients should depend on the specific one they need.
Line-priced interface example
As another example, consider a situation where we need to render out the basket
in an ecommerce system. We’ve got a list of BasketItem instances, which look
like this:
class BasketItem {
price: Price
description: string
productId: string
addedAt: Date
}
A trivial rendering might work like this:
function renderBasketItems(basketItems: BasketItem[]): string {
return basketItems.map(
(item) => `${item.description}: ${item.price.format()}`
).join('\n')
}
Notice that we’re not using the productId or addedAt properties of the
BasketItem here.
Later we have a new requirement to also display special discounts on the basket
as if they were also items in the basket. The rendering code has got a bit more
complicated, but we’d like the discount lines to be rendered in the same way.
It would be easy if we could just append the discounts to the basket items array
and have them rendered in the same way. There might be a temptation to have
DiscountLine extend BasketItem , but that would get messy quickly. They could
both implement the same interface, but we don’t need productId or addedAt on
DiscountLine .
This is a good chance to segregate off an interface: LinePriced or something
like that.
interface LinePriced {
price: Price
description: string
}
class BasketItem implements LinePriced {
price: Price
description: string
productId: string
addedAt: Date
}
class DiscountLine implements LinePriced {
price: Price
description: string
}
Now the same rendering logic can receive all the lines and render them, without
needing to know what exactly each one is. The key point is that LinePriced is
an interface based on the needs of client code, not on categorising
implementation code. BasketItem and DiscountLine might also implement other
interfaces separately to support other client code in different places.
An improvement: implicit interface implementation (e.g. in Go)
The above would be easier to achieve if you could define the required interface
with the client code, and not elsewhere with the implementation. One famous
feature of Go is that the way it does
interfaces encourages this. In Go,
interfaces are implemented implicitly, i.e. you don’t have to say FooClass implements BarInterface . If FooClass has the required methods for
BarInterface , then the compiler will allow it to be passed anywhere
BarInterface is required.
This means you can define the exact interface you need with the client code:
type geometry interface {
area() float64
perim() float64
}
func measure(g geometry) {
fmt.Println(g.area())
fmt.Println(g.perim())
}
Now we can pass any struct that has those method signatures, even if it’s from
some third-party library that’s out of our control. This encourages focusing on
what the client code needs when defining interfaces, which in turn encourages
interface segregation.
Even better implicit interfaces in TypeScript
TypeScript has the old-school FooClass implements BarInterface style of
interfaces, so you might think it’s not as good as Go on this front. However,
you don’t actually need to have implements BarInterface for the TypeScript
compiler to allow an implementation to passed where BarInterface is required.
Simply implementing the methods and properties is enough, as in Go.
TypeScript can take it one step further, though:
class OrderRedirector {
redirectOrder(order: { accountId: AccountID, orderDestination: OrderDestination }) {
//
}
}
In this class (or it could be an interface itself), we’re taking an order
argument, with a literal interface as the type. This could have been defined
explicitly as an interface, e.g RedirectableOrder , and we might well want to
extract it into that if the same interface is required in multiple places.
The key thing is that we define exactly what we need in the exact place we need
it. The TypeScript compiler will be happy to let us pass any object or interface
that has those properties.
It’s easy to refactor to this style; you just replace an explicit interface with
an implicit literal one with only the methods you need. This can often make it
easier to write unit tests for existing code – use a small literal interface,
and only mock out that in the unit tests with a small object literal.
Implicit interfaces combined with literal interface types is quite a powerful
combination, and I often find it useful when working on TypeScript projects.
View post:
Easier interface segregation in TypeScript (even better than Go!)
|