コドモン Product Team Blog

株式会社コドモンの開発チームで運営しているブログです。エンジニアやPdMメンバーが、プロダクトや技術やチームについて発信します!

私がクラスを作るときに考えること

 こんにちは。プロダクト開発部の関口です。皆さんはシールを集めてますか?私は集めていません。ですが長女と次女が集めまくるせいで私のお財布からは毎週お金が飛んでいきます。昨今のシールはボンドロとかおはじきとかタイルとかシャカシャカとか水入りとか、色々あるみたいですよ?余ったシールを会社貸与の私のmacに貼ってくるので、私の「mac book」は「シール book」になりつつあります。

 さて、今日は私がクラスを作るときに考えることをつらつらと述べてみようと思います。もう生成AIが全部作ってくれるからそんなこと考える必要ないんだよという向きもあると思いますが、誰かの何かの参考になれば幸いです。

うまくできたときに何が嬉しいか

 どういうことをモチベーションに設計や実装をうまくやろうと思うのか、私の場合を言語化してみました。

ユニットテストで担保できる部分を最大化できる

 ケースが増えるほどに伸びるCIの時間、落ちた原因を追及する際のつらみ、セットアップを書くのに散々苦労したけど本当にこんなエッジケース必要?という悩み…。生成AIの登場でインテグレーションテストを書くハードルはだいぶ下がりましたが、依然その作成やメンテナンスの苦労は筆舌に尽くしがたいものがありますよね?それに対してユニットテストは「早い安い旨い」が三拍子揃った至高のテストです。テストサイズを小さく保つメリットは変わっていません。ユニットテストであれば、生成AIが特にドキュメントを作り込む必要もなく対象クラスのコードを読ませるだけで秒で精度高く作ってくれます。適度なモックや抽象化で自動テストのメンテナンスのつらみを抑える。何よりもまずこれがクラスをうまく作りたい第一のモチベーションになります。

決定を適切なタイミングまで遅延できる

 未決定部分や難所、変更の手間がかかる部分を局所化しておけることもよいことのひとつです。実装していけば、当初想定の誤りに気づいて変えたくなることもたくさん出てきます。適切なパッケージングやクラスの設計は手戻りを減らし、進め方に柔軟性をもたらしてくれます。

認知負荷が減らせる

 誰が読んでもスラスラわかるように作るのは至難の業です。自分が作ったものでさえ、2ヶ月後に見返すと何も分からなくて愕然とするなんてことはよくあることです(私はもう「そういうものだ」と諦めています...)。だからと言って認知負荷を減らす努力に意味がないかといえば、そんなことはありません。名前付けが理解の助けになるように作られたものの方が、そうでないものより読み手の理解をより早く楽に進めてくれるはずです。同じように、一つの大きな箱の中で課題が全て解決されているよりも、適度に課題が分割されてそれぞれが適切な場所で独立して解決されているような作りの方が、読み手は早く無駄なく探し物に辿り着くでしょう。
 適切なパッケージングやクラス設計、名前付けを考えることは解きたい課題のコンテキスト分割 / 整理 / ラベリングする作業に等しいものです。小さいコンテキストは人だけじゃなく生成AIにも優しい。人にも生成AIにも優しい作りを目指したいですね。

最大の勝負は作り始める前

 「エンジニアなんだから作るのが勝負だろう?」と思う向きもあるかもしれませんが、私は最高のものづくりを目指すのが最善ではないと思っています。最善はそもそも作らないで済ますこと、次点はとにかくシンプル。頑張らないために最大限頑張るべきなのは作る前です。

