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

Liskov substitution principle (LSP)

"If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T."
                                                                                                                   - Barbara Liskov

After reading that three times, I am still not sure I have got it straight. Thankfully, Robert C. Martin made it easier on us and summarized it as follows:

"Subtypes must be substitutable for their base types."
                                                                                    -Robert C. Martin

That I can follow. However, isn't he talking about abstract classes again? Probably. As we saw in the section on OCP, while Go doesn't have abstract classes or inheritance, it does have a composition and interface implementation.

Let's step back for a minute and look at the motivation of this principle. LSP requires that subtypes are substitutable for each other. We can use Go interfaces, and this will always hold true.

But hang on, what about this code:

func Go(vehicle actions) {
if sled, ok := vehicle.(*Sled); ok {
sled.pushStart()
} else {
vehicle.startEngine()
}

vehicle.drive()
}

type actions interface {
drive()
startEngine()
}

type Vehicle struct {
}

func (v Vehicle) drive() {
// TODO: implement
}

func (v Vehicle) startEngine() {
// TODO: implement
}

func (v Vehicle) stopEngine() {
// TODO: implement
}

type Car struct {
Vehicle
}

type Sled struct {
Vehicle
}

func (s Sled) startEngine() {
// override so that is does nothing
}

func (s Sled) stopEngine() {
// override so that is does nothing
}

func (s Sled) pushStart() {
// TODO: implement
}

It uses an interface, but it clearly violates LSP. We could fix this by adding more interfaces, as shown in the following code:

func Go(vehicle actions) {
switch concrete := vehicle.(type) {
case poweredActions:
concrete.startEngine()

case unpoweredActions:
concrete.pushStart()
}

vehicle.drive()
}

type actions interface {
drive()
}

type poweredActions interface {
actions
startEngine()
stopEngine()
}

type unpoweredActions interface {
actions
pushStart()
}

type Vehicle struct {
}

func (v Vehicle) drive() {
// TODO: implement
}

type PoweredVehicle struct {
Vehicle
}

func (v PoweredVehicle) startEngine() {
// common engine start code
}

type Car struct {
PoweredVehicle
}

type Buggy struct {
Vehicle
}

func (b Buggy) pushStart() {
// do nothing
}

However, this isn't better. The fact that this code still smells indicates that we are probably using the wrong abstraction or the wrong composition. Let's try the refactor again:

func Go(vehicle actions) {
vehicle.start()
vehicle.drive()
}

type actions interface {
start()
drive()
}

type Car struct {
poweredVehicle
}

func (c Car) start() {
c.poweredVehicle.startEngine()
}

func (c Car) drive() {
// TODO: implement
}

type poweredVehicle struct {
}

func (p poweredVehicle) startEngine() {
// common engine start code
}

type Buggy struct {
}

func (b Buggy) start() {
// push start
}

func (b Buggy) drive() {
// TODO: implement
}

That's much better. The Buggy phrase is not forced to implement methods that make no sense, nor does it contain any logic it doesn't need, and the usage of both vehicle types is nice and clean. This demonstrates a key point about LSP:

LSP refers to behavior and not implementation.

An object can implement any interface that it likes, but that doesn't make it behaviorally consistent with other implementations of the same interface. Look at the following code:

type Collection interface {
Add(item interface{})
Get(index int) interface{}
}

type CollectionImpl struct {
items []interface{}
}

func (c *CollectionImpl) Add(item interface{}) {
c.items = append(c.items, item)
}

func (c *CollectionImpl) Get(index int) interface{} {
return c.items[index]
}

type ReadOnlyCollection struct {
CollectionImpl
}

func (ro *ReadOnlyCollection) Add(item interface{}) {
// intentionally does nothing
}

In the preceding example, we met (as in delivered) the API contract by implementing all of the methods, but we turned the method we didn't need into a NO-OP. By having our ReadOnlyCollection implement the Add() method, it satisfies the interface but introduces the potential for confusion. What happens when you have a function that accepts a Collection? When you call Add(), what would you expect to happen?

The fix, in this case, might surprise you. Instead of making an ImmutableCollection out of a MutableCollection, we can flip the relation over, as shown in the following code:

type ImmutableCollection interface {
Get(index int) interface{}
}

type MutableCollection interface {
ImmutableCollection
Add(item interface{})
}

type ReadOnlyCollectionV2 struct {
items []interface{}
}

func (ro *ReadOnlyCollectionV2) Get(index int) interface{} {
return ro.items[index]
}

type CollectionImplV2 struct {
ReadOnlyCollectionV2
}

func (c *CollectionImplV2) Add(item interface{}) {
c.items = append(c.items, item)
}

A bonus of this new structure is that we can now let the compiler ensure that we don't use ImmutableCollection where we need MutableCollection.

主站蜘蛛池模板: 马鞍山市| 赤峰市| 宣化县| 玛多县| 新民市| 普兰店市| 砀山县| 东宁县| 孟津县| 梧州市| 区。| 寻甸| 秭归县| 德州市| 凌海市| 吉木乃县| 宁陵县| 庄河市| 惠来县| 西峡县| 民勤县| 阿拉尔市| 兴业县| 河北省| 扶沟县| 宣化县| 田东县| 岳普湖县| 赣榆县| 南溪县| 宜春市| 九寨沟县| 大田县| 西安市| 山东| 游戏| 岳阳市| 蓝山县| 福泉市| 麻江县| 西乡县|