SOLID原則とは
SOLIDとは以下の各法則の頭文字をとってつなげたもの。
これらの原則を組み合わせることで、保守性と拡張性の高いソフトウェアを作成するための基盤が提供され、ソフトウェア開発におけるベストプラクティスと広く考えられています。
ここでは簡略化した解釈をのせているので、詳細に知りたい方は別途調べてみてください。
-
Single Responsibility Principle (単一責任の法則)
ある構造体は変更されるべき理由が一つであるべき。
-
Open-Closed Principle (オープンクローズドの法則)
構造体は拡張に対してオープンであり、変更に対してクローズであるべき。
-
Liskov Substitution Principle (リスコフの置換原則)
スーパークラスのオブジェクトは、プログラム上の正しさに影響を与えることなく、サブクラスのオブジェクトに置き換えることができるべき。
-
Interface Segregation Principle (インターフェース分離の法則)
インターフェースは可能な限り小さく限定的に設計されるべきであり、クライアントが使用しないインターフェースには依存させてはならない。
-
Dependency Inversion Principle (依存関係逆転の法則)
高位モジュールは下位モジュールに依存すべきではなく、むしろ両者は抽象化されたものに依存すべきである。
SOLID原則をGo言語で実装する際に、どのように実装すれば良いのかをサンプルコードを交えて紹介していきます。
SRP (単一責任の法則)
Employee を表現する構造体がある。名前、給料、メールアドレスを持っているとする。
type Employee struct {
Name string
Salary float64
Address string
}
SRPによると、各構造体の責任は1つだけであるべきなので、この場合は2つの構造体に分割できる。
type EmployeeInfo struct {
Name string
Salary float64
}
type EmployeeAddress struct {
Address string
}
これにより、各構造体のメソッドが分割される。
func (e EmployeeInfo) GetSalary() float64 {
return e.Salary
}
func (e EmployeeAddress) GetAddress() string {
return e.Address
}
SRPに従うと各構造体が明確で具体的な責任を持つようになるので、保守性と可読性が高まりやすくなります。
給与の計算や住所の処理に変更を加える場合は、関係ないコードを読む必要がなく、読むべきところが明確になります。
OCP (オープンクローズドの法則)
クレジットカード決済を処理できる決済システムを実装する場合を考えて見ましょう。
将来的に様々な支払い方法に対応できるように柔軟性を持たせたいとします。
package main
import "fmt"
type PaymentMethod interface {
Pay()
}
type Payment struct{}
func (p Payment) Process(pm PaymentMethod) {
pm.Pay()
}
type CreditCard struct {
amount float64
}
func (cc CreditCard) Pay() {
fmt.Printf("Paid %.2f using CreditCard", cc.amount)
}
func main() {
p := Payment{}
cc := CreditCard{12.23}
p.Process(cc)
}
OCPに従うとPayment構造体は拡張にオープンであり修正にクローズとなります。
Payment構造体は PaymentMethod インターフェースを使用しているので、新しい支払い方法が追加されても Paymentのふるまいは修正する必要がありません。
新しい支払い方法として PayPal を追加する場合は、次のようになります。
...
type PayPal struct {
amount float64
}
func (pp PayPal) Pay() {
fmt.Printf("Paid %.2f using PayPal", pp.amount)
}
func main() {
pp := PayPal{22.33}
p.Process(pp)
}
LSP (リスコフの置換原則)
Animal という構造体を考えてみます。
type Animal struct {
Name string
}
func (a Animal) MakeSound() {
fmt.Println("Animal sound")
}
ここで特定の種類の動物を表す Bird を作成したいとします。
type Bird struct {
Animal
}
func (b Bird) MakeSound() {
fmt.Println("Chirp chirp")
}
Animalの鳴き声を抽象化し、それを利用する場合はこのようになります。
...
type AnimalBehavior interface {
MakeSound()
}
func MakeSound(ab AnimalBehavior) {
ab.MakeSound()
}
a := Animal{}
b := Bird{}
MakeSound(a)
MakeSound(b)
サブタイプのBirdオブジェクトは、ベースタイプのAnimalオブジェクトが期待されるところであればどこでも使用することができます。
DIP (依存関係逆転の法則)
会社の労働者を表す Worker構造体と、上司を表す Supervisor構造体があるとします。
type Worker struct {
ID int
Name string
}
func (w Worker) GetID() int {
return w.ID
}
func (w Worker) GetName() string {
return w.Name
}
type Supervisor struct {
ID int
Name string
}
func (s Supervisor) GetID() int {
return s.ID
}
func (s Supervisor) GetName() string {
return s.Name
}
さらに高位モジュールで会社の部署を表す Department構造体があり、労働者や上司の情報を持つ必要があるとします。
アンチパターンでよくあるのは次のような実装です。
type Department struct {
Workers []Worker
Supervisors []Supervisor
}
これは上位モジュールが下位モジュールに依存しており DIP に違反しています。
代わりにどちらも抽象化されたものに依存するよう変更してみましょう。
まず、従業員と上司の両方を表す Employeeインターフェースを作成します。
type Employee interface {
GetID() int
GetName() string
}
Department構造体が Employeeインターフェースを満たすように修正すると、下位モジュールに依存しなくなります。
type Department struct {
Employees []Employee
}
上記の例を用いた全体像は以下のようになります。
package main
import "fmt"
type Worker struct {
ID int
Name string
}
func (w Worker) GetID() int {
return w.ID
}
func (w Worker) GetName() string {
return w.Name
}
type Supervisor struct {
ID int
Name string
}
func (s Supervisor) GetID() int {
return s.ID
}
func (s Supervisor) GetName() string {
return s.Name
}
type Employee interface {
GetID() int
GetName() string
}
type Department struct {
Employees []Employee
}
func (d *Department) AddEmployee(e Employee) {
d.Employees = append(d.Employees, e)
}
func (d *Department) GetEmployeeNames() (res []string) {
for _, e := range d.Employees {
res = append(res, e.GetName())
}
return
}
func (d *Department) GetEmployee(id int) Employee {
for _, e := range d.Employees {
if e.GetID() == id {
return e
}
}
return nil
}
func main() {
dep := &Department{}
dep.AddEmployee(Worker{ID: 1, Name: "John"})
dep.AddEmployee(Supervisor{ID: 2, Name: "Jane"})
fmt.Println(dep.GetEmployeeNames())
e := dep.GetEmployee(1)
switch v := e.(type) {
case Worker:
fmt.Printf("found worker %+v\n", v)
case Supervisor:
fmt.Printf("found supervisor %+v\n", v)
default:
fmt.Printf("could not find an employee by id: %d\n", 1)
}
}
これにより Worker構造体や Supervisor構造体が修正されても Department構造体には影響を与えないため、コードの柔軟性が高まり、保守が容易になります。
以上となります。ありがとうございました!