This lesson was translated automatically – it may contain errors.
Clients should not depend on interfaces they do not use.
Several small interfaces are better than one huge one that forces you to implement methods you do not need.
What a violation looks like
One interface for every office device – and a simple printer is obliged to scan and to send faxes:
// A fat interface: the implementation must provide what it cannot do
public interface IDevice
{
void Print(string document);
void Scan(string path);
void Fax(string number);
}
public class SimplePrinter : IDevice
{
public void Print(string document) { }
public void Scan(string path) => throw new NotSupportedException();
public void Fax(string number) => throw new NotSupportedException();
}
// Reports need printing alone, yet they depend on the whole device
public class ReportSender
{
public ReportSender(IDevice device) { }
}
// A fat interface: the implementation must provide what it cannot do
class IDevice {
public:
virtual void Print(const std::string& document) = 0;
virtual void Scan(const std::string& path) = 0;
virtual void Fax(const std::string& number) = 0;
virtual ~IDevice() = default;
};
class SimplePrinter : public IDevice {
public:
void Print(const std::string& document) override {}
void Scan(const std::string&) override { throw std::logic_error("not supported"); }
void Fax(const std::string&) override { throw std::logic_error("not supported"); }
};
// Reports need printing alone, yet they depend on the whole device
class ReportSender {
public:
explicit ReportSender(IDevice& device) : device_(device) {}
private:
IDevice& device_;
};
// A fat interface: the implementation must provide what it cannot do
type Device interface {
Print(document string) error
Scan(path string) error
Fax(number string) error
}
type SimplePrinter struct{}
func (SimplePrinter) Print(document string) error { return nil }
func (SimplePrinter) Scan(path string) error { return errors.New("not supported") }
func (SimplePrinter) Fax(number string) error { return errors.New("not supported") }
// Reports need printing alone, yet they depend on the whole device
type ReportSender struct {
device Device
}
# A fat interface: the implementation must provide what it cannot do
class Device(Protocol):
def print(self, document: str) -> None: ...
def scan(self, path: str) -> None: ...
def fax(self, number: str) -> None: ...
class SimplePrinter:
def print(self, document: str) -> None:
pass
def scan(self, path: str) -> None:
raise NotImplementedError("not supported")
def fax(self, number: str) -> None:
raise NotImplementedError("not supported")
# Reports need printing alone, yet they depend on the whole device
class ReportSender:
def __init__(self, device: Device) -> None:
self._device = device
// A fat interface: the implementation must provide what it cannot do
interface Device {
print(document: string): void;
scan(path: string): void;
fax(number: string): void;
}
class SimplePrinter implements Device {
print(document: string): void {}
scan(): void {
throw new Error('not supported');
}
fax(): void {
throw new Error('not supported');
}
}
// Reports need printing alone, yet they depend on the whole device
class ReportSender {
constructor(private readonly device: Device) {}
}
// A fat interface: the implementation must provide what it cannot do
public interface Device {
void print(String document);
void scan(String path);
void fax(String number);
}
public class SimplePrinter implements Device {
public void print(String document) { }
public void scan(String path) {
throw new UnsupportedOperationException();
}
public void fax(String number) {
throw new UnsupportedOperationException();
}
}
// Reports need printing alone, yet they depend on the whole device
public class ReportSender {
private final Device device;
public ReportSender(Device device) {
this.device = device;
}
}
// A fat interface: the implementation must provide what it cannot do
interface Device {
fun print(document: String)
fun scan(path: String)
fun fax(number: String)
}
class SimplePrinter : Device {
override fun print(document: String) = Unit
override fun scan(path: String): Nothing = throw UnsupportedOperationException()
override fun fax(number: String): Nothing = throw UnsupportedOperationException()
}
// Reports need printing alone, yet they depend on the whole device
class ReportSender(private val device: Device)
It compiles, and it works by half:
- the contract lies – the type promises three operations, the implementation performs one, and there is no way to learn that before runtime;
- the client depends on what it does not need –
ReportSenderprints reports, yet it is rebuilt every time a scanner method is added to the interface; - the stubs in tests grow – to test printing you have to implement faxing and scanning as well;
- adding a method is scary – a new
ScanDuplex()has to be written into every implementation, including the ones that do not need it.
Note this: throw NotSupportedException is exactly the same symptom as a violation of
LSP. Both principles guard the honesty of the contract, just
from different sides: ISP from the side of the interface, LSP from the side of the
implementation.
How to split it up
We cut along the clients: as many different sets of operations as the callers ask for, that many interfaces.
// Every interface is about one capability
public interface IPrinter { void Print(string document); }
public interface IScanner { void Scan(string path); }
public interface IFax { void Fax(string number); }
// A simple device implements only its own
public class SimplePrinter : IPrinter
{
public void Print(string document) { }
}
// The all-in-one does everything, and that is its own choice
public class Mfp : IPrinter, IScanner, IFax
{
public void Print(string document) { }
public void Scan(string path) { }
public void Fax(string number) { }
}
// The client asks for exactly what it uses
public class ReportSender
{
public ReportSender(IPrinter printer) { }
}
// Every interface is about one capability
class IPrinter {
public:
virtual void Print(const std::string& document) = 0;
virtual ~IPrinter() = default;
};
class IScanner {
public:
virtual void Scan(const std::string& path) = 0;
virtual ~IScanner() = default;
};
class IFax {
public:
virtual void Fax(const std::string& number) = 0;
virtual ~IFax() = default;
};
// A simple device implements only its own
class SimplePrinter : public IPrinter {
public:
void Print(const std::string& document) override {}
};
// The all-in-one does everything, and that is its own choice
class Mfp : public IPrinter, public IScanner, public IFax {
public:
void Print(const std::string& document) override {}
void Scan(const std::string& path) override {}
void Fax(const std::string& number) override {}
};
// The client asks for exactly what it uses
class ReportSender {
public:
explicit ReportSender(IPrinter& printer) : printer_(printer) {}
private:
IPrinter& printer_;
};
// Every interface is about one capability
type Printer interface {
Print(document string) error
}
type Scanner interface {
Scan(path string) error
}
type Faxer interface {
Fax(number string) error
}
// A simple device implements only its own
type SimplePrinter struct{}
func (SimplePrinter) Print(document string) error { return nil }
// The all-in-one does everything, and that is its own choice
type Mfp struct{}
func (Mfp) Print(document string) error { return nil }
func (Mfp) Scan(path string) error { return nil }
func (Mfp) Fax(number string) error { return nil }
// The client asks for exactly what it uses
type ReportSender struct {
printer Printer
}
# Every interface is about one capability
class Printer(Protocol):
def print(self, document: str) -> None: ...
class Scanner(Protocol):
def scan(self, path: str) -> None: ...
class Faxer(Protocol):
def fax(self, number: str) -> None: ...
# A simple device implements only its own
class SimplePrinter:
def print(self, document: str) -> None:
pass
# The all-in-one does everything, and that is its own choice
class Mfp:
def print(self, document: str) -> None: ...
def scan(self, path: str) -> None: ...
def fax(self, number: str) -> None: ...
# The client asks for exactly what it uses
class ReportSender:
def __init__(self, printer: Printer) -> None:
self._printer = printer
// Every interface is about one capability
interface Printer {
print(document: string): void;
}
interface Scanner {
scan(path: string): void;
}
interface Faxer {
fax(number: string): void;
}
// A simple device implements only its own
class SimplePrinter implements Printer {
print(document: string): void {}
}
// The all-in-one does everything, and that is its own choice
class Mfp implements Printer, Scanner, Faxer {
print(document: string): void {}
scan(path: string): void {}
fax(number: string): void {}
}
// The client asks for exactly what it uses
class ReportSender {
constructor(private readonly printer: Printer) {}
}
// Every interface is about one capability
public interface Printer {
void print(String document);
}
public interface Scanner {
void scan(String path);
}
public interface Faxer {
void fax(String number);
}
// A simple device implements only its own
public class SimplePrinter implements Printer {
public void print(String document) { }
}
// The all-in-one does everything, and that is its own choice
public class Mfp implements Printer, Scanner, Faxer {
public void print(String document) { }
public void scan(String path) { }
public void fax(String number) { }
}
// The client asks for exactly what it uses
public class ReportSender {
private final Printer printer;
public ReportSender(Printer printer) {
this.printer = printer;
}
}
// Every interface is about one capability
fun interface Printer {
fun print(document: String)
}
fun interface Scanner {
fun scan(path: String)
}
fun interface Faxer {
fun fax(number: String)
}
// A simple device implements only its own
class SimplePrinter : Printer {
override fun print(document: String) = Unit
}
// The all-in-one does everything, and that is its own choice
class Mfp : Printer, Scanner, Faxer {
override fun print(document: String) = Unit
override fun scan(path: String) = Unit
override fun fax(number: String) = Unit
}
// The client asks for exactly what it uses
class ReportSender(private val printer: Printer)
Not a single «not supported»: the all-in-one still does everything, but now that is its own
decision rather than an obligation of a simple printer. And the signature of ReportSender
shows at once that all it does is print.
What following it gives
- The implementation drags in no extra dependencies. A simple printer does not need fax libraries just because a method ended up in a shared interface.
- Mocks in tests get small. Replacing an interface of one method is one line; of ten – a separate stub class.
- A change to an interface touches only those who use it. A method added to the scanner – and the printers are not even recompiled.
- The interface becomes documentation. A list of one or two methods shows at once what is required of an implementation.
The sign of a violation
If an implementation is forced to throw «not supported» or to leave a method empty – the interface is too wide. Note that this is the same symptom as a violation of LSP: both principles guard the honesty of the contract.