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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.163.13