目次
はじめに
こんにちは、乃木恋のサーバサイドでエンジニアしている田中です。
今回は、Goを用いたオープンクローズの原則について書かせていただきたいと思います。
本題
長期タイトルのゲームを運用していると記述されているコードの量が膨大なために、仕様変更/機能実装があるたび、実装したコードが他の箇所へ影響を与えるかデバックしなければいけないことが多々あります。
そんな状況にならないようにするために、SOLID原則という有名なものがあります。
SOLID原則とは、ソフトウェア設計をより平易かつ柔軟にして保守しやすくすることを目的とし、その特徴はインターフェースを仲介にしての機能の使用と、インターフェースによる機能の注入であるというものです。
SOLIDの意味は五つの原則の頭字語であり、
- S … 単一責任の原則 (Single-responsibility principle)
- O … 開放閉鎖の原則(Open/closed principle)
- L …リスコフの置換原則(Liskov substitution principle)
- I … インターフェース分離の原則 (Interface segregation principle)
- D … 依存性逆転の原則(Dependency inversion principle)
からなります。
この記事ではSOLID原則のオープンクローズの原則を守って、保守性や柔軟性といったメリットを享受したガチャを実装してみようと思います。
オープンクローズの原則(open/closed principle | OCP)
wikiにはこう書かれています。
software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
ソフトウェアの実体(クラス、モジュール、関数など)は、拡張に対して開かれているべきであり、修正に対して閉じていなければならない
わかりやすく言い換えると、
仕様の追加/変更が発生した場合に既存のコードには修正を加えず、新しくコードを追加するだけで対応できるようにしましょう
ということです。
OCPを守ってない実装
以下の場合、GachaServiceがNormalガチャの実体に依存しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
package main import ( "fmt" ) type GachaService struct { normal Normal } func NewGachaService() GachaService { return GachaService{} } type Normal struct{} func (n *Normal) execute() error { fmt.Println("通常ガチャを実行") return nil } const NormalGacha = "normal" func (s *GachaService) Draw(gachaType string) error { switch gachaType { case NormalGacha: if err := s.normal.execute(); err != nil { return err } } return nil } func main() { service := NewGachaService() _ = service.Draw(NormalGacha) } |
【Go Playground – The Go Programming Language】
OCPは、仕様の追加/変更が発生した場合に既存のコードには修正を加えず、新しくコードを追加するだけで対応する原則です。
新しく「フェス限定ガチャ」の仕様追加が発生した場合、
GachaServiceの構造体へフェス限定ガチャの構造体を追加した上でDrawメソッドを修正する必要があります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
type GachaService struct { normal Normal festival Festival // フェス限定ガチャ } func (s *GachaService) Draw(gachaType string) error { switch gachaType { case NormalGacha: if err := s.normal.execute(); err != nil { // エラー処理 return err } case Festival: if err := s.festival.execute(); err != nil { // エラー処理 return err } } return nil } |
この修正方法だとガチャの種類が増えてきた際に条件分岐が複雑化し、GachaServiceが新規にガチャを追加するたびに変更されるので既存のガチャに影響を及ぼす可能性が増えいきます。
OCPを守った実装
interfaceを用いた場合のパターンを説明します。
以下の場合、GachaServiceがinterfaceの実体に依存させることができます。
こういった手法は Duck typing と言われています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
package main import ( "fmt" ) type GachaInterface interface { Execute() error } type GachaService struct { GachaModule GachaInterface } // NewGachaService factory method func NewGachaService(GachaType string) GachaService { switch GachaType { case NormalGacha: return NewNormalGachaService() case FestivalGacha: return NewFestivalGachaService() // 新規ガチャ } return GachaService{} } func (s *GachaService) Draw() error { if err := s.GachaModule.Execute(); err != nil { return err } return nil } type Normal struct{} func (n *Normal) Execute() error { fmt.Println("通常ガチャを実行") return nil } func NewNormalGachaService() GachaService { return GachaService{GachaModule: &Normal{}} } type Festival struct{} func (f *Festival) Execute() error { fmt.Println("フェス限定ガチャを実行") return nil } func NewFestivalGachaService() GachaService { return GachaService{GachaModule: &Festival{}} } const NormalGacha = "normal" const FestivalGacha = "festival" func main() { service := NewGachaService(FestivalGacha) _ = service.Draw() } |
【Go Playground – The Go Programming Language】
GachaInterfaceでExecuteメソッドを定義しています。
つまり、このExecuteメソッドを満たす構造体を作れば、それはそのインタフェースを実装した型とすることができます。
構造体であるNormalとFestivalがExecuteメソッドを実装しているので、GachaInterfaceの型として扱うことが出来るようになりました。
各ガチャ をGachaInterfaceにしたことにより、処理内容が実体に依存しなくなりました。
これにより
- open … 各ガチャを個別に実装する事が可能
- close … 各ガチャ同士の修正が影響を受けあわない
ロジックを使用する側からは、FactoryMethodをもちいて依存性を注入する方法が多く取られるかなと思います。
まとめ
エンジニアが多く関わるプロジェクトや長期化しそうなプロジェクトでは、実装したコードが平易かつ柔軟であれば保守がしやすくなるのでぜひSOLID原則を守って設計してみてください。
コードを書くモチベーションがかなり変わると思います。