- Hands-On Dependency Injection in Go
- Corey Scott
- 909字
- 2021-06-10 19:17:47
Liskov substitution principle (LSP)
- 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:
-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.
- Unity 2020 Mobile Game Development
- Dependency Injection in .NET Core 2.0
- SAP BusinessObjects Dashboards 4.1 Cookbook
- 深入淺出PostgreSQL
- Scala程序員面試算法寶典
- Web性能實戰
- 精通MySQL 8(視頻教學版)
- C編程技巧:117個問題解決方案示例
- Fastdata Processing with Spark
- Unity 2017 Game AI Programming(Third Edition)
- ASP.NET 4.0 Web程序設計
- QlikView Unlocked
- UML基礎與Rose建模實用教程(第三版)
- 體驗之道:從需求到實踐的用戶體驗實戰
- AngularJS UI Development