GolangのDesignPatternをコード付きで簡単に紹介!
はじめに
今回この記事を書こうと思った背景として、とある技術記事でLoggerの実装方法について盛り上がってるものを見つけて、その部分の知見が弱い事を再認識させられた一件がありました。
また、一年弱ほど長期インターンでGo言語を沢山書いてきた中で、シンプルな文法で様々なDesignPatternを表現できるGo言語も面白さを皆さんに知っていただきたいなという想いも込めました。
それではGopher君の世界に踏み入れていきましょう。
Dependecy Injection
Dependency Injection(DI)は、依存関係の逆転(Dependency Inversion)の原則を実現するためのデザインパターンです。DIを使用すると、高レベルのモジュールが低レベルのモジュールに直接依存するのではなく、抽象化を介して間接的に依存するようになります。これにより、モジュール間の結合度を低く保ち、テスト容易性やコードの再利用性を向上させることができます。
- 実装例
type Repository interface{
GetAccount(ID int)(Account, error)
}
func NewRepoImpl(db *gorm.DB) Repository{
return &repoImpl(db:db)
}
type repoImpl struct {
db *gorm.DB
}
func (r *repoImpl) GetAccount (ID int) ( Account,error) {
// IDでFindしてAccountにMappingする処理
}
DIされたRepositoryのメソッドを呼び出すとrepoImplで実装した処理が動きます。
DI自体を詳しく知りたいなら以下の記事がおすすめ。
こちらのGithubで、実際のUsecaseをネットバンクの決済処理のCleanArchitectureでまとめて見ましたので参考までにご覧下さい。
Injectionのコードを自動生成する
また、DIをするとき、『New〇〇』で表現されるコンスタをInjectionするコードを肥大化していく傾向にあるのですが、例えばwire,digを使用することでInjectionのコードの生成を自動化する事も可能です。
wireでDIする実装例
// ./wireに以下を書いて wire genとコマンドを打つと ./wire_gen.goに依存関係が自動で整理されたコードが生成されます。
//go:build wireinject
// +build wireinject
〜〜〜
type handlers struct {
BankHandler *handler.BankHandler
}
func initializeHandlers(config *entity.Config) (*handlers, error) {
wire.Build(
wire.NewSet(
wire.FieldsOf(new(*entity.Config), "DB"),
gorm.NewDBClient,
),
wire.NewSet(
gorm.NewBankImpl,
wire.Struct(new(gorm.GormParams), "*"),
),
usecase.NewBankUsecase,
handler.NewBankHandler,
wire.Struct(new(handlers), "*"),
)
return nil, nil
}
〜〜〜
// 呼び出し側(エントリポイント)
func main() {
e := echo.New()
config := newConfig()
// initializeHandlers()は./wire_gen.goで生成された関数
h, err := initializeHandlers(config)
if err != nil {
log.Fatal(err)
}
e.PUT("/withdraw", h.BankHandler.Withdraw)
e.Logger.Fatal(e.Start(":8080"))
}
Functional Options Pattern
Functional Options Patternは、複数の引数を柔軟に持たせたいときに有効なパターンです。godocが見にくくなりますが、以下のようなメリットが存在します。
Loggerでの実装例
// 呼び出し側
customLogger, err := l.NewLogger(
l.WithLevel(zapcore.DebugLevel),
l.WithOutputPaths([]string{"stdout", "logs/app.log"}),
l.WithEncoder("json"),
)
〜〜〜
// 定義元
type LoggerOption func(*zap.Config)
func NewLogger(opts ...LoggerOption) (*zap.Logger, error) {
config := zap.NewProductionConfig()
for _, opt := range opts {
opt(&config)
}
return config.Build()
}
type options struct {
logger *zap.Logger
}
type Option func(*options)
func WithLevel(level zapcore.Level) LoggerOption {
return func(cfg *zap.Config) {
cfg.Level = zap.NewAtomicLevelAt(level)
}
}
func WithOutputPaths(paths []string) LoggerOption {
return func(cfg *zap.Config) {
cfg.OutputPaths = paths
}
}
func WithEncoder(encoder string) LoggerOption {
return func(cfg *zap.Config) {
if encoder == "json" {
cfg.Encoding = "json"
} else if encoder == "console" {
cfg.Encoding = "console"
}
}
}
</details>
Builder Pattern
Builder Patternは、ConstructerのInterfaceを定義して、メソッドチェーン形式で呼び出して使います。
// 下記の記事からコードを拝借いたしました🙇♂️
bpApp := NewApplicationWithBP(Premium).
WithBackupService(true).
WithSupport(true).
WithMovie(false).
Build()
Functional Options Patternと比べて、パフォーマンスが良いという記事がありましたが、実装コストが増加するデメリットもあるので考え所ではあるかと。
Singleton Pattern
シングルトンパターンは、特定のクラスのインスタンスがプログラム実行中に一つだけ存在することを保証するデザインパターンです。このパターンは、グローバルにアクセス可能なリソースやサービス、例えばロガーのようなものによく使用されます。シングルトンパターンの主な目的は、一貫性と効率を確保することです
シングルトンパターンの特徴
実装例(怪しいかも)
version 1.21で追加されたonce.Syncを使用して再現してます。(testしましたが毎回pointerのアドレスが異なるのでもう一捻りが必要かもしれません🙇♂️)
さらに、golobalで呼び出されるとトレースしにくいので、contextからtrace_idを取り出してセットする仕様としました。
var (
logger *zap.Logger
once sync.Once
)
func GetLogger(ctx context.Context) *zap.Logger {
once.Do(func() {
var err error
config := zap.NewProductionConfig()
config.Encoding = "json"
config.OutputPaths = []string{"stdout", "logs/app.log"}
logger, err = config.Build()
if err != nil {
panic(err)
}
})
if traceID, ok := ctxutils.GetTraceIDFromContext(ctx); ok {
return logger.With(zap.String("trace_id", traceID))
}
return logger
}
Observer Pattern
イベント駆動のシステムに適してるデザインパターンで、ObserberとSubjectというコンポーネントに分けることで、それぞれが疎結合な状態で、機能追加が柔軟だったりするメリットがあります。
イベント駆動とは何か?が気になる方はこちらをどうぞ
登場人物
- Observer(観察者)
type Observer[Event any] interface {
OnNotify(handler func(msg string))
}
- Subject(観察対象)
type Subject[event] interface {
Register(o Observer[event])
Unregister(o Observer[event])
Notify(e Event)
}
実装例 (ex.在庫の変化に応じてメールを送信する処理)
// [InventoryServiceとかのクラスの中の処理]
// 再入荷のイベント情報
type RestockedEvent struct {
InventoryID int
Quantity int
}
// 延期イベント情報
type DelayedEvent struct {
InventoryID int
ExpectedDate time.Time
}
=== Constructer ===
// Obserberの具象クラス
restockedObserver := NewInventoryObserber[RetockedEvent](...)
delayedObserver := NewInventoryObserber[DelayedEvent](...)
// Subject(Notifier)の具象クラス
restockedNotifier := NewInventoryNotifier[RetockedEvent](...)
delayedNotifier := NewInventoryNotifier[DelayedEvent](...)
===
restockedNotifier.Register(restockedObserver)
delayedNotifier .Register(delayedObserver)
...
restockedEvent := RestockedEvent{...}
delayedEvent := DelayedEvent{...}
restockedHandler := func(e RestockedEvent) {
// 再入荷したと伝えるメール処理
}
delayedEvent := func(e DelayedEvent) {
// 入荷の遅延を伝えるメール処理
}
restockedObserber.Notify(restockedEvent,restockedHandler)
delayedObserber.Notify(delayedEvent,delayedHandler)
...
上記の実装例のように、を発行する側とされる側の関心を分けてライフサイクルの処理を整理してかけます。コードが長すぎて載せませんでしたが、こちらでは、内部でRedisのPub/Sub機能を利用して、このPatternを再現しました。(一部動作が怪しいので参考までに🙇♂️)
Pub/Sub Pattern
Pub/SubパターンもObserberPatternと同様にイベント駆動アーキテクチャ上でよく採用される設計パターンになります。
Obserber Patternとの違い
ObserberPatternはEventの管理をSubject(観測される側)で行うのに対して、Pub/Subはそれ専用のBroker(Event Channel)が存在します。
Pub/SubはGCPのPub/Subの文脈で使われることが多くそちらをイメージしてもらうとわかりやすいかもしれません。
実装例 (kafka-goを利用して書こうとしましたが時間がありませんでした🙇♂️後ほど追記します。)
Pub/Sub実例紹介
特に、mercariさんがこのPub/Subを利用した非同期処理の知見が強いイメージがありますね、
Strategy Pattern
戦略パターン(Strategy Pattern)は、アルゴリズムの振る舞いを実行時に選択できるようにするデザインパターンです。このパターンを使用すると、アルゴリズムのバリエーションを独立したクラスとして定義し、それらを動的に切り替えることができます。これにより、アルゴリズムの使用を柔軟にし、コードの再利用性と拡張性を高めることができます。(これをGo言語で実現するためには、初めにお伝えしたDIを利用します。)
実装例
- じゃんけんのアリゴリズム
- 深さ優先探索、幅優先探索etcの切り替え
最後に
Go言語で表現された様々なDesignPatternはいかがでしたか。
今回の記事で紹介されたコードは以下のレポジトリにあるので、気になる方は参照してみて下さい。
何かご意見等ございましたらお気軽にコメントお寄せ下さい。
Discussion