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

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.

主站蜘蛛池模板: 太保市| 信阳市| 通渭县| 达日县| 时尚| 盘山县| 汤原县| 嘉定区| 福海县| 江门市| 固始县| 太仆寺旗| 横峰县| 呼伦贝尔市| 常宁市| 长岛县| 平山县| 班玛县| 安仁县| 靖宇县| 隆昌县| 扎兰屯市| 泸西县| 宜都市| 三台县| 铜鼓县| 台南市| 莲花县| 蓬溪县| 秦安县| 军事| 余姚市| 贵港市| 和田市| 同仁县| 习水县| 宝应县| 玉环县| 洛阳市| 电白县| 锡林浩特市|