こんにちは! フロントエンド開発部の澁谷(shibe23) です。Creator's Noteには初投稿となります。
「レガシーフロントエンド脱却への挑戦」というテーマで各メンバーが投稿してきましたが、今回の投稿で一区切りとなります。
各メンバーの投稿はこちらです。
- 自前アーキテクチャなコードを Redux 構成に書き換えているお話
- 【Chatworkフロントエンドを大解剖!!】フロントエンド開発部に入社して3ヶ月が経ちました
- ウェブフロントエンドの設計力を高めるためにアプリケーションの構造を捉えてみる話
最後のテーマは、jQuery -> Reactへの移行にあたって特に重要な役割を果たしている、ACL(腐敗防止)層です。
「jQueryからReactへの移行ってどうやってるの?」という質問をいただく機会も多いので、今回の記事が参考になれば幸いです。
Chatworkのアーキテクチャの変遷は 自前アーキテクチャなコードを Redux 構成に書き換えているお話 に詳しく書かれているので、あわせてご覧ください。
巨大なアーキテクチャの移行に欠かせないACL層
Chatworkでは、React移行期にDDDベースの自前アーキテクチャを採用しています。 「ViewのライブラリをjQuery -> Reactに移行する」というだけでなく、モデル構造を含めたアーキテクチャ全体を段階的に変更しています。この段階的変更で活躍をするのがACL層の存在です。
DDDにおけるACL層はオブジェクトやアクションを異なるシステム間で変換するための防護壁のような役割を果たしています。Chatworkのフロントエンドでは、旧コードと新コードの依存関係を制御するために用いられています。
ACL層は2種類あり、依存性の方向によって使い分けています。
- FromOldACL ... 旧コード側から新コード側を呼び出す
- ToOldACL ... 新コード側から旧コード側を呼び出す
文章だけだとイメージが付きづらいので、こちら の図を引用します。
このACLと書かれた部分について、少し細かく記述をしたのが、下図となります。
※ Renderer以降のViewに関わる部分は省略しています。
boot
はアプリケーションのエントリーポイントで、OldProduct
を基点とした旧アーキテクチャのコード群が結びついています。
移行にあたっては旧コード側からFromOldACL
を経由して、新コード側の各種Serviceの処理を呼び出すことができるようになっています。
ToOldACL
は新コード側でinterfaceを用意し、実装を旧コード側で行っています。
もしinterfaceが無い場合は、下図のように直接旧コード側に依存するしかないため、循環参照が発生してしまいます。
interfaceを利用して依存関係を逆転させることで、新コード側をクリーンな状態に保つことができます。
サンプルコード
サンプルコードをCodeSandboxにまとめています。 ディレクトリ構造を確認するには、エディタ左上にあるメニューを選択してください。
index.ts
import { OldProduct } from "./old-code/OldProduct"; export const OLD = new OldProduct(); OLD.init();
index.ts
で起動処理として OldProduct
を呼び出しています。
old-code/OldProduct.ts
import { ToOldCodeImpliments } from "./ToOldACLImpliments"; import { ApplicationService } from "../new-code/ApplicationService"; import $ from "jquery"; export class OldProduct { private application = new ApplicationService(ToOldCodeImpliments); // 起動に関する処理 init() { // ... } // 新コード側の値を取得して処理をする doSomething() { const value = this.application.getACL().getNewService1Value(); // ... } // 旧コードに依存した値を取得する getSomeComponentPositon() { // ... return { x: $(".someContents").width() || 0, y: $(".someContents").height() || 0 }; } }
OldProduct
で新コード側の ApplicationService
に依存しています。
新コード側の処理を呼び出したいときは、this.apprication.getACL()
で取得した FromOldACL
のメソッドを呼び出しています。
ApplicationService
にはToOldACL
の実装を外部から注入するために、コンストラクタ引数として ToOldACLImpliment
を渡しています。
new-code/ApplicationService.ts
import { ToOldACL } from "./ToOldACL"; import { FromOldACL } from "./FromOldACL"; export class ApplicationService { private toOldACL: ToOldACL; private fromOldACL: FromOldACL; constructor(toOldACL: ToOldACL) { this.toOldACL = toOldACL; this.fromOldACL = new FromOldACL(); } // 旧コード側でFromOldACLにアクセスできるようにするgetter getACL() { return this.fromOldACL; } // 旧コードから受け取った値を受け取って処理をする doSomethnigToOld() { const oldPositions = this.toOldACL.getOldComponentPositions(); // ... } }
FromOldACL
のgetterや、コンストラクタ引数で受け取ったToOldACL
の実装を使ったメソッドを定義しています。
new-code/FromOldACL.ts
import { NewService1 } from "./NewService1"; export class FromOldACL { getNewService1Value() { const newService1 = new NewService1(); return newService1.getNewService1Value(); } }
新コード側のServiceに書かれた各種メソッドを呼び出しています。
new-code/ToOldACL.ts
export interface ToOldACL { getOldComponentPositions: () => { x: number; y: number }; }
旧コード側で実装するinterfaceを定義し、ToOldACLImpliment
でインポートしています。
old-code/ToOldACLImpliment.ts
import { OLD } from "../index"; import { ToOldACL } from "../new-code/ToOldACL"; export const ToOldCodeImpliments: ToOldACL = { getOldComponentPositions() { return OLD.getSomeComponentPositon(); } };
新コード側で呼び出したい旧コード側の処理をここに書きます。
まとめ
異なるアーキテクチャを並行して動かしつつリファクタリングするには、新しいコードがクリーンな依存関係になるように注意が必要です。 Chatworkでは下記のような工夫を施しています。
- 依存関係を循環させない
- アーキテクチャ間の境界を意識する
- 依存関係の制御にはinterfaceを活用する
一筋縄ではいかないことも多い反面、戦略を立てて大規模なコードを移行していく面白さはChatworkならではだと感じています。
フロントエンド開発部では、一緒にアーキテクチャの移行を推進してくれる仲間を募集中ですので、興味がある方はぜひご応募ください。
[中途採用] hrmos.co
[新卒採用] hrmos.co