【連載 第4回】freeeカード Unlimited でのClean Architecture実践

こんにちは、金融チームでエンジニアをしているlvmingbeiです。この記事はfreeeカード Unlimited の開発の裏側について紹介する連載の第4回目になります。

この記事では、freeeカード Unlimitedの開発でClean Architectureを導入して学んだことをシェアできればと思います。

第1回~第3回の連載記事は以下のリンクでご覧ください。

Clean Architectureの概要

Clean ArchitectureはRobert C. Martinが提唱し、関心の分離、疎結合を提供するアーキテクチャパターンです。

以下の図がとても有名で、ご覧になった方が多いと思います。

f:id:lvmingbei:20210920182759j:plain

Clean Architectureは同心円上に広がるレイヤーがあり、外側が内側に依存するということをルールとしています。つまり内側のレイヤーのソフトウェアコンポーネントは、外側のレイヤーについて何も知らないということを保証します。逆にレイヤーについて方針はあるものの、種類や数について制限しておらず、右下のレイヤー間の依存の方法も一つの例として挙げられているだけです。

Clean Architectureを利用することで、以下の問題を解決します。(原文から翻訳)

  • フレームワーク独立。 アーキテクチャは、ソフトウェアやライブラリに依存しません。

  • テスト可能。 ビジネスルールは、UI、データベース、Webサーバー、またはその他の外部要素なしでテストできます。

  • UI独立。 ビジネスルールを変更せずに、UIを置き換えることができます。

  • データベース独立。 OracleまたはSQLServerを、Mongo、BigTable、CouchDBなどに交換できます。 ビジネスルールはデータベースに拘束されていません。

達成したいこと

freeeカード UnlimitedはClean Architectureの採用にあたって、以下の項目を達成することを目標にしています。

  • 変化に強い設計
    • freeeカード Unlimitedは多数外部サービスに依存しているため、外部サービスの仕様が変わっても、コアとなるビジネスロジックは影響されないように設計しなければならない。
    • プロダクトの各フェーズでは、ユーザーに提供している機能が大きく変わるので、機能追加しやすい設計をしなければならない。
  • 再利用性の高い設計
    • コードの再利用により、重複な機能排除ができ、修正漏れをなくす。
    • テストの負担軽減。
  • メンテナンスが容易になる
    • 障害が発生した場合にできるだけ早くメンテナンスを完了させる。
  • テストが容易になる
    • 決済サービスであり、お客様の財産を守るため、より一層の品質向上が必要、テストを大事にする。
    • レイヤーごとテストができるようにする。

プロジェクトの構成

freeeカード Unlimitedは上記の画像と同じ構造にしており、Enterprise Business Rules、Application Business Rules、Interface Adapters、Frameworks and Driversの4つのレイヤーに分けられています。

レイヤー名 職責 パッケージ名
Enterprise Business Rules エンティティは包括的で重要なビジネスルールを隠蔽化したオブジェクトを含むレイヤーです。最も内側に位置するように他のレイヤーの変更を受けない核心の部分です。 domain
Application Business Rules 固有のビジネスルールを含むレイヤーです。ここでは、ドメインのエンティティを利用し、アプリケーションの機能を実現するためのメイン実装になります。 usecase
Interface Adapters 入力のバリデーションや、データベースのORMや、画面の表示用データ変換などはここで記述します。 controller, repository, rpc, client
Frameworks and Drivers データベースやフレームワークなどの設定が含まれます。このレイヤーは全ての詳細が含まれておりどこからも依存されません。 database, config

パッケージの依存関係

f:id:lvmingbei:20210927141524p:plain

依存性逆転の原則

freeeカード Unlimitedは「抽象に依存せよ」の原則に従って実装しました。すべてのレイヤーは「抽象」に依存してます。

具体的には、上位レイヤーは下位レイヤーの実体に依存せずに上位レイヤーで宣言した抽象に依存するようにしています。 下位レイヤーは上位が宣言したインターフェースを依存するので上位レイヤーは下位レイヤーに依存しなくなり、つまり、上位レイヤーは下位レイヤーの知識を持たなくなります。 そうすると、上下位のレイヤー両方は「抽象」に依存するようになります。下位レイヤーを変更しても上位レイヤーがその影響を受けなくなります。髪の毛一本引っ張ると,体全体が動くようなことを避けることができます。

