Error});抽象クラスと具象クラス新しく書き直されたProductは、全ての商品に共通する性質や機能をまとめたものであり、何か具体的な商品を表しているわけではない。非常に抽象的な存在である。そのため、Productのインスタンスが作られることはない。Productは、それを継承した子クラスを作るためだけに存在すると言える。このようなクラスのことを、抽象クラスという。それに対して、抽象クラスを継承し、具体的な商品を表現するために作られたBookやDrinkのようなクラスを、具象クラスという。abstractキーワードを使って、抽象クラスであるということを明示的に宣言することができる。abstractをつけたクラスは抽象クラスになり、インスタンスを作ろうとするとエラーになる。1000,});この機能によって、誤って抽象クラスのインスタンスを作ってしまうことを防止できる。abstractキーワードをつけることで、コードがドキュメントとして機能するようになる。このプロジェクトに新しく入った開発者でも、「Productは抽象クラスなんだな」ということをすぐに理解できる。抽象メンバ開発は順調に進んでいたが、またも仕様の追加が発生した。書籍と飲料の他に、カレンダーも扱うことになったという。商材の追加は予想されていたことであり、だからこそProductクラスを作ったのだ。Productを継承することで、商品として必ず持つべき振る舞いと、カレンダーに固有の振る舞い、その両方を持ったクラスを簡単に作れる。継承を使うことで、カレンダーを表現するクラスにはカレンダーに固有の振る舞いだけを書けばよく、商品全般に関する振る舞いを改めて記述せずに済む。カレンダーが独自に持つべき振る舞いは何なのかを検討した結果、当面は「何年のカレンダーか」が分かればよいということになった。そこで、Productを継承し、独自にyearメンバを持つCalendarクラスを実装した。2020一見問題なく動いているが、CalendarにgetTaxIncludedPriceを忘れてしまっている。このコードはエラーにはならない。「Productを継承した具象クラスはgetTaxIncludedPriceを実装しなければならない」とは、どこにも定義されていないからだ。この規模のコードなら、getTaxIncludedPriceの実装を忘れてしまう可能性は低いかもしれない。実装が不可欠なメンバの数が増えたり、たくさんの具象クラスを書くようになったりすれば、ミスをすることは十分にあり得る。そもそも、「getTaxIncludedPriceが必須である」という情報は、開発者の頭のなかにしかない。これまでの開発の経緯を把握している開発者でないと、知り得ない情報だ。getTaxIncludedPriceは全ての具象クラスが実装すべきものなのか、それともBookとDrinkに必要だっただけで必須ではないのか、現状の実装では判断がつかない。整備された仕様書が存在すればよいが、そうでない場合、周囲の開発者に尋ねたり、コード全体の流れを追って調査したりしないといけない。getTaxIncludedPriceは、抽象クラスであるProduct自体には実装しないが、Productを継承した具象クラスには必ず実装しないといけない。このようなメンバを、抽象メンバという。抽象メンバの表現も可能になっている。抽象メンバの表現にも、abstractキーワードを使う。下記のgetTaxIncludedPriceは、「引数は受け取らず数値を返す抽象メンバ」として定義されている。constructorの引数の型情報にgetTaxIncludedPriceを追記するのも忘れないようにする。}}これで、具象クラスにgetTaxIncludedPriceを実装することが必須となり、忘れたり、型が間違っていたりすると、エラーを出すようになった。}}Productにabstractをつけたときと同様にドキュメントとしての効果もあり、Productの実装を見るだけで、具象クラスには必ずgetTaxIncludedPriceメンバを実装しないといけないということが分かる。これで、Calendarの実装も無事に終わった。880抽象クラスと具象クラスの役割分担を見極めるこれで盤石と思われたが、getTaxIncludedPriceの仕様変更をキッカケに、この実装にはまだ改善の余地があることが発覚した。getTaxIncludedPriceの引数として商品の個数を渡して、その個数における税込価格を返すことになった。個あたり税込価格を返す。以下のように動作することが求められる。432「getTaxIncludedPriceのインターフェイスが変わるときに何が起こるのか」をシンプルなコードで表現したくて、このような例にした。getTaxIncludedPriceにこの機能をもたせることの是非は、今回は問わない。getTaxIncludedPriceに渡す引数が変わったり、返り値の構造が変わったりすることは、現実的に十分にあり得る。変更自体は、大して難しくない。getTaxIncludedPriceの引数としてquantityを受け取るようにして、そのデフォルト値を1にする。calculateTaxIncludedPriceの返り値にquantityを掛ければよい。}最後に、ProductでのgetTaxIncludedPriceの型定義にも、引数を追加しておく。number;これで完了。複雑なことは何もない。問題は、全ての具象クラスのgetTaxIncludedPriceを変更しなければならないことだ。具象クラスが増えていたタイミングで同様の変更が発生したら、具象クラスの数だけ手間が掛かる。変更の内容が複雑なものだった場合、相当な手間になってしまう。getTaxIncludedPriceをそれぞれの具象クラスに実装してしまったために、このような状況になってしまった。getTaxIncludedPriceの実装が一箇所にまとまっていれば、インターフェイスが変わることになっても、変更箇所は一箇所で済む。getTaxIncludedPriceを抽象クラスではなく具象クラスに担当させたのは、設計ミスだった。全ての商品に共通する振る舞いは抽象クラスに書く、という原則から考えても、Productに書くべきだ。確かに、税率に関するロジックは商材毎に異なる。その異なる部分こそを具象クラスに切り出すべきであり、getTaxIncludedPrice自体はProductに持たせたほうがよい。quantityオプションのことは一旦忘れ、getTaxIncludedPriceをProductに移す。代わってconsumptionTaxRateという抽象メンバを用意して、それをgetTaxIncludedPriceの中から参照する。そのことを検知してくれるこれで、商材毎に異なる部分をconsumptionTaxRateとして具象クラスに切り出し、getTaxIncludedPriceそのものはProductに書くことができた。これなら、getTaxIncludedPriceの仕様変更が発生しても、Productだけを編集すれば済む。}具象クラスの抽象クラスへの依存を避ける先程の設計の見直しによって、getTaxIncludedPriceをProductに移した。このメソッドは、具象クラスがconsumptionTaxRateメンバを持っていることを知っている。このメンバが各商品の消費税率を示していることも、知っている。一方で具象クラス側は、抽象クラスがgetTaxIncludedPriceのなかでconsumptionTaxRateを使っていることを知らない。consumptionTaxRateが抽象クラスでどのように使われているのか、全く関知しない。このように、具象クラスはできるだけ、抽象クラスに対する知識を持たないようにするのが望ましい。抽象クラスがどのような実装になっているのか知るべきではない。具象クラスが抽象クラスについて知っていればいるほど、依存関係が深くなる。なると、抽象クラスの変更の影響が、具象クラスにも波及しやすくなってしまう。実は、まさにそのような状況になってしまっている箇所が既に存在する。それが、具象クラスのconstructorメソッドである。例として、Bookのconstructorを見てみる。}superの箇所が問題である。unitPrice}を受け取ることを知ってしまっている。Productの知識を持ってしまっている。このせいで、Productのconstructorに変更が発生した場合、具象クラスもその影響を受けてしまう。在庫数を表現するために、Productに新たにstockメンバを持たせることになったとする。}この変更によって、全ての具象クラスのconstructorでエラーが発生する。つを渡すべきなのに、stockを渡していないからだ。そのため、全ての具象クラスで以下の変更作業を行わなければならない。author;これが、具象クラスが抽象クラスに依存することで生まれる弊害である。抽象クラスで変更が発生する度に、具象クラスもそれに振り回されてしまう。この状況を改善し、依存を弱めたのが以下のコードである。Bookを例にしたが、他の具象クラスも同じ要領で対応できる。つに分け、第一引数で抽象クラス用のデータを、第二引数で具象クラス用のデータを受け取るようにした。この設計の利点は、superにはproductPropertyをそのまま渡せばよく、その中身については関知せずに済むことだ。そのため、先程のstockの例のように抽象クラスにメンバを増やしても、具象クラスには一切手を加えずに済む。このように、具象クラスが抽象クラスの知識を持たないようにすることで、抽象クラスの変更の影響が具象クラスに波及しにくくなる。まとめこうして、共通部分をProductクラスに共通化しつつ、変更にも強いクラスを設計できた。全ての商品に共通する性質や機能を抽象クラスにまとめるようにしているため、stockのように全ての商品に対して新しくメンバを追加するようなケースでも、それを抽象クラスに追加するだけで対応できる。適切に設計することで、抽象クラスを編集しても、その影響が具象クラスに波及することはない。getTaxIncludedPriceやstockの例で見た通りである。逆に、具象クラスで変更が発生しても、その影響が抽象クラスや他の具象クラスに及ぶことはない。Calendarクラスに、表紙のデザインに関する情報を持つためのcoverメンバを追加したとする。この変更の影響範囲はCalendarだけに留まり、Productクラスや他の具象クラスには影響しない。abstractキーワードを使うことで、抽象クラスや抽象メンバであることを明示的に宣言できる。行いやすいという利点がある。今回説明した設計が常に正しいというわけでは勿論ないが、抽象クラスや抽象メンバの使い方や利点は、イメージできたと思う。章をベースにしている。この書籍の概要は以下のリポジトリにまとめており、今回もこれを読み返して参考にした。追加されていない提案段階の機能を積極的に先取りしている。それはクラスも例外ではなく、例えば先日リリースされたv3.8では、現時点ではまだ提案段階であるプライベートフィールドが使えるようになった。独自の機能、そしてプライベートフィールドのような最新の機能について、説明していく。動作確認はv3.8.2で行った。同じような感覚で書いたコードだが、これだとエラーになってしまう。}}xなんてプロパティは存在しない、と言われてしまう。以下のように、予め宣言しておく必要がある。}}宣言時に値を渡すこともできる。2型推論が行われるので、yの型はnumberになる。特有のルール等はない。staticキーワードをつけたメンバは、インスタンスではなく、そのクラス自身のメンバになる。]継承も、特に変わったところはない。プロトタイプチェーンを辿って、親クラスのプロトタイプメソッドを利用できる。]これらの機能については、過去にブログに書いた。numb86-tech.hatenablog.comnumb86-tech.hatenablog.comクラスの型情報クラスを定義すると、その名前の型が自動的に作られる。1;}注意しなければならないのは、上記のMyClass型が指しているのはMyClassのインスタンスの型であり、MyClassそのものではないということ。Error型の前にtypeofキーワードをつけることで、クラス自身を表現するようになる。構造的部分型を採用しており、それはクラスでも変わらない。そのため、型に互換性があると判断されれば、実際にそのクラスやインスタンスである必要はない。下記のMyClassはインスタンスメンバとしてxを、そして静的メンバとして、つまりクラス自身のメンバとして、yを定義している。どちらも型推論の結果、number型となる。そのため、number型のxというプロパティを持ったオブジェクトなら、MyClassのインスタンスと同じ構造になり、MyClass型と互換性があると判断される。OKkyeofを使って型のプロパティ名を抽出してみると分かりやすい。独自の機能で、抽象クラスや抽象メンバを表現できる。抽象クラスについての説明や、abstractの実践的な使い方などについては、別途記事を書いた。numb86-tech.hatenablog.comここでは、abstractキーワードをつけたクラスやメンバがどのような挙動をするのかを、見ていく。abstractをつけたクラスは抽象クラスとなり、インスタンスを作成しようとするとエラーになる。OK抽象クラス内のメンバにはabstractをつけることが可能で、そのメンバは抽象メンバとなる。抽象メンバは子クラスで実装する必要があり、実装していない子クラスはエラーになる。'a';}?をつけても、それはあくまでもundefinedが許されるというだけであり、実装は必要。undefined;}子クラスが実装していれば、孫クラスは抽象メンバを実装しなくても問題ない。{}もちろん実装してもよいが、その場合は型が一致している必要がある。'a';}孫クラスが抽象メンバを実装しているが子クラスは実装していない、というケースでは、子クラスのみがエラーになる。1;}アクセス修飾子アクセス修飾子という機能を使うと、メンバのアクセス可能範囲を制御することができる。privateはそのクラス内でのみアクセス可能、protectedはそのクラスと子クラスからのみアクセス可能、publicはどこからでもアクセス可能、となる。アクセス修飾子をつけていない場合は、publicになる。OKnewでインスタンスを作成する際、constructorメソッドが実行される。そのため、constructorをprotectedにすると、外部からconstructorにアクセスできなくなるため、外部でインスタンスを作成することは不可能になる。protectedなので子クラスからはアクセスできるため、子クラスのconstructorでsuperを使って実行することはできる。declaration.アクセス修飾子をconstructorの仮引数で使うと、メンバの宣言を省略できる。1さらに省略することもできる。1readonlyはその名の通り、メンバを読み取り専用にするための修飾子。v3.8でこの機能を取り入れたので、既に使うことができる。接頭辞として#をつけたメンバが、プライベートフィールドとして扱われる。プライベートフィールドは、宣言したクラスのなかでのみ利用できる。identifier.プライベートフィールドとクラスフィールドはそれぞれ別のメンバなので、名前が重複しても問題ない。修飾子の違いプライベートフィールドは、既に紹介したprivate修飾子と似ている。どちらも、宣言したクラスの外からは利用できず、子クラスから利用することもできない。ここからは、両者の違いを見ていく。Privateという概念がある。以下の記事が分かりやすいが、端的に言えば、後者のほうがより厳密にアクセスが制限されている。Privateであるため、外部から存在を確認することができるし、外部からアクセスするための抜け道もある。Privateであり、外部から存在を確認することはできず、抜け道もない。'MyClass'.console.log(instance['#hard']);private修飾子を使ったsoftはgetOwnPropertyNamesの一覧に出てくるし、ブラケット記法でアクセスすることもできてしまう。プライベートフィールドを使ったhardは、外部からのアクセスが完全に遮断されている。コードには影響を及ぼさない。コードは問題なく動く。以下は外部からxにアクセスしようとしているためエラーになるが、@ts-ignoreでそのエラーを無視している。ファイルを実行すると、エラーは出ず、1と表示される。上でのエラーさえ回避すればよい、というものではない。ファイルが生成されてしまう。先程と同じように@ts-ignoreでエラーを無視して以下のコードをトランスパイルしてみる。エラーを出さないのでトランスパイルは行われるが、生成されたファイルは壊れており、実行するとSyntaxErrorになる。targetプライベートフィールドは、tsconfig.jsonのtargetがES2015以上でないと利用できない。ES5にしてみると、以下のコードはエラーになる。higher.}private修飾子の場合はtargetの制限はなく、例えばES3にしても以下のコードは動く。はてなブログPro30歳無職から独学でプログラミングを開始した人間の記録。ブログを報告するスターをつけました引用をストックしましたストック一覧を見る閉じる引用するにはまずログインしてくださいログイン閉じる引用をストックできませんでした。再度お試しください閉じる限定公開記事のため引用できません。