官术网_书友最值得收藏!

Interface segregation principle (ISP)

"Clients should not be forced to depend on methods they do not use."
                                                                                                         –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.

主站蜘蛛池模板: 淳安县| 荥经县| 高碑店市| 锡林郭勒盟| 象州县| 环江| 司法| 故城县| 宜昌市| 柳州市| 新邵县| 来宾市| 祥云县| 海阳市| 保康县| 新津县| 二连浩特市| 江永县| 宜丰县| 商河县| 五河县| 辉县市| 大埔县| 公安县| 铁岭市| 织金县| 金溪县| 望都县| 台北县| 佛冈县| 新龙县| 昔阳县| 二连浩特市| 张家口市| 新津县| 中阳县| 姜堰市| 稻城县| 绥芬河市| 陇川县| 兴隆县|