作った後のメンテコストを侮ってはいけない

 生成AIの登場により作るのが容易になったとはいえ、無駄なモノを作ってしまうのは時間とリソースの浪費です。作ったものは消すまでの間ずっとメンテナンスしていかなければなりません。しかし作らなければ、未来永劫そのコストは発生しません。「いやいや、自動テストさえしっかりしていればメンテコストなんて…」というご意見もありそうですが、残念ながら私が観測してきた事実は自動テストもこの方面の解ではないことを告げています。自動テストは変更頻度に比例してメンテコスト削減効果がありますが、それ自身に作成やメンテのコストが発生します。自動テストがあることはほぼ間違いなくよいことですが、それがあるからといってメンテコストを過小評価しないよう注意しましょう。
 ましてや昨今は製品の脆弱性だけでなくOSSの侵害を通じて製品のサプライチェーンや開発者のローカルに至るまでが攻撃を受けるご時世です。何かを作ってそれを世に出すということはそれだけ多くの防御側面を攻撃者に披瀝することにもなるわけで、そこを守り抜くコストも勘案していく必要があります。EOL駆動で数年おきに仕方なく諸々をアップデートしていた無邪気なあの頃はもう遠い昔のことになりました…さらにダメ押しで、公開してユーザーが使ってくれるようになった機能やサービスは、閉じるのにもいろんな方面でそれなりの労力かかることも付言しておきます。  まずはどう作るかよりも作る必要があるのか、作らなくて済ませる方法がないかどうかをちゃんと検討しましょう。

作るなら最大限にシンプルに

 作るのが最善となったら、シンプルに済むよう調整できる軸を検討し尽くしましょう。シンプルであればあるほど、(私の体感では)指数関数的にメンテナンスコストは低減します。ユーザー、組織、開発の運用など、あなたが知っているコンテキストを総動員して臨みましょう。知らないコンテキストに気づくために、色んな関係者や有識者に相談するのも手です。みんなが嬉しい「三方よし」を目指しましょう。
 作られたものは将来に渡って開発組織の体力を奪い続けます。エンジニア数が増え続ける組織でもない限り、作るという決断は一定、未来の機動力に制約を課す選択でもありますので、慎重でありたいものですね。

進め方

設計は最初から完璧にできない

 設計と実装は相互作用的な関係です。実際にできたものから得られるフィードバックを大事にしましょう。見落としや改善点が見つかったら素早くそれを反映することをためらわないようにします。設計とは仮説を立てる作業であり、無謬(むびゅう) の真理を構築する作業ではありません。仮説は例えそれがどれほどもっともらしくとも、実装で覆る可能性が大いにあります。そもそも設計していなかった部分に気づくこともあるでしょう。それは悪いことではありません。実装で見つけた事実を設計にフィードバックすることで、作っているものはより現実に即した形に進化します。誤りを見つけに行きましょう。「思った通りに行かなかった」を楽しみましょう。
 設計で迷うこともあります。例えば、オプションが複数あってどれがいいのか話しているだけではなかなか見えてこない状況です。そんな時は思い切って実装してみましょう。設計の議論だけで分からないことは実装してみればわかることがあります。例え設計が全部終わってなくても、設計の手を止めてその部分だけを仮組みして検討するだけでも得られるものがあるかもしれません。

サンクコストと正しく向き合う

 設計や実装を進めていると、手戻りしてでも「こうすべきだ」とか「こうした方がよい」という気づきが出てくることがあります。今までの積み重ねをサンクコストと割り切る検討は辛いものですが、勇気を持って立ち戻る選択を検討しましょう。特に長期的に保守が見込まれるものは、安易に目先の速さを選ぶと永きに渡ってペインを背負い込む危険があります。未来の「あの時…」は多分今です。目を瞑らずに声を上げてちゃんと評価しましょう。
 なお、サンクコストは一歩の歩幅が大きいほどに大きくなりがちです。普段から小さな一歩の積み重ねでゴールを目指すように心がけましょう。一歩の歩幅を小さくするのは一見簡単なようですが、意外と奥の深い職人芸です。考え始めるとパズルみたいに楽しめますので、普段は完成品を一発でお披露目するスタイルが多い方にも是非挑戦してみてほしいです。大きな一歩に注意して、一歩を小さくするための工夫をしましょう。

