- Hands-On Dependency Injection in Go
- Corey Scott
- 1121字
- 2021-06-10 19:17:48
Interface segregation principle (ISP)
–Robert C. Martin
Personally, I prefer a much more direct definition—interfaces should be reduced to the minimum possible size.
Let's first discuss why fat interfaces might be a bad thing. Fat interfaces have more methods and are therefore likely to be harder to understand. They also require more work to use, whether this be through implementing, mocking, or stubbing them.
Fat interfaces indicate more responsibility and, as we saw with the SRP, the more responsibility an object has, the more likely it will want to change. If the interface changes, it causes a ripple effect through all its users, violating OCP and causing a massive amount of shotgun surgery. This is the first advantage of ISP:
ISP requires us to define thin interfaces
For many programmers, their natural tendency is to add to the existing interface rather than define a new one, thereby creating a fat interface. This leads to a situation where the, sometimes singular, implementation becomes tightly coupled with the users of the interface. This coupling then makes the interface, their implementations, and users all the more resistant to change. Consider the following example:
type FatDbInterface interface {
BatchGetItem(IDs ...int) ([]Item, error)
BatchGetItemWithContext(ctx context.Context, IDs ...int) ([]Item, error)
BatchPutItem(items ...Item) error
BatchPutItemWithContext(ctx context.Context, items ...Item) error
DeleteItem(ID int) error
DeleteItemWithContext(ctx context.Context, item Item) error
GetItem(ID int) (Item, error)
GetItemWithContext(ctx context.Context, ID int) (Item, error)
PutItem(item Item) error
PutItemWithContext(ctx context.Context, item Item) error
Query(query string, args ...interface{}) ([]Item, error)
QueryWithContext(ctx context.Context, query string, args ...interface{}) ([]Item, error)
UpdateItem(item Item) error
UpdateItemWithContext(ctx context.Context, item Item) error
}
type Cache struct {
db FatDbInterface
}
func (c *Cache) Get(key string) interface{} {
// code removed
// load from DB
_, _ = c.db.GetItem(42)
// code removed
return nil
}
func (c *Cache) Set(key string, value interface{}) {
// code removed
// save to DB
_ = c.db.PutItem(Item{})
// code removed
}
It's not hard to imagine all of these methods belonging to one struct. Method pairs such as GetItem() and GetItemWithContext() are quite likely to share much, if not almost all, of the same code. On the other hand, a user of GetItem() is not likely to also use GetItemWithContext(). For this particular use case, a more appropriate interface would be the following:
type myDB interface {
GetItem(ID int) (Item, error)
PutItem(item Item) error
}
type CacheV2 struct {
db myDB
}
func (c *CacheV2) Get(key string) interface{} {
// code removed
// load from DB
_, _ = c.db.GetItem(42)
// code removed
return nil
}
func (c *CacheV2) Set(key string, value interface{}) {
// code removed
// save from DB
_ = c.db.PutItem(Item{})
// code removed
}
Leveraging this new, thin interface makes the function signature far more explicit and flexible. This leads us to the second advantage of ISP:
ISP leads to explicit inputs.
A thin interface is also more straightforward to more fully implement, keeping us away from any potential problems with LSP.
In cases where we are using an interface as an input and the interface needs to be fat, this is a powerful indication that the method is violating SRP. Consider the following code:
func Encrypt(ctx context.Context, data []byte) ([]byte, error) {
// As this operation make take too long, we need to be able to kill it
stop := ctx.Done()
result := make(chan []byte, 1)
go func() {
defer close(result)
// pull the encryption key from context
keyRaw := ctx.Value("encryption-key")
if keyRaw == nil {
panic("encryption key not found in context")
}
key := keyRaw.([]byte)
// perform encryption
ciperText := performEncryption(key, data)
// signal complete by sending the result
result <- ciperText
}()
select {
case ciperText := <-result:
// happy path
return ciperText, nil
case <-stop:
// cancelled
return nil, errors.New("operation cancelled")
}
}
Do you see the issue? We are using the context interface, which is fantastic and highly recommended, but we are violating ISP. Being pragmatic programmers, we can argue that this interface is widely used and understood, and the value of defining our own interface to reduce it to the two methods that we need is unnecessary. In most cases, I would agree, but in this particular case, we should reconsider. We are using the context interface for two entirely separate purposes. The first is a control channel to allow us to stop short or timeout the task, and the second is to provide a value. In effect, our usage of context here is violating SRP and, as such, risks potential confusion and results in a greater resistance to change.
What happens if we decide to use the stop channel pattern not on a request level, but at the application level? What happens if the key value is not in the context, but from some other source? By applying the ISP, we can separate the concerns into two interfaces, as shown in the following code:
type Value interface {
Value(key interface{}) interface{}
}
type Monitor interface {
Done() <-chan struct{}
}
func EncryptV2(keyValue Value, monitor Monitor, data []byte) ([]byte, error) {
// As this operation make take too long, we need to be able to kill it
stop := monitor.Done()
result := make(chan []byte, 1)
go func() {
defer close(result)
// pull the encryption key from Value
keyRaw := keyValue.Value("encryption-key")
if keyRaw == nil {
panic("encryption key not found in context")
}
key := keyRaw.([]byte)
// perform encryption
ciperText := performEncryption(key, data)
// signal complete by sending the result
result <- ciperText
}()
select {
case ciperText := <-result:
// happy path
return ciperText, nil
case <-stop:
// cancelled
return nil, errors.New("operation cancelled")
}
}
Our function now complies with the ISP, and both inputs are free to evolve separately. But what happens to the users of this function? Must they stop using context? Absolutely not. The method can be called as shown in the following code:
// create a context
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// store the key
ctx = context.WithValue(ctx, "encryption-key", "-secret-")
// call the function
_, _ = EncryptV2(ctx, ctx, []byte("my data"))
The repeated use of context as a parameter likely feels a little weird but, as you can see, it's for a good cause. This leads us to our final advantage of the ISP:
ISP helps to decouple the inputs from their concrete implementation, enabling them to evolve separately.
- 程序員面試白皮書
- Monkey Game Development:Beginner's Guide
- C語言程序設計(第3版)
- Android開發精要
- JMeter 性能測試實戰(第2版)
- Offer來了:Java面試核心知識點精講(原理篇)
- JavaScript Unlocked
- oreilly精品圖書:軟件開發者路線圖叢書(共8冊)
- Nexus規模化Scrum框架
- 精通Python自然語言處理
- Elasticsearch Server(Third Edition)
- The DevOps 2.5 Toolkit
- HTML 5與CSS 3權威指南(第3版·上冊)
- Node.js:來一打 C++ 擴展
- Modern C++ Programming Cookbook