The Clean Architecture Components概要
序文
React(Vueも然り)でアプリケーションを開発する際、atomic designを採用した場合大抵問題になるのがatomic desingにおいて、非同期処理や状態管理をどこで行うかというの問題です。
コンポーネント設計に関しては現在も主流はatomic design、非同期処理や状態管理については選択肢が多々あると思いますが、最近だとRedux(Redux toolkitなど)やRecoil、React Query、SWRらへんのどれかを使ってる人がほとんどなんじゃないかと思ったりします。ただこれらの選択肢を組み合わせた際のベストプラクティスについては未だ唯一解は存在しない状態で、例えばReduxとの繋ぎ込み(connectやuseSelector等)を「organismsで繋ぐべき」と言う人もいれば、「pagesで繋ぐべき」「atomsで繋ぐことも許容すべき」という人もいるなど、現在も様々な論争がされているため初学者を混乱させる一因になっています。
The Clean Architecture Componentsはこれらコンポーネント設計と非同期処理や状態管理の問題を含んだフロントエンド設計を、Clean Architectureの思想を用いてコンポーネントを中心に再設計を図る試みです。
図1
本記事ではこれまでの問題点とこの再設計の概要を解説します。
従来の設計問題
まず従来の問題点として僕が問題視してる以下の点について整理していきます。
- atomic designの粒度が人による
- コンポーネントの依存が5段階と深く、バケツリレーな実装になりがち
- atomic designと状態管理・非同期処理ライブラリの親和性が悪い
atomic designの粒度
atomic designは独特の命名をしていますが、organismsやmoleculesがどの程度の粒度を表現しているのかがわかりづらいと言う問題があります。これはBEMやSMACSSといったCSS設計でよく起きていた議論に似ており、「何を持ってコンポーネントとして大きいと判断するのか」というのが個人の感覚による判断になりがちなため難しいところです。
特にWebアプリにおいては、見かけが小さくとも機能が複雑でコード行数がとても多くなるといったケースもあるので、px数・振舞い・コード行数など、いくつかの判断基準を絡めて総合的に判断する必要があると思います。
また、レイアウト層がないことによって、似たようなレイアウトを組みたいときに重複したコードが量産されるのも悩ましいところです。
コンポーネントの依存の深さ
atomic designに忠実に作ったWebアプリは、あらゆる動的な情報をpages->templates->organisms->molecules->atomsとバケツリレーを実装することになり、軽微な修正でも影響範囲のリレーロジックを4回修正する必要に迫られたりします。
これはClean Architectureで出てくる依存性逆転の原則を適用しなかった場合の依存ツリーにそっくりで、親の修正がどこにどのように現れるか見えづらいという問題を生んでいます。
atomic designと状態管理・非同期処理ライブラリの親和性の悪さ
個人的にはatomic designと状態管理の問題について、React / Redux を実務で使うということはという記事にあった以下の説明が今のところしっくりきてたりします。
AtomicDesignがワークするということは、React/Reduxの設計パターンに反しているということになるからです
「グローバルな物は祖先に逃そう」というatomic designと、「グローバルな物はコンポーネントの外におこう」というReduxなどのパラダイムは衝突してしまうのです。
僕がよく使っていた設計
とはいえ、atomic designはReactのコンポーネント設計として主流な設計であることも事実なため、ある程度ゆるく導入する分にはメリットも大きく、僕の場合は以下のような構成で開発することが多かったです。
-
src/components
配下にはpages, templatesはなしのatomic design+layouts
- Redux/Recoilなどの利用はorganismsで行う
- pagesはフレームワーク(Next/Gatsby)に合わせ、SPAの場合は
src/index.tsx
にrouterの処理を記載する
├── components
│ ├── atoms
│ ├── molecules
│ ├── organisms
│ └── layouts
├── pages
├── redux(recoil)
├── styles
└── lib
├── api
└── utils
この構成でやることでatomic designと状態管理ライブラリの問題については、ある程度秩序を持たせることに成功し、UIの修正とアプリケーションロジックの修正を切り離して行うことはできました。
ただ、この構成も結局はオレオレatomic designでしかないのでatomic designの粒度の問題はずっと感じており、そもそもアプリケーションロジックと切り離してコンポーネント開発することでバケツリレーが発生しがちなため、フロントエンド全体を見て再設計した方が良いのではという気持ちが強くなっていきました。
The Clean Architecture Components
これら前述の問題点について、Clean Architecture的アプローチでコンポーネントの再設計を試みたのが、最初の方に図で貼ったThe Clean Architecture Componentsです。
具体的には、先に貼った僕がよく使っていたディレクトリ構成に以下のような変更を試みています。
- atomic designの粒度がわかりにくい
- atomic designと状態管理・非同期処理ライブラリの親和性の悪さ
-> 命名を全体最適な形に変更
- コンポーネントの依存が5段階と深い
-> 依存の方向を逆転させ(依存性逆転の原則)、APIやブラウザの機能含め再整理
コンポーネントだけで言うと、以下の図の赤枠がコンポーネントになります。
図2
長々と書いてしまってますが、ディレクトリの構成例でみた方がわかりやすいかもしれません。
├── components
│ ├── atoms
│ ├── features
│ ├── contents
│ └── layouts
├── pages
├── redux(recoil)
├── styles
├── client
│ └── polyfil
└── lib
├── storages
├── fetcher
└── utils
ここからは各レイヤーについて1つづつ解説しようと思います。
Clean Architectureの本質
よくClean Architectureで出てくる以下の図は、The Clean Architecture(Clean Architectureの一例)です。
名称 | 意味 |
---|---|
Clean Architecture | Clean Architectureというアーキテクチャがあるわけではない。綺麗なアーキテクチャにおける一般原則は同じであるという考え方。 |
The Clean Architecturee | 以下の図、クリーンアーキテクチャ的な例の1つ。 |
ちなみにこの例ではUIをひとまとめにして変更容易な状態にしようとしてますが、現代のWebアプリはUIとAPIに別れてることが多く、インタラクションに応じてAPI通信を行う必要があるため、この図とは相性がよくないと思ってます(この構成がよくないと言いたいわけではありません、実際APIを作るときは僕はほとんどこの構成で実装します)。
ではClean Architectureとはなんなのでしょう?
ここで詳細を書き始めるととても終わらないので自分なりに超要約すると、Clean Architectureの本質はこれだけです。
- アーキテクチャとは、関心の分離を目指すことである
- 関心の境界線では、必要に応じ依存性逆転の原則に従う必要がある
原著ではもちろんこれ以外にも、これらを達成する上で出てくるいくつかの原則やHowが出てきますが、基本はこれらを達成するための話です。
図の見方
さて、Clean Architectureの重要な部分もわかったので今回の図1、図2を見ていくわけですが、この図の見方は本家The Clean Architecture同様「外側は直近の内側のみ依存する」「矢印は依存の方向を表す」としています。pagesはcontentsやlayoutsに依存してて、contentsやlayoutsはfeatureに依存してて...といった具合ですね。
コンポーネントの境界の超え方
「内側に向かってのみ依存する」とは言っても、図を見ると「atomsがfeaturesに依存してる」というのがどういうことなのかよくわからないという方も多いと思います(featuresはコンポーネントのレイヤーです、詳細は後述)。
ここでまさに依存性逆転の原則が出てきます。これも詳細書くととても長くなるので超要約すると、「利用者側がInterfaceを決定し、詳細がInterfaceを実装する」ことで、どう使われるかに依存する状態を作ることです。
例として、カウンター機能を持ったコンポーネントを実装するとします。
パッと思いつく実装だと、こんな感じの実装が多いかと思います。
// features
const Counter: React.FC = () => {
const [count, setCount] = React.useState(0)
const onCountClick = () => setCount(count + 1)
return (
<>
count is {count}
<CountButton onClick={onCountClick} />
</>
)
}
// atoms
type CountButtonProps = {
onClick: () => void
}
const CountButton: React.FC<CountButtonProps> = ({ onClick }) => (
<button onClick={onClick}>count</button>
)
ここでatomsとfeaturesのPropsの依存の方向を逆転させてみます。
// features
type CounterButtonProps = {
onClick: () => void
}
const Counter: React.FC = () => {
const [count, setCount] = React.useState(0)
const onCountClick = () => setCount(count + 1)
return (
<>
count is {count}
<CountButton onClick={onCountClick} />
</>
)
}
// atoms
const CountButton: React.FC<CounterButtonProps> = ({ onClick }) => (
<button onClick={onClick}>count</button>
)
これで、atmosがfeaturesが求めるPropsを受け取るコンポーネントを実装する形になりました。
この実装だけ見ると何が嬉しいかちょっとわかりづらいかもしれませんが、atomsの利用ケースが増えた実装が増えてきたときに、どういった利用ケースを網羅してればいいかがより明確になります。
type Props = {
onClick?: () => void
disabled?: true
}
const CountButton: React.FC<Props> = ({ onClick, disabled }) => {
if (disabled) {
return <button disabled={disabled}>count</button>
}
return <button onClick={onClick}>count</button>
}
このくらいの分岐ならわかりますが、propsが10個とかになってくると利用ケースが想定仕切れなくなってきますね。
type UseCase1 = {
onClick?: () => void
}
type UseCase2 = {
disabled?: true
}
const CountButton: React.FC<UseCase1 & UseCase2> = ({ onClick, disabled }) => {
if (disabled) {
return <button disabled={disabled}>count</button>
}
return <button onClick={onClick}>count</button>
}
依存性を逆転させるとどういったPropsのケースを担保しなきゃいけないか明確になり、このUse Caseは機能によるので利用者側(features)で定義していると言うことです。やってることは同じで書き方変えただけですが、この知る範囲を限定するためにどこで型を定義するかということこそが、Clean Architectureの本質の関心の分離を実現する手段になります。
実際にはコンポーネントのデザインとかにも依存してきますが、デザインをTypescriptで抽象化する手段が思いつかなかった+重要なのはどういう機能を持っているかを抽象化することだと思い、こういったアプローチを取っています。
では次に、それぞれの要素について内側の円から順にみていきたいと思います。
Application State
まず円の一番中心にはApplication Stateが存在します。Webアプリケーションにおいてグローバルなアプリケーション状態管理が必要になる場面は多々あります。
これはReduxでもRecoilでもContext APIでもいいと思いますが、ここにAPIのキャッシュを含むかどうかは微妙なところで、個人的には反対です。APIのキャッシュはAPIにより近いところでだけ知ってればいいと思うので、fetcherなどで行えばいいと思ってます(キャッシュもアプリケーション状態の一種なのはわかるのですが、APIキャッシュってどちらかと言うとメモ化の一種として扱う方が楽な気がしてるので、個人的にはfetcherの方がしっくりきます)。
なのでSWRとか使っててAPIキャッシュ以外にグローバルな状態管理が不要という方はここの層はなくてもいいかもしれません。
Application Features
アプリケーションの機能として独立したレイヤーです。atomic designで言うところのmoleculesやorganismsとReduxなどの繋ぎ込みを合わせて、独立したコンポーネントを持ちます。ここで言う独立とは、Propsを持たないということではなく、「Propsに応じて非同期処理やグローバルな状態管理にアクセスし機能を実現する」コンポーネントであることを意味しています。
このレイヤーはatomsの変更やfetcherの変更の影響を受けないよう、依存性を逆転させています。
これは本家The Clean ArchitectureのUse Case同様、機能を成り立たせるための実装の詳細を分離させることを目的としています。
Application Adapters
The Clean Architecture同様、アプリケーションと外界(ページ、ブラウザの機能、ストレージやAPI、etc)とを繋ぐレイヤーです。
後述の外的要因による影響範囲を止める役割をになっています。
ブラウザのAPIのpolyfil読み込みなども、この層でのみ行うことで影響範囲を明確にしましょう。
Extenal Factor
The Clean Architecture同様、一番外側には自分たちで制御しづらい外的要因因子で構成します。
現実世界ではブラウザは毎月のように何かしら更新があり、ベンダーごとに実装も違ったりします。また、APIも変更するたび影響範囲が広く調査に苦しむと言ったケースが多々あるかと思います。更新頻度が高かったり、影響範囲が広大になりがちなものほど依存の円の外側に置くことこそが、変更容易性の高い状態を維持する上で重要になってきます。
ちなみにpagesは自分たちで制御しやすい部分だと思われるかもしれませんが、実際にはSEO的URLの制約などがあることも多いかと思われるのと、アプリケーションへの入り口となっているため最も外側に配置しています。
まとめ
今回まとめた、このThe Clean Architecture Componentsは、これが至高の形とは思ってませんし、まだまだ改善の余地があるかと思います。ただatomic designなどの従来の設計問題については、多少解決を得られたんじゃないかと言う気がしてます。
また今回、いったん概要として出そうと思って書き始めたもののだいぶ長くなってしまいました。
(Clean Architectureについて語るだけで記事1つかけてしまうので、それを交えて記事を書くのはとても大変でした、、、)
ただ参考実装のリポジトリとかも用意できなかったし、長いんでだいぶ削ったところもあるのでやる気が持てば、今後もう少し詳細詰めて続編かけたらと思っています。
間違いなどあればご指摘いただけると幸いです+改善点もあると思うので、ご意見・ご感想もお待ちしております。
長文でしたが最後まで読んでいただきまして、ありがとうございました!
Discussion