手仕舞いのタイミングを適切に判断する

 エンジニアはこだわりたい生き物です。時間を忘れて悩みがちです。現実の制約を思い出しましょう。明後日の方向にドライブしてしまったこだわり脳を正道に戻すための私のメソッドをご紹介します。

今必要なことだけを実装する

 一般的にどうあるかは関係ありません。課題に対して最小限必要なことだけを実装しましょう。例えば、バックエンドに外部システムとのやり取りを追加するためにAPIクライアントの実装が必要になったとします。今回の要件で必要なのはURL指定のみが必要なGETリクエストだけです。この場合はURL(あるいはbase urlをインスタンス変数に保持するならURI)だけが必要なGETメソッドを用意します。POST/PUT/PATCH/DELETEは必要ありません。クエリパラメータやオプションも必要ありません。たとえ一般的なAPIクライアントが標準的にそのような機能を備えていたとしてもです。キャッシュ機能を実装しようかと迷うかもしれません。その場合にやることは、まずそれが本当に必要か、そもそも懸念に対して有効なアプローチなのかの検証です。不必要な実装は不必要にシステムの運用コストを増加します。いるとわかっていることだけを実装しましょう。

大事なことにフォーカスする

 大事なこととそうでもないことを見極める眼を養いましょう。ペアプロしていると、コードの見やすさや実装スタイルなど、人によって最善解が異なる問題で議論が白熱することがしばしばあります。ユーザーは待っています。ユーザーの価値に結びつかない議論や修正はステークホルダーみんなの時間を無駄にします。時には寛容になりましょう。目の前の議論が、本当にステークホルダー全体を見回して時間をかける価値があるかないか。価値がなさそうな問題に時間を割いているなと思ったら、議論の重みづけやタイムボックスの切り方を見直したり、第三者に相談するなどの決着の仕方を工夫するといいかもしれません。

未来に託す

 目先は問題ないが、一定考えてどうしても納得できる解が出なかった場合は、最終手段としてFIXMEなどを書いて未来に託しましょう。きっと誰か(未来のあなたかもしれない)がいい案を出してくれます。些末な問題だったとしたら未来の誰かがそっと消してくれるでしょうし、実はそのあと二度とメンテされないなんてこともあるかもしれません(だとしたら無駄に悩まずに済みましたね!)。

煮詰まったら、寝かせよう

 これは完全にn=1の話かもしれませんが、「煮詰まったな」と感じたら一旦問題を脇に置いて別のことをしましょう。「煮詰まる」とは頭に焼き付いてしまった同じ回路をずっと行き来している状態です。この状態に陥るといくら時間をかけてもブレイクスルーは生まれにくいです。勇気を持って寝かせましょう。私の場合はウェイトリフティング中や子どものお迎えの自転車が仕事から離れて一人になれるいい機会になっています。

戦術の話

 ここからは細かい戦術の話になります。ご参考になるものがあれば幸いです。