実装例として、以下の図をご覧ください。 ControllerはIUsecaseのインターフェースに依存して、IUsecaseの実装となるUsecaseはIRepositoryのインターフェースに依存しています。

f:id:lvmingbei:20210923150200p:plain

依存性の注入

前のセクションで記述したように、各レイヤーはインターフェースなどの抽象を依存しているので、実現するにはDI(Dependency Injection, 依存性の注入)が必須条件になります。DIにより煩雑な初期化処理を解決するためには、DIライブラリの利用することが必要です。

JavaにはSpringというDIコンテナがとても有名ですが、GoのDIライブラリは、uber digや、google wireなども多数があります。その中にgoogle wireが利用者が一番多いと思われ、freee社内はwireを導入しているので、freeeカード Unlimitedはgoogle wireを選定しました。

開発者はインターフェースと注入したい構造体を指定して、wireはgo generateを利用して、初期化処理をコードを生成します。

WireでCardControllerをインスタンス化する実装例は以下となります。

package controller

var Set = wire.NewSet(
    NewCardController,
    wire.Bind(new(ICardUsecase), new(*usecase.Card)),
    usecase.NewCard,
)

type CardController struct {
    cardUsecase ICardUsecase
}

type ICardUsecase interface {
    FindCard(ctx context.Context, ID int64) (*domain.Card, error)
}

func NewCardController(cu ICardUsecase) *Server {
    return &Server{cardUsecase: cu}
}

package usecase

type Card struct {}
func (u *Card) FindCard(ctx context.Context, id int64) (*domain.Card, error) {...}

単一責任の原則

freeeカード Unlimitedでは、Usecaseはdomainのentityごとに分かれています。Usecaseは該当するentity以外のものを操作するのを極力避けています。

したがって、Usecase職責が明確になり、機能変更の影響を特定の部分に封じ込めて、他のUsecaseに影響を与えないロバストな構造にすることが出来ます。

f:id:lvmingbei:20210924160155p:plain

インターフェース分離の原則

インターフェースの利用者にとって不要なメソッドに依存させてはいけないというルールです。 このルールに従って、Usecaseは自分に必要なインタフェースのみを知ることができるようになっています。他のUsecaseの変更の影響を最小限に抑えることが出来ます。

IRepositoryインターフェースの実装となるDBHandlerはICardRepositoryやICompanyRepositoryなど複数のRepositoryインターフェースを実装していますが、Usecase側は依存しているインターフェース以外のメソッドが利用できないです。例えば、CardUsecaseはFindCard()だけが利用できます。

f:id:lvmingbei:20210923151510p:plain

感想

freeeカード UnlimitedではClean Architectureを基本的には採用していますが、実際の状況により、一部妥協したり、変更したりすることもあります。

良かった点

テストが書きやすくなりました。MockObjectを利用して、レイヤーごとテストができるようになりました。コードの品質向上ができたと思います。

また、インターフェースに対してプログラミングすることで、実装を変えても、他のレイヤーのビジネスロジックに影響しないようになりました。

インターフェースが分離しているため、各Usecaseの責務が明確になっており、機能変更と追加する時にどこを変更すべきか、またバグが発生した時にどこをチェックすべきかが判断しやすくなります。

改善したい点

インターフェースに対してのプログラミングすることになるのですが、開発の初期段階でインターフェースを全部洗い出すのは難しく、考慮が足りないこともあります。実装しながら、インターフェースの宣言を変更したり追加したりする頻度も高いです。インターフェースの変更と伴い、依存しているレイヤーの修正も発生しています。

Google Wire使ってセットアップしているので、プロジェクトの規模が大きくなると、各package毎にProvider Setを用意する必要があり、コードの構造が分かりづらくなります。DI未経験者には、学習コストが高いかもしれません。あと、新しい実装を追加する時、また依存関係を変更する時、初期化コードを作り直すことが忘れがちです。

次回

次の連載記事は、freeeカード Unlimited の開発序盤に金融チームに配属された新卒のsekkyくんの「新卒一年目からの新規プロダクト開発」になります!


金融チームでは、一緒に「freeeカード Unlimited」を開発する仲間を募集しています。 ベンチャー企業であるfreeeの中でも更にスタートアップ色が強い金融チームで、スモールビジネスの資金繰りにイノベーションを起こしましょう! https://freeecommunity.force.com/jobs/s/detail/a4l2r000000CaUpAAK