SOLID原則をGO言語で理解する-LSP

デザインパターンの理解を深めようという話。第二弾。LSP、リスコフ置換の原則についてまとめます。

LSPとは

LSP(Liskov Substitution Principle)。リスコフの置換原則。

S 型のオブジェクト o1 の各々に、対応する T 型のオブジェクト o2 が 1 つ存在し、T を使って定義されたプログラム P に対して o2の代わりに o1 を使っても P の振る舞いが変わらない場合、S は T の派生型である。

Barbara Liskov

Subtypes must be substitutable for their base types.

派生型は基本型と代替可能でなければならない。

Robert C. Martin

継承したクラスは継承元のクラスと同じ動作をする必要があるということ。

実践

serviceBicycleName: stringSpeed: float64Ride(): stringTransport(dist: string): stringCarName: stringSpeed: float64Ride(): stringTransport(dist: string): stringRocketName: stringSpeed: float64Ride(): stringTransport(dist: string): stringVehicleInterfaceRide(): stringTransport(: string): string

Bicycle, Car, Rocketという構造体がVehicleInterfaceを実装している図。

VehicleInterfaceが基本型で、その他の構造体はVehicleInterfaceの派生型。

ディレクトリ構造

.
├── go.mod
├── main.go
└── service
    ├── bicycle.go
    ├── car.go
    ├── interface.go
    └── rocket.go

interface.go

package service

type VehicleInterface interface {
        Ride() string
        Transport(string) string
}

car.go

VehicleInterfaceを実装(VehicleInterfaceから派生した構造体とメソッド)。 CarImpl VehicleInterface = &Car{} という箇所でインターフェースを実装しています。

package service

import "fmt"

type Car struct {
        Name string
        Speed float64
}

var CarImpl VehicleInterface = &Car{}

func (c *Car) Ride() string{
	return fmt.Sprintf("Ride in %!s(MISSING).", c.Name)
}

func (c *Car) Transport(dist string) string {
	return fmt.Sprintf("Go to %!s(MISSING) at %!f(MISSING)[km/h]", dist, c.Speed)
}

bicycle.go

car.goと同様、VehicleInterfaceの派生型。

package service

import "fmt"

type Bicycle struct {
        Name string
        Speed float64
}

var BicycleImpl VehicleInterface = &Bicycle{}

func (b *Bicycle) Ride() string{
	return fmt.Sprintf("Ride in %!s(MISSING).", b.Name)
}

func (b *Bicycle) Transport(dist string) string {
	return fmt.Sprintf("Go to %!s(MISSING) at %!f(MISSING)[km/h]", dist, b.Speed)
}

rocket.go

car.go、bicycle.goと同じくVehicleInterfaceの派生型。しかしTransport()メソッドの中身が少し違う。

package service

import "fmt"

type Rocket struct {
        Name string
        Speed float64
}

var RocketImpl VehicleInterface = &Rocket{}

func (s *Rocket) Ride() string{
	return fmt.Sprintf("Ride in %!s(MISSING).", s.Name)
}

func (s *Rocket) Transport(dist string) string {
        var text string
        if dist == "space" {
                text = fmt.Sprintf("Go into the space at %!f(MISSING)[km/s].", s.Speed)
        } else {
                text = "This rocket can only go into the space."
        }
	return text
}

main.go

package main

import (
	"fmt"

	"main.go/service"
)

var (
	car     = service.CarImpl
	bicycle = service.BicycleImpl
	rocket  = service.RocketImpl
)

func transport(v service.VehicleInterface, d string) string {
	return fmt.Sprintf("%!s(MISSING) %!s(MISSING)", v.Ride(), v.Transport(d))
}

func main() {
	car = &service.Car{
		Name:  "TOYOTA Land Cruiser",
		Speed: 60,
	}
	bicycle = &service.Bicycle{
		Name:  "Jamis Ventura Comp",
		Speed: 10,
	}
	rocket = &service.Rocket{
		Name:  "SpaceX Falcon 9",
		Speed: 7.9,
	}

	fmt.Println(transport(car, "Tokyo"))
	fmt.Println(transport(bicycle, "nearby park"))
	fmt.Println(transport(rocket, "space"))
}

まとめ

LSP完全に理解した。

どこにinterfaceを実装するコードを置くのか迷う。今回は派生型(car.go, bicycle.go, rocket.go)に置いたけど、interface.goにまとめて置いても良いかもしれない。ここら辺はパッケージの分け方とかで最適解は変わりそうです。

↓コード置き場。

https://github.com/hodanov/solid-principles/tree/master/liskov_substitution