SOLID原則をGo言語で理解する-SRP

SOLID原則について学んでいるのですが、文献などの説明だけでは抽象的で理解が難しい。

ちゃんと理解するにはコードを書くしかないかぁと思い、Go言語で実装してみて理解を深めようという目的の記事です。

今回はSRP、単一責務の原則についてまとめます。

SRPとは

single responsibility principle

SRP(Single Responsibility Principle)。単一責務の原則。

モジュールを変更する理由はたった一つであるべき。

転じて

モジュールはたった1つのアクターに対して責務を負うべき。

一つのモジュールの中で複数の役割を持つメソッドがあると、コード改修時の影響範囲が増えるため、バグが発生しやすい。複数人で改修したりするとコンフリクトも起きて、開発効率にも影響するかもしれません。

なのでモジュールの持つ役割を一つになるようにして、改修があった場合でも影響範囲をそのモジュールのみにとどめるような設計をしましょう。という話。

具体的なソリューションの一つとして、一つの責務を持つモジュールに分割をして、Facadeパターンで呼び出すやり方があります。

実践

下図のようなモジュールがあったとする。

Instrumentsのクラスに4つのメソッドがあり、それぞれ下記のような責務を持つ。

  • playMelody(): saxophone。メロディを奏でる。
  • playChords(): piano。和音を奏でる。
  • playBass(): bass。低音を奏でる。
  • playRhythm(): drums。リズムを刻む。

drumsがメロディを担当したり、bassが和音を担当することはしない。

このモジュールのbass( playBass())とdrums( playRhythm())のメソッドが useMetronome() という関数に依存している場合。 useMetronome() を変更するとbassとdrumsの両方に影響がでる。

useMetronome() がどのメソッドから呼ばれても処理内容が変わらないなら問題ない。しかしメソッドごとに処理を変えたい場合、 playBass() から呼ばれたらA、 playRhythm() から呼ばれたらBという具合にif文などで分岐する必要がある。分岐が増えると複雑になり、影響範囲が広がるためテストが大変になる。

なので、下図のように役割ごとにメソッドを分割し、Facadeでそのメソッドを呼ぶようにする。

クラス図

清書しました。

facadeinstrumentsInstrumentFacadeBass: instruments.BassDrums: instruments.DrumsPiano: instruments.PianoSaxophone: instruments.SaxophonePlayMusic()BassPlayBass(): stringDrumsPlayRhythm(): stringPianoPlayChords(): stringSaxophonePlayMelody(): string

ディレクトリ構造

.
├── facade # facade層
│   └── instrument.go
├── go.mod
├── instruments # 各役割ごとに分割した構造体とメソッド置き場
│   ├── bass.go
│   ├── drums.go
│   ├── piano.go
│   └── saxophone.go
└── main.go # ここからfacadeを使ってメソッドを呼び出す

./instruments/bass.go

package instruments

import "fmt"

type Bass struct{}

func (b *Bass) PlayBass() string {
	return fmt.Sprintf("bass.PlayBass()")
}

./instruments/drums.go

package instruments

import "fmt"

type Drums struct {}

func (d *Drums) PlayRhythm() string {
        return fmt.Sprintf("drums.PlayRhythm()")
}

./instruments/piano.go

package instruments

import "fmt"

type Piano struct {}

func (p *Piano) PlayChords() string {
        return fmt.Sprintf("piano.PlayChords()")
}

./instruments/saxophone.go

package instruments

import "fmt"

type Saxophone struct {}

func (s *Saxophone) PlayMelody() string {
        return fmt.Sprintf("saxophone.PlayMelody()")
}

./facade/instrument.go

InstrumentFacade という構造体の中に、Bass, Drumsなどの各構造体を詰め、 NewInstrumentFacade()InstrumentFacade を呼び出します。 NewInstrumentFacade() はコンストラクタとして機能します。

package facade

import (
	"fmt"

	"main.go/instruments"
)

type InstrumentFacade struct {
        Bass *instruments.Bass
        Drums *instruments.Drums
        Piano *instruments.Piano
        Saxophone *instruments.Saxophone
}

func NewInstrumentFacade() *InstrumentFacade{
        return &InstrumentFacade{
                &instruments.Bass{},
                &instruments.Drums{},
                &instruments.Piano{},
                &instruments.Saxophone{},
        }
}

func (i *InstrumentFacade) PlayMusic() {
        fmt.Println(i.Bass.PlayBass())
        fmt.Println(i.Drums.PlayRhythm())
        fmt.Println(i.Piano.PlayChords())
        fmt.Println(i.Saxophone.PlayMelody())
}

main.go

package main

import "main.go/facade"

func main() {
	instrument := facade.NewInstrumentFacade()
	instrument.PlayMusic()
}

出力結果

> go run main.go
bass.PlayBass()
drums.PlayRhythm()
piano.PlayChords()
saxophone.PlayMelody()

まとめ

SRP完全に理解した。

↓コード置き場

https://github.com/hodanov/solid-principles