30歳からのプログラミングスマートフォン用の表示で見る30歳からのプログラミング30歳無職から独学でプログラミングを開始した人間の記録。abstractキーワードという機能があり、これを使うことで抽象クラスや抽象メンバを宣言することができる。この機能を上手く使ってクラスを作ることで、可読性が高く、変更にも強いコードを設計できる。abstractキーワードを使ったクラスやメンバの詳細な挙動については、以下の記事に書いた。numb86-tech.hatenablog.com本記事では、架空のオンラインショップの開発現場を通して、具体的にabstractをどう使えばよいのか、どのようなケースで役に立つのか、といったことを説明していく。動作確認はv3.8.2で行っている。間違った共通化このオンラインショップは書籍の販売を行っており、商品である書籍はProductというクラスで表現している。}}このように書くことで、全ての書籍に共通するメソッドであるgetTaxIncludedPriceを一箇所で定義すれば済むようになる。このメソッドの内容を変更したり、新しく別のメソッドを追加したりする際も、Productクラスにだけ手を加えればよいことになる。ちなみに、小数点以下の計算を正確に行うためには様々な注意点があるのだが、本記事の主題ではないので割愛し、今回は簡単な実装で済ませている。このコードに特に問題はなく、正しく動く。1100しかしビジネスモデルに変更があり、書籍以外の商材も扱うことになった。缶ビールや缶コーヒーなどの飲料の取り扱いを開始するが、他の商材も今後追加していく可能性があるらしい。飲料にauthorやtitleが存在しないのは自明である。一方で、アルコールかどうかで消費税率が変わるため、新たに、アルコールか否かを表現するデータを持つ必要がある。その情報は、書籍には存在しない。それぞれの商材にどのようなメンバが必要なのかを精査し、まとめたのが、以下の表である。◯こうしてみると、共通するメンバがそれなりにある。今後も、全ての商品に共通する性質や機能を追加する可能性は、十分にある。書籍と飲料以外の商材の追加も検討中だ。考えると、商材毎に完全に別々のクラスに分けてしまうのは、筋が悪そうである。そこで、Productにtypeという属性を持たせて、Productが複数の商材を表現できるようにした。書籍の場合はtypeをbookに、飲料の場合はtypeをdrinkにする。これなら、共通の処理を一箇所にまとめつつ、複数の商材を表現できるようになる。して出来上がったProductクラスが、こちら。}}取り敢えず、動作に問題はない。税込価格の計算ロジックも正しく動いている。飲料は軽減税率の対象だが、アルコールは対象外となる。つの問題を抱えている。生成されたインスタンスがどのようなメンバを持つのかが、定かではない。同じProductから生成されたインスタンスでも、どのようなメンバを持つのかは、生成時に渡した引数によって変化する。そのため、実際に中身にアクセスするまで、そのメンバが存在するかどうか分からない。型推論の結果もそうなる。beer;isAlcoholはオプショナルなメンバであるため、存在するかもしれないし、undefinedかもしれない。後からこのプロジェクトに参加した開発者は特に、挙動の把握に苦労することになる。生成されたインスタンスがどのメンバを持っているのかは、Productのコードを読んで調べなければ分からない。つめの問題が影響してくる。単純に、Productの実装が複雑で、可読性が低いという問題。Productというひとつのクラスで複数種の商材を表現するために、typeによる条件分岐が行われている。この程度の規模ならまだよいかもしれないが、今後商材が増えれば、その度に、条件分岐は増えていくことになる。最後の問題は、isAlcoholなどをオプショナルにしたことで、型システムの恩恵を受けられなくなってしまったこと。飲料にはisAlcoholが必須なのだが、忘れてしまってもエラーにならない。324逆に不要な情報を渡しても、それもエラーにはならない。エラーにならない});継承を使った共通化こうなってしまった原因は、異なる商材をひとつのクラスにまとめてしまったことにある。共通するものをまとめてしまおう、という考え方は、ある程度は正しい。共通するメンバがいくつかあるから、ではなく、書籍と飲料は別々の商材ではあるが、どちらも同じ「商品」であるためだ。種類のProduct」と考えることができる。「インターフェイスが似ているだけで意味としては異なるもの」をまとめてしまうのはアンチパターンだが、この記事の主題から逸れるので、今回はそれについては扱わない。書籍と飲料のようなケースでは、継承を使うことで、綺麗に書ける。継承が唯一の選択肢というわけではないのだが、仕様の全体像や今後の事業計画を総合的に考えた結果、今回は継承を使うことにした。Productを親クラスとして設計し直して、書籍と飲料に共通する性質や機能を書く。子クラスとしてBookとDrinkを作り、商材毎に異なる性質や機能はそちらに書く。この方針に基づいて、どのメンバをどのクラスに実装するか、検討した。?他のメンバは簡単だが、getTaxIncludedPriceが問題。全ての商品が持っているべきメソッドなので、Productに書くべきかもしれない。そのロジックは商材毎に微妙に異なる。そこで、getTaxIncludedPriceは各商材に持たせ、ロジックの共通部分を抽出してそれをProductに書くようにした。具体的には、Productの実装は以下のようになる。}}商材毎に異なるのは消費税率の部分なので、その部分は、BookやDrinkに書く。それを使ったロジックの大部分についてはcalculateTaxIncludedPriceとしてProductに書く。BookとDrinkはgetTaxIncludedPriceを実装し、そのなかでcalculateTaxIncludedPriceを呼び出して消費税率を渡す。以下が、BookとDrinkの実装。}}処理の流れが理解しやすくなっており、動作も問題ない。330生成されたインスタンスがどのようなメンバを持つのか、自明になった。インスタンス生成時の引数によってメンバの有無が変化することはなく、同じクラスから生成されたインスタンスなら必ず同じメンバを持つ。