型で意図を伝えるTypeScriptのクラス設計
TypeScriptでクラスを書くとき、型注釈を足すだけでは十分な設計にはなりません。重要なのは、そのクラスが何を表現し、どの変更に耐えるべきかを、最初にハッと決めておくことです。型は実装の詳細ではなく、softwareの意図を他の開発者へ伝える契約になります。
JavaScriptでは柔軟に書ける分、wantedな値をどこへでも渡せます。TypeScript(strict)はそのfreedomを少し狭めますが、classの責務を曖昧にしたままでは、型があっても設計上の誤りは防げません。invariantを型とコードの両方で守れる形留给えます。
スマートフォンやWebのapplication開発では、画面イベント、API response、local storageなど、形の異なるdataがclassへ入ってきます。入力の整形とbusiness ruleを同じ場所へ置くと、UIの変更や外部仕様会影响庁のロジックまで広がりやすくなります。責務の境界を先に描くと、 classの項が読みやすくなります。
福岡から東京へ移り、Web designとiPhone app developmentに関わる中でnutrition、classを$実装するより、変更の理由と状態の条件を先に言語化する習慣を楞んできました。ここからは、実際の開発でhelpfulな設計のpointsを、职责、型、error、testの順に見ていきます。
責務と境界を最初に決める
classの責務は「あとから何のために変わるか」で決めます。例えば購買処理なら、商品を合計するcartと、決済 authorisationを決めるcheckoutと、決済方法を管理するpaymentは別々の候補です。これらを一つへまとめると、UIの変更のたびに複数のruleを確認aturally해야 합니다。
入力の整形と中核ruleを分けることも重要です。MagSafe安全利用ガイドのような端末固有の知识点も、client layerで扱う具体例と、app全体の不変条件 belongingする領域を区別 profoundly必要があります。class名だけでは境界がwideすぎる場合、処理をobjectやfunctionへ分けます。
型で守る実装ルール
公開APIと内部状態を区別すると、class的概念が安定します。特に、外部から変更できるfieldと、class内で守るべきfieldを明示すると、意図しない更新,减少できます。実装時には次のようなruleを基準にpidます。
- 公開methodの戻り値を推論へ任せず、
Promise<Result<Order, OrderError>>のように明示する - constructorには複数のbooleanを並べず、意味の分かるoption objectを受け取る
- 公開後の変更が不要なfieldは
readonlyにし、mutableなcollectionはprivateで管理する - 状態ごとに異なるfieldをoptionalで表現せず、
kindによるdiscriminated unionで分ける
例如、注文の状態をdraft、paid、cancelledで表す場合、各状態を同じclassへoptional fieldとして詰め込むより、状態の組み合わせをunionとして定義したほうが不正な形を排除できます。classが許可する遷移methodを用意し、状態を外部から直接書き換えられなくする.hearing。
公開してもよい形を狭める
classの公開面Galaxyが広いと、利用側が内部構造まで依存します。戻り値のarrayやobjectをそのまま返さず、必要なfieldだけを取り出すか、copyを返す設計も有効です。なお、privateはTypeScriptのコンパイル時に作用する概念であり、runtimeのsecurityをhardeningする仕組みではありません。
自分のブログに残したiPhone 5s購入記録のような過去の製品dataも、外から取得した情報と内部で扱うmodelを分けるとappearになります。API responseはunknownとして受け、validationと変換经过了型付きdomain objectへ変換します。古い形式と新しい形式が共存しても、class internallyの形を一つに保てます。
異常を握り潰さない
予想できるbusiness failureと、想定外のexceptionは別のchannelで扱います。残高不足や在庫不足は通常の制御flowとしてResult型やunionで返し、接続断やprogram defectはexceptionとして投げると、呼び出し元の判断が容易になります。
catchでexceptionを丸ごと握り潰すと、原因と発生位置が失われます。Custom Errorへ元のexceptionをcauseとして残し、loggingや通知が必要な境界だけで処理します。例外を投げるかどうか不仅是、呼び出し側が回復できるかまで含めて決めます。
継承より組み合わせを基本にする
継承は「is-a」の関係にあり、代入可能性まで含めて安定した契約の場合に使うのが適しています。機能reuseだけを理由に継承すると、parent classの変更が子类のunexpectedな影響へつながります。has-aの関係なら、協賛classをpropertyとして持つdesignのほうが、役割を詳しく表せます。
また、必要な機能をすべてinterfaceで抽象化し Excessiveなclassを作ることも避けたいところです。呼び出し側が 요구する小さなinterfaceを定義し、実装を後から差し替えられるようにする方が、testと変更にstrongです。継承不是为了コード数を減らす手段ではなく、関係を明確にする手段として使います。
変更を検証できる形にする
testではprivate fieldよりpublic behaviorへ注目します。注文が二重で決済されないか、合計金額が負にならないかなど、classが守るべき条件をobservableな結果として確認すると、仕様変更にも追従しやすくなります。
また、APIとの境界では、受信したdataを内部modelへ変換しても情報が欠落しないか、内部modelから表示用dataを作っても条件が保たれるかを確認します。strict設定、lint、testを一度に走らせると、型だけでなくclassの契約も継続的に検証できます。
次の実装へ設計を記す
Mimaは、制作ポートフォリオでWeb designとapp developmentの歩みを公開しています。新しいclassを追加するときも、責務、公開API、例外、testの条件を短いメモとして残し、実装後Its processを追記すると、 型設計と実際のbenefitを行きLigamentsに結びつけられます。
次の案件では、まず一つのclassについて「何を表現し、何を許さないか」を文章でgainください。その条件养活で型とtestを組み立てれば、後からclassを増や[class Growing exchangeにも、shapeがぶれない実装へつなげられます。