いきなり部分に飛びつかない。最初に全体を見る

 ものづくりにおける私のメンタルモデルは、本棚などのDIYのイメージに近いです。パーツ全体を仮止めして全体を見た後、各所のネジを締めに行くのですが、一箇所ネジを締めると他の箇所がズレたり緩むことがあるので、何周かして全体的に整えていく。そんなイメージです(下手な表現でアレですが、伝わりましたでしょうか?伝わっていたら幸いです…)。
 何が言いたいかというと、部分部分を作り込んでボトムアップで全体を完成させるアプローチは手戻りや無駄が多くなる可能性が高いということです。設計と実装は相互作用的だと前項で申しましたが、ある部分を実装する中で発覚した設計の誤りが別の部分に影響を及ぼす、ということはよくあることです。すでにできてしまった部分で見つかる誤りは、まだできていない部分の誤りよりも確実に高いダメージを生みます。例えば、今思えば私がよくやっていた、「先にDBのテーブルやコアなドメインロジック、リポジトリの実装を作り込んじゃう、なんならテストのカバレッジもバッチリ!」みたいなアプローチを想起してください。もし、ユーザーインターフェースに誤りがあってその修正のためにテーブル定義に変更が余儀なくされる、なんてことが起きた日には目も当てられないですよね?あの頃の私は「自動テストなんて作るほど手戻りコストが倍増する苦行」みたいなことを思っていましたが、なんのことはない、そりゃ自ら苦行を望むかのような進め方をしていたからなのです…
 私のアドバイスはただ一つ、いくら完璧なドメイン分析から生まれた設計であっても、ほぼ確実にその設計(なんならその前のユーザー分析に誤りがあるかもしれない)は現実を前にしてなんらかの修正が必要になるだろうから、最初は仮組みだけにしてネジを締めるなということです。以下ぐらいをやれば十分でしょう。

  • 登場人物を並べてインターフェースやメソッドのAPIを設計する
    • ユーザーのインターフェース(画面ならガワ、APIサーバーならOpenAPI、バッチならCLI)やusecaseの設計ぐらいまでの浅いところで最初は十分
  • 命名や依存関係の妥当性などを確認する
  • 繋げて動きそうなことを確認する

 ユーザーに近いところからやっていくところがミソです。確認しやすいし、込み入った設計も必要ない。早い段階で修正点を見つけることができます。(ただしデータモデルが支配的なドメインや、技術的実現性そのものが最大のリスクになる領域など、このアプローチが当てはまらない場面もありますのでご承知おきください)

実装「していく」

 ある程度見通しができたからといって、いきなり実装「しきる」(=完成を目指す)のはお勧めしません。まずは一通りを流し切れる一番簡単な実装を用意して、可能ならそれでもう一旦本番リリースしましょう(本番がダメなら検証環境など)。最初は全くボタンが機能しない画面でもいいし、何を与えても固定のレスポンスしか返さないAPIでも、開始終了のログだけを吐くバッチでも構いません。feature toggleなどで開発者だけが見られる状況があるなら本番環境でも安心です。本番環境で確認できることはカットオーバー時の不安を大きく取り除きます。
 あとはユーザーに近いところからテストと共に実装して、どんどんリリースしていきましょう。一定実装できた部分はPdMなどに早めに確認してもらってフィードバックを受けるのがよいです。もしインフラに近い部分で不確実性の高いものや実現性に疑問符のあるものがあれば、早めにプロトタイピングして実現可能性を担保しておきましょう。疎通できる部分からはその部分にインターフェースを切って単純な実装で仮置きしておき、解決のタイミングを遅延できるようにおきます。

クラス設計のtips

 最後に、私のクラス設計Tipsをちょっと書きます。

ライフサイクルに注目する

 クラスを作る時に「この変数、インスタンス変数とメソッドの引数どっちに置けばいいんだろう?」という悩み、ありませんか?私はこれを考える切り口の一つとして、よく変数のライフサイクルの違いに着目します。ライフサイクルが長いものはインスタンス変数、短いものはメソッドの引数におきます。こうすることで、インスタンスのライフサイクルがより長くできるからです。もしインスタンス変数のライフサイクルが揃わない場合は設計見直しのサインです。

staticメソッドを避ける

 自クラスのファクトリーメソッド以外のstaticメソッドは極力避けます。呼び出し側がmockしにくくテストが書きづらい、引数が冗長になりがち、インターフェースで詳細を隠蔽できないので具象クラスに依存してしまう、などの理由からいいことがあまりないからです。自クラスの状態に依存しないということは、他にあるべきクラスの役割をインラインで書いてしまっている可能性もあります。一度staticで書いたメソッドをインスタンスメソッドに戻すためには呼び元を全て変更しなければなりません。特にメソッドをpublicで公開する場合は相当慎重になることをおすすめします。

