良いコード悪いコードで学んだモデリングについて
目次
はじめに
こんにちは!フロントエンドエンジニアのAraiです!
今回は、プログラミングの本でよくおすすめされている、「良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方」の話をします!
本の中にあるモデリングの部分に、実際に活用できる良い内容があったので、その内容と活用例を紹介します。
モデリングとは
そもそも、「モデリングとは何か」を軽く紹介します。
調べた結果、こんな感じでした↓
目的に合わせて、具体的な情報から共通する部分をみつけ、また不要な情報を取り除くことで注目すべき情報を抽象的に定義したもの。
「抽象的に定義したもの」が、クラスや型にあたるイメージです。今回は型を例に出して説明を進めていきます。
良いモデルの考え方
目的駆動で定義されていること
目的別に1つずつモデルを定義した方が良いという話です。
説明します。
まず、1つのエンティティ(物体)でも、いろいろな用途で使われます。
例えば、ユーザー情報だったら、写真を投稿する時にも使われますし、投稿に対するリプライにも使われます。
目的ではなく、1つのエンティティ(物体)に対してモデル定義すると、Userという型ができますね。
User型の中に、写真を投稿する時のユーザー情報も、リプライする時のユーザー情報も詰め込むとしましょう。そうすると、Userに詰め込まれている情報の種類が、写真を投稿する用の情報と、リプライする時の情報の、2種類あるということになります。
このような状態で、リプライする時のユーザー情報の項目が1つ増えたとします。
User型には、リプライする時のユーザー情報の他に、写真を投稿する時のユーザー情報もあるので、修正する時に、どこを修正するのか分かりづらくなります。
この時、うっかり写真を投稿する時に使うユーザー情報の項目を変えてしまうというリスクがあります。
このようなリスクがあると、バグを埋め込む可能性が高いので、目的別でモデルを分けた方が良いということです。
1つのモデルが単一の目的で定義されていること
「目的駆動で定義されていること」と似たような考え方です。
1つのモデルを、複数の目的で定義してしまうと、複雑なコードになってしまいます。
1つ例を考えてみました。
例えば、インスタには、ユーザーがあります。
そして、ユーザー検索結果で使いたいユーザーの情報と、ポスト投稿で使いたいユーザーの情報があるとしましょう。
これらの複数の目的を、1つの型にまとめてしまうと、複雑になってしまいます。
検索結果で使いたいユーザー情報と、投稿で使いたいユーザー情報は違うので、
検索結果で使う場合に、投稿で使いたい情報は不要になるので、わざわざ排除する処理が必要になります。結構めんどくさいですよね。
どの情報が、どちらに使いたい情報なのか、分かりづらくなることもあると思います。
なので、1つのモデルにつき、1つの目的に定義しましょう。
モデルの見直し方
モデルの見直し方も本に書いてありました。
内容は以下です。
- モデルが達成しようとしている目的を洗い出す
- 単一目的単一モデルにしてモデリングする
- 目的駆動の命名をする
- モデルに目的外の要素が混ざっている場合、見直す
一言で言うと、目的駆動でやれってことですね。
また、なんとなく必要そうな要素を適当に入れたりしてしまうこともあると思うんで、そういったものが無いように見直すというのも大事とのことです。
実際に活用した例
ここから、実際に活用した例を紹介します。
1つの画面で、以下の2種類の講師情報が必要でした。
- シフト表に表示される講師情報
- シフト表内の講師検索機能の検索結果で表示される講師情報
悪い例
まずは悪い例です。
シフト情報と検索結果で使う型を、1つのInstructor型にまとめてしまう例です。
こんな感じです
1 2 3 4 5 6 7 |
export type Instructor = { id: number; name: string; role: 1 | 2 | 3 | 4; onlyShiftProperty?: boolean; // シフト情報だけで使う onlySearchResultProperty?: string; // 検索結果だけで使う }; |
シフト情報にだけ使う情報と、検索結果だけで使う情報が混在しています。これは管理がしづらいですね。。
解決策
悪い例だと、複数の目的が1つの型にまとめられているので、ここを改善します。
シフト情報と検索結果の2つの用途で分けて、型を定義してみましょう。
良い例
シフト情報用の講師と、検索結果用の講師で分けて定義をします。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
export type ShiftInstructor = { id: number; name: string; role: 1 | 2 | 3 | 4; onlyShiftProperty: boolean; }; export type SearchResultInstructor = { id: number; name: string; role: 1 | 2 | 3 | 4; onlySearchResultProperty: string; }; |
悪い例だと、1つの型につき、onlyShiftPropertyが必要な場合と、onlySearchResultPropertyが必要な場合がどんな条件なのか、無駄に頭を使うことが増えてしまいます。
が、
良い例の場合だと、型を2つに分けているので、そんなめんどくさいことを考える必要がなくなりました。
オプショナルの型がundefinedの時の処理も必要なくなるので、コード量も減るでしょう。
共通部分が多いので、omitとかpickとか&を使うのも良いかなと思いましたが、omitとかpick元の型の内容が変わったときに影響してしまうのが嫌なので、別々で定義した方が個人的に好きです。
こういうのは本読んだ方がいい
考え方とか設計手法的なものは、ググったり技術記事をさらっと読むだけだとあんまり定着しないのですが、本を読むと割と頭に残る感じがすると思いました。
また、なんとなく本読んで終わりじゃなく、実際に活用できてよかったです。