社内研修で初めてTypeScriptを使ったアプリを作ることになった。
カプセル化ってよく聞く言葉だったが、実際に取り入れようと思うと躓く点があったので、自分なりの解釈をまとめておく。
カプセル化とは
- データとそれに関連するメソッド(操作)をオブジェクトとしてまとめ、外部からの直接的なアクセスを制限することを指す。
初心者の考え
とりあえずオブジェクトのプロパティに片っ端からprivate
をつけていけばそれっぽくなるんやろ、という認識だった。が、JavaScriptにはprivate修飾子が用意されておらず、クロージャを使って実質的なprivateを実装するしかないらしい…。
と思ったら、TypeScriptにはprivate
が用意されているらしい!なーんだ、簡単じゃん!
実際にコーディングしていると…
コンソール上で動作するブラックジャックを作っていた。
import type { Card } from './Card'
import type { Deck } from './Deck'
export interface Player {
hand: Card[] // これと
playerName: string // これを外部からアクセスできないプロパティにしたい
drawCard: (deck: Deck) => void
calcTotalNum: () => number
displayHand: () => Card[]
}
上記のinterfaceを、実際にインスタンス化して使うことはない親クラスにimplementsして、それを子クラスで継承して使おうとすると、どこにprivate
をつければいいのだろう…と悩んだ。
親クラス、子クラスはこんな内容。
export class ImpPlayer implements Player {
hand: Card[] = []
playerName: string = ''
// 以下省略
import { ImpPlayer } from './ImpPlayer'
export class Human extends ImpPlayer {
playerName: string = 'あなた'
}
import { ImpPlayer } from './ImpPlayer'
export class Computer extends ImpPlayer {
playerName: string = '相手'
}
さて、どれをprivateにしたらよいだろう…全部のファイルのプロパティにつけてみるか。
- interfaceにアクセス修飾子はつけられないよって怒られた。存在するプロパティ・メソッドの宣言をするだけの機能なんだから当たり前か。privateプロパティが存在するよって宣言とかはできないらしい…!
interfaceだけ外して、親クラスと子クラスにつけてみた。
-
Class 'Human' incorrectly extends base class 'ImpPlayer'. Types have separate declarations of a private property 'playerName'.ts(2415)
ってエラー。親クラスと子クラスで同じ変数名なのにそれぞれprivateを宣言してるって怒られた。
親クラスも外して、子クラスだけつけてみた。
-
Class 'Human' incorrectly extends base class 'ImpPlayer'. Property 'playerName' is private in type 'Human' but not in type 'ImpPlayer'.ts(2415)
ってエラー。親クラスと子クラスでアクセス修飾子の属性が違うって怒られた。
子クラスで外して、親クラスでつけてみた。
-
Class 'ImpPlayer' incorrectly implements interface 'Player'. Property 'playerName' is private in type 'ImpPlayer' but not in type 'Player'.ts(2420)
ってエラー。interfaceとimplementsクラスでアクセス修飾子の属性が違うって怒られた。え、interfaceでアクセス修飾子つけられないなら、implementsするプロパティはprivateにしようがないじゃん…!
ということで、調べてみたらそれっぽい記事が見つかった。
ふむふむ、何となくわかったぞ。
分かった事まとめ
- InterfaceはPublicなメソッド・プロパティを宣言するためのもの。
private
をつける余地はない - 抽象クラスを使えば同じような用途でPrivateの記述もできる(?)っぽいけど、Interfaceとの使い分けが全く想像できない
- そもそも、今回のようにplayerNameだけをインスタンスごとに変えたいならインスタンス化する際にコンストラクタに受け取る引数で変えてあげることもできた。(勉強のためということで、それ以外の解決策を調べた)
解決策1
- Implementsクラスでprivateなプロパティを用意したいなら、Interfaceに
getter
やsetter
メソッドを用意して、プロパティは書かない。Implementsクラスで初めてprivate
属性のプロパティを宣言する。
解決策2
- TypeScript2.0以降だとreadonly修飾子が使える。public readonlyにすることで、どこからでも読み取れるが、変更はできないプロパティを簡単に用意できる
(ちなみにreadonlyを配列に設定するとその中身まで変更できなくなるらしい)
いまはreadonlyを使う方が主流っぽい?のと、ESLintを入れるとなんでもかんでもreadonlyをつけろと言われるので、これで実装してみることにした。もちろん、privateと違って参照自体はできちゃうので、そこは気をつけたい。