概要
友達と開発しているChrome拡張の設計話です。
Chrome拡張は、以下の2つの構成で成り立っています。
- 特定のウェブページのコンテキストで実行される
contents script
- Webページ上に記述されたスクリプトをWebブラウザに組み込み、ページを離れた後も実行できる
service worker
このcontents script
にUI層の処理、ドメイン層の処理が混在して肥大化していました。
また、utilsフォルダに色んなものが詰め込まれていたりしていました。
これらをドメイン駆動設計(DDD)をもとに肥大化したパッケージを爆殺させて(リファクタリングして)、責務がわかりやすいコードにしました。
Chrome拡張の紹介
MokurenというGitHub上でIssueやPullRequestをサイドバーで開けるChome拡張機能を開発しています。
Mokurenについては以下をご覧ください。
友達と個人開発したChrome拡張が便利すぎた
パッケージの肥大化という課題
先にも述べたように以前のMokurenはパッケージが肥大化しているという課題がありました。
具体的には以下のようなフォルダ構成になっていました。
※ Mokurenは、Vue.js + TypeScriptを使用しております
Before
src
|-apis
|
|-contents_script
| |-detail
| | |-*.vue // UI
| |-detail.ts // サイドバーを開く処理とGitHubのIssueの処理
| |-index.ts // contents_scriptのメイン処理
| |-pr.ts // GitHubのPRの処理
|
|-components
| |-*.vue // UIコンポーネント
|
|-models
| |-*.ts // GitHubやMokurenのモデルクラス
|
|-store
| |-*.ts // store(状態管理)
|
|-style
| |-*.scss // SCSS(CSS定義)
|
|-utils
| |-*.ts // ごちゃ混ぜの色んな処理がたくさん
|
|-App.vue
|--background.ts // service workerの処理(バックグランド処理)
|--main.ts // Vueインスタンスの作成処理
|--manifest.json // Chrome拡張の設定ファイル
以下のような パッケージ肥大化の問題があり、どこに何の処理があるか分かりづらい状況です。
- contents scriptパッケージ内に「UI層の処理」と「ドメイン層の処理」が混在している
- componentsパッケージにドメインに関する処理がある
- utilsパッケージに色んなものが詰め込まれている
- src/contents_script/details.tsの中に「サイドバーを開く処理」と「GitHubのIssueの処理」の二つの処理がある
- modelパッケージには、GitHubのモデルとMokurenのモデルの両方が並列に置かれている
設計
上記の課題を認識し、一緒に開発している友達とクリーンアーキテクチャやドメイン駆動設計を勉強し直しました。
よりDDDらしい設計でリファクタリングを行うことに決めました。
リファクタリングを行なって、下記のフォルダ構成に至るまでに以下のことを行いました。
- ユースケースの洗い出し
- UIとロジックの分離
- utilsフォルダを爆殺
After
src
|-background
| |-index.ts // service workerの処理(バックグランド処理)
|
|-contents_script
| |-index.ts // contents scriptの処理 (コントローラーを呼び出すだけ)
|
|-domain
| |-github
| | |-model
| | |-client // GitHubに関するAPIインターフェース や GitHubのDOM操作インターフェース
| |-mokuren
| |-model
| |-client // モクレンに関するAPIインターフェース
| |-repository // モクレンに関する永続化処理インターフェース
|
|-infrastructure
| |-client // domain層で定義されたAPIインターフェースの実体
| |-repository // domain層で定義された永続化処理インターフェースの実体
| |-store // store(状態管理)
|
|-ui
| |-components_creator // DOMにUIコンポーネントを挿入するクラス
| | |-*.ts
| |-components // UIコンポーネント
| | |-*.vue
| |-page // UIページ
| | |-*.vue
| |-style
| |-*.scss // SCSS(CSS定義)
|
|-usecase
| |-*.ts // ユースケース処理
|
|-config // DIコンテナ設定
|
|-manifest.json // Chrome拡張の設定ファイル
ユースケースの洗い出し
ユーザーとソフトウェアの間の相互関係を起こすアクションを定義しました。
実際に文章にして具体化してみると、どのようなモデルを作ればいいのかが分かってきます。
また、ユースケースごとに開発が進められるため、ユースケース層を見れば開発状況を把握することが可能となりました。
mokurenでは、以下のようなユースケースになりました。(他にもありますが、一部のみ抜粋しています)
ここで挙げたユースケースがそのままユースケース層の処理となります。
utilsフォルダを爆殺
utilsフォルダは名前の通り「役に立つもの」みたいところで、とりあえずここに入れとけば間違いないみたいな状態になりがちです。
そうするとutilsパッケージは肥大化してきてしまいます。
今回のリファクタリングでutilsフォルダを廃止して、適切な場所へ配置しました。
どこか分からないものについては、新規にパッケージを作ったりしてパッケージを見て何に関する処理か分かることを目指しました。
UIとロジックの分離
クリーンアーキテクチャやDDDのよくある理想系は以下の図になります。
自分たちもこの依存関係を参考に実装に落とし込みました。
Mokurenの一部の機能のみ以下に図示します。
UI→UseCase
components(UI層)からは、クリックした時にUseCaseを呼ぶのみのロジックしか持ちません。
UseCase→Domain
ドメインの処理を組み合わせてユーザーのユースケースの挙動を実装します。
Infrastructure→Domain
InfrastructureからDomainへの依存は、Client(APIインターフェースなど)やRepository(永続化処理)のinterfaceを継承しているのみです。
例外
分かりづらいかもしれませんが、基本的には依存関係を上図を意識していますが、一部例外を作っています。
それは、UseCase層がcomponents_creator(UI層)を呼び出すことです。
components_creator(DOMにVueコンポーネンツを埋め込むパッケージ)はUI層に置いています。MokurenはChrome拡張です。なので、どうしてもDOMにUIを直接埋め込むという処理をしなければなりません。
UseCase層にinterfaceを作り依存関係逆転させることも考えましたが、冗長になりコードが分かりづらくなるデメリットを天秤にとった結果、UseCase層がUI層を依存することを許容することに決めました。
UI層には、Domainのロジック(業務知識など)入れず、Domain層を開発の中心にすえ、コードやコミュニケーションを常にDomainモデルと一体化させました。
このようなモデルがどのような振る舞いをするのか、仕様変更時にどう変わるのか。常に最新のより表現力の高いモデルを追求し、コードを追従させていくことにより、MokurenのDomainの理解を深め、Mokurenの価値を追求できるようになりました。
まとめ
今回はChrome拡張にDDDの考え方を適用して設計を行うことで、肥大化したパッケージを爆殺しました。
以前に増して、開発しやすくなったと実感しています。
他にも単体テストやE2Eテストの作成や、StoreとRepositoryの連携など試行錯誤しながら開発を行っています。
Mokurenの価値(コア)と向き合いながら、毎週機能開発を進めてアップデートしております。
そんな素晴らしい拡張機能を是非インストールしてみてください!
ログインや初期設定などは要りません!ストアからダウンロードしてすぐに使えます!便利です!
MokurenはUI/UXにもこだわっています。気になった人は以下noteも見てみてください。