Ateam Hikkoshi Samurai Inc. & Ateam Connect Inc. Advent Calendar 2019 19日目は真のGopherになりたいルーキーGopher @lostfindがお送りします。
はじめに
println("みなさん、こんにちは!")
12日目の記事にも少しお話がありましたが, はい!そうです!
エイチーム引越し侍では言語はGo
, 設計思想はClean Architecture
, 開発方法はTDD
でバックエンド側のリプレイスに取り組んでいます!
この記事ではGo x Clean Architecture
でリプレイスしながら迎えた壁やまとまった考えについて, 少し共有ができたらなと思います!
まず, 結論から 🏁
Goとクリーンアーキテクチャの相性は?
個人的にはすごく良いと思います!
go
のこのような特徴がクリーンアーキテクチャをするにはメリットだと思います。
- ファイルやクラス単位ではなく,
package
単位での参照する - インタフェースで抽象化, 依存性逆転・注入ができる
- 構造体にメソッドをつけてOOPの
class
のようにも使えること - テストコードが書きやすい, テストがしやすい
Clean Architecture 🏛
最初にクリーンアーキテクチャの本を読んだときは, 「ふむふむ, なるほど・・」でしたが,
未だにQiita記事なども参考しながら, 本を何回か読み直すたびに, 「そういうことか!理解できていなかった!」と感じております。
この記事では各層の説明は割愛します!
概念についてはUncle Bobのブログ(英文), もしくは他の方のQiita記事をご参考ください!
書籍「Clean Architecture 達人に学ぶソフトウェアの構造と設計」もおすすめです。
僕が考えるクリーンアーキテクチャの原則
現時点での自分なりに理解したクリーンアーキテクチャの原則をまとめると
- クリーンアーキテクチャに正解はない
- ビジネスルールに近いほど内側の層に置く
- IO処理に近いほど, 外側の層に置く
- 内側から外側への依存はしない = 内側は外側の挙動や存在を知らない, 知る必要がない
- 修正の発生頻度で重要度や依存関係を判断してはいけない。
特に修正の発生頻度で依存性を判断しては行けないと思った理由は,
実際にビジネスルールに近いロジックの修正がDBやフレームワークの変更より頻繁に発生するからです。
メリット 👍
- テスト可能で, テストカバレッジを高く維持しやすい
- 修正が発生する際, 他の部分に与える影響が低いことで, 安定な運用ができる
- 新機能の追加時, 既に実装されている他の機能がベストプラクティスとして使える
この中で, 自分が思う最高のメリットは各層で**「テスト可能」**になることです!
テスト駆動開発にも良いと思います!
デメリット 👎
最初, 理解して開発ができるまで勉強コストがかなりかかりました。
開発中の今でも二歩前進、一歩後退を繰り返しています。
コードの量もフレームワークを利用することに比べたら1.5倍以上増えます。
Goを書いて感じたこと ʕ◔ϖ◔ʔ
使わない変数とimport
は許容しないので, ソースがきれいに維持できるのが好きです。
go
は言語仕様がシンプルで勉強しやすかったと思います。
ただ, 実際に開発を進めていくと基本的に提供するメソッドが少なくて初期は不便でした。
特に, スライス関連の関数がほしかったですね。
一から作るか, 外部のものを取り入れる必要がありましたが, 実際に作っていく気がしていて楽しいです。
現れた壁 ⛰
Go x Clean Architecture
で開発を進みながら大小の無数の壁に直面しました。
Goの壁とクリーンアーキテクチャの壁が交互に現れます。
場合によっては同時に現れるときもありましたね。
その中で, 幾つかをご紹介します!
Usecaseの肥大化
これはClean Architecture
の壁です。
初期はEntity
層にはDBテーブルとほぼ1:1に近いモデルだけ置き, ロジックはUsecase
層に書きました。
その結果, 開発が進んでいくとアプリケーションロジックを配置する層として考えたUsecase
に, ビジネスロジックもたっぷりで, ほとんどのコードがUsecase
に入っている状況になっていました。
// 変更前の構成
domain(Entity層)
L model
usecase
L repository
L service
L presenter
既存のusecase/service
に入っていたロジックを再検討して分離・移動しました。
アプリケーションロジックはusecase/interactor
を作ってそこに置くことでusecase
の肥大化を解決しました。
// 変更後の構成
domain(Entity層)
L model
L repository
L service
usecase
L interactor
L presenter
依存性の注入(DI)で引数が増え続ける
内から外への依存性を逆転させるため, インタフェースを置き, 依存性の注入をしていて
外部のライブラリーが増えるほどconstructor
の引数が増える構成をしていました。
これは, 暗号化ライブラリーEncrypter
を使うpresenter
の例です。
変更前はpresenter
構造体のフィールドとしてEncrypter
を持ちます。
ここでは引数が一つですが, 使われる外部ライブラリーが増えると引数も増やしていました。
type hogePresenter struct {
Encrypter encrypt.Encrypter
}
// NewHogePresenter HogePresenterを生成します。
func NewHogePresenter(encrypter encrypt.Encrypter) presenter.HogePresenter {
return &hogePresenter{
Encrypter: encrypter,
}
}
func (p *hogePresenter) Fuga(model []*model.HogeModel) []*presenter.ResponseHoge {
...
p.Encrypter.IntToRandStr(int(r.ID)),
...
}
コードも長くなりますし, わかりにくくなっていましたので
このように外部のライブラリーのインタフェース変数をグローバル変数で使うように修正しました。
package library
var Encrypt Encrypter // このようにグローバル変数を使います。
// Encrypter 暗号化に関する関数を提供します。
type Encrypter interface {
IntToRandStr(a int) string
...
}
// SetEncrypter Encrypterを生成します。
// 元々DIを行われている場所でグローバル変数へDIします。
func SetEncrypter(encrypt Encrypter) {
Encrypt = encrypt
}
func (p *hogePresenter) Fuga(model []*model.HogeModel) []*presenter.ResponseHoge {
...
library.Encrypt.IntToRandStr(int(r.ID)), // libraryパッケージのグローバル変数からメソッドを呼び出します。
...
}
グローバル変数使って大丈夫かなって感覚的は不安も少し感じましたが,
外側に依存しない規則は守られて, 置く場所だけ変わるので大丈夫かと思いましてこう決定しました。
エラーハンドリング設計
これはGoとクリーンアーキテクチャの両方の壁です。
Go
の壁は例外処理がないので, 想定できるエラーに対応しておく必要があります。
Clean Architecture
の壁は層を渡ってメソッドを呼び出しているので,
エラーが発生するとどの層までバケツリレーで渡して処理するべきなのかで悩みました。
考えた結果, 今は各エラーは各層で処理し, エラーログはグローバル変数でlogger
を持たし, それを利用する形をとっています。
今後, 変わる可能性はすごくすごーくあります。
終わりに
記事を書いてから見るとGoについては少なく, ほぼクリーンアーキテクチャの話になりました。
クリーンアーキテクチャを取り入れなくても概念は勉強してみると
当たり前なことを言ってるようで, またそこで新しく気づくことも多いと思います。
みなさんも是非Go
とClean Architecture
の世界へ入ってみるのはいかがでしょうか?
お知らせ 📢
エイチームグループでは一緒に活躍してくれる優秀な人材を募集中です。
興味のある方はぜひともエイチームグループ採用ページよりご応募ください!
Qiita Jobsのエイチーム引越し侍社内システム企画 / 開発チーム、社内システム開発エンジニアを募集!からチャットでご質問いただくことも可能です!
明日 👋
明日は, 引越し侍にテストコードを書く文化を率先して取り組んでいる @ysysysys さんの記事です!
おーたのしみにー!😺