具体例

 以上二つのルールを念頭に、例えばAPIリクエストを受けて、そのリクエストに含まれる画像パスへのユーザーの認可をするクラスの設計を考えてみましょう。前提として、ユーザーの権限はリクエスト内で不変であり、かつ認可で照合する権限は画像の種類を問わずに決まっているものとしましょう。また、画像パスは単一リクエスト内に複数指定される可能性があるものとします。 考えられる組み合わせは以下4通りになります。

  1. 画像パスをインスタンス変数に置き、ユーザー権限を引数に取る
    • 認可クラスは画像パスごとにインスタンスを生成する形になります
    • 画像パスがユーザー権限をチェックする形にも見えるため、やや歪な設計です
  2. ユーザー権限をインスタンス変数に置き、画像パスを引数に取る
    • 認可クラスはリクエスト中一回の生成で済みます
    • ユーザーの権限で画像パスをチェックする形なので、見え方も自然です
  3. 両方をインスタンス変数に置く
    • 1.とインスタンスのライフサイクルは同じになります
    • インスタンス変数のライフサイクルが揃わないので設計見直しのサインが出ています
    • 呼び元が複数画像を検証する際に毎回同じユーザー権限をインスタンス生成に与える必要があり、冗長です
  4. 何もインスタンス変数に置かない
    • ただの関数になるのでライフサイクルはシステムと同じになり、全てを同じインスタンスで捌けるようになります
    • メソッドもstaticにできますが、その代わりに3.と同様に呼び元が複数画像を検証する際に毎回同じユーザー権限をメソッドに渡す必要があり、冗長です

 ライフサイクルに注目すると2.がよさそうですが、もう少し2.と4.を掘り下げて、staticメソッドを避ける積極的な理由を述べたいと思います。思考実験をもう一段進めて、ユーザー権限を取得するクラスのインスタンスをDIできるようになった、という状況を考えます。今まで呼び元が権限を取得して渡してくれていましたが、それが必要なくなり、権限の取得から判定までを認可クラスに閉じて処理できるようになります。
 2.を選択していた場合、すんなり新しいクラスに移行できるでしょう。直接インスタンス変数で参照していた権限をこのクラスに置き換えてDIするようにし、処理の中で権限を取得して参照するように修正するだけです。呼び元も、今まで自分で生成していた認可のクラスをDIするように変更するだけです。これにより、呼び元からは画像の認可のためにユーザー権限が必要であるという知識が必要なくなります。
 4.の場合はどうでしょうか?2.と同様、認可クラスで権限の取得から判定までを閉じて処理できるようにするリファクタをするとします。まずは認可クラスですが、インスタンスメソッドで認可処理を公開していた場合はまだ新しいメソッドを生やして古いメソッドを廃止していくことでなんとかなりそうです。あとは呼び元に2.同じ修正を加えて新しいメソッドの呼び出しに切り替えていくだけです。staticメソッドで認可処理を公開していた場合はどうなるでしょう?修正というよりは新しいクラスの作り直しになると思います。呼び元のメソッド呼び出し箇所の全面的な書き換えが必要なので新しいクラスの新しいメソッドに参照を向けるのと対して手間が変わらないからです。状態を持たないことを前提にstaticメソッドを公開する形で設計されたクラスは、後から状態を持たせようとすると破綻することがわかります。
 今回は都合のいい例を取り上げているので、実際に現場で起きることとはやや異なるかもしれません。下手な例ではありますが、クラスの状態の設計の必要性や考え方が伝われば幸いです。

 ざっくり、私がクラスを作るときに考えることをご紹介しました。「え?生成AIをいつどう使ってるのかって話、ないの?」と思われたでしょうか?はい、ありません。ただ、書いていることのいつどのタイミングでも生成AIは使えますとだけ申し添えておきます。
 次回は弊社最古参のベテランリードエンジニア、高橋さんです。お楽しみに。