前提
インナークラスに型名を持たせられる言語と、持たせられない言語があり、Javaの場合インナークラスは以下Fooのように型名を持てます。
// Javaの場合
package com.sample.snippet;
public class Hoge {
public static void main(String[] args) throws Exception {
Foo foo = new Hoge().new Foo();
foo.sayFoo();
}
class Foo {
public void sayFoo() {
System.out.println("foo");
}
}
}
対して現時点のTypescript(3.5.1)では型名を持ったインナークラスを定義する方法が存在しておらず、以下のFooはプロパティ名であって型名ではないので匿名クラスと機能的に変わりがありません。
// TypeScriptの場合
class Hoge {
public static Foo = class {
public sayFoo(): void {
console.log("foo");
}
};
}
// × Fooは型名ではないのでエラー
let foo: Foo = new Hoge.Foo();
foo.sayFoo();
TypescriptでFooに型名を持たせるには継承させて親クラス型を使う必要が有ります。
はじめに
Typescriptのインナークラスは型を持てないのでスコープが広範囲になると扱い辛いという話です。
最初からインナークラスにしなければ型名が持てて個別にメソッド実装出来るよね、って話ですが、一つのクラス内で複数の関連サブクラスを管理することも無くはないですし、そういうソースをメンテする側になり、インナークラス毎に個別対応が発生した場合を想定したら怖いですね。どう逃げるか考えました。
想定する状況
以下のようにCommandクラスを継承したインナークラスが、Commandクラス自身のインナークラスとして定義されているとします。
abstract class Command {
// コマンドの共通処理
public static CommandA = class extends Command {
// Commandを汎化したCommandAインナークラスの処理実装
};
public static CommandB = class extends Command {
// Commandを汎化したCommandBインナークラスの処理実装
};
このインナークラスをインスタンス化して使う際、インナークラス自体には型は持てなくても、継承をしていればポリモーフィズム的に親クラス型で表せます。
let commandA: Command = new Command.CommandA();
let commandB: Command = new Command.CommandB();
ただこの時、「commandBはCommandAと違って実行時にファイル名を必要とする」となった場合、Commandクラス型だけでは表現出来ない機能が必要になってしまいます。この機能差分を埋める為にインターフェースを使って回避する、が今回のテーマになります。
環境・準備
- Windows 10
- NodeJS 8.11
- Typescript 3.5.1
Typescript3.5.1が入ったディレクトリでVSCodeを起動。実験ソース「downcast.ts」が編集されたらdowncast.jsに自動トランスパイルされるようにしておきます。
C:\src\node> mkdir tscast
C:\src\node> cd tscast
C:\src\node\tscast> npm init
C:\src\node\tscast> npm install typescript
+ typescript@3.5.1
added 1 package in 11.812s
C:\src\node\tscast> code .
C:\src\node\tscast>tsc -W downcast.ts
初期の実装例
Commandクラスでは全コマンドの共通処理(preOperateメソッド)と処理フロー(executeメソッドが呼ばれるとoperateテンプレートメソッドをコール)を実装、各継承クラスではoperate抽象メソッドをオーバーライド実装してコマンド処理が実装されているものとします。
// downcast.ts
abstract class Command {
public execute(): boolean {
this.preOperate();
return this.operate();
}
protected preOperate(): void {
console.log("コマンド共通の前処理");
}
// 各コマンドクラスに実装を強制
protected abstract operate(): boolean;
// インナークラスA
public static CommandA = class extends Command {
protected operate(): boolean {
console.log("CommandAの処理");
return true;
}
};
// インナークラスB
public static CommandB = class extends Command {
protected operate(): boolean {
console.log("CommandBの処理");
return true;
}
};
}
// 起動クラス
class Main {
public invoke(): void {
let commandA: Command = new Command.CommandA();
commandA.execute();
let commandB: Command = new Command.CommandB();
commandB.execute();
}
}
new Main().invoke();
実行結果
C:\src\node\tscast>node downcastsample.js
コマンド共通の前処理
CommandAの処理
コマンド共通の前処理
CommandBの処理
継承を使うことで一連のコマンド処理実装量を減らし、各コマンドをCommandクラスで束ねておく意図でこうしているとします。
この設計で生じる課題
冒頭のCommandBにはファイル名を与える必要が出てきたと仮定します。共通親クラスであるCommandクラスにはCommandBでしか必要とされないファイル名情報を持たせたくないのでCommandBだけにファイル名を与えたいところですが、AもBもインナークラスである為型名を持たず、親クラスであるCommandクラス型としてしか表せない為そもそも区別が出来ません。
課題に対する回避策
インナークラスに個別インターフェースを実装することで、個別機能I/Fを持たせて対処してみます。赤い部分が新規追加になります。
abstract class Command implements ICommand {
public execute(): boolean {
this.preOperate();
return this.operate();
}
protected preOperate(): void {
console.log("コマンド共通の前処理");
}
protected abstract operate(): boolean;
public static CommandA = class extends Command {
protected operate(): boolean {
console.log("CommandAの処理");
return true;
}
};
public static CommandB = class extends Command implements ICommandB {
private fileName: string;
public setFile(fileName: string): void {
this.fileName = fileName;
}
protected operate(): boolean {
console.log(this.fileName + "を使ったCommandBの処理");
return true;
}
};
}
interface ICommand {
execute(): boolean;
}
interface ICommandB extends ICommand {
setFile(fileName: string): void;
}
class Main {
public invoke(): void {
let commandA: ICommand = new Command.CommandA();
commandA.execute();
// ICommandBインターフェースで表し、setFile()を個別に使用可能にする
let commandB: ICommandB = new Command.CommandB();
commandB.setFile("hoge.txt");
commandB.execute();
}
}
new Main().invoke();
実行結果
C:\src\node\tscast>node downcastsample.js
コマンド共通の前処理
CommandAの処理
コマンド共通の前処理
hoge.txtを使ったCommandBの処理
ICommandBを介することで個別にファイル名設定機能を持たせられました。
まとめ
何とか個別に型を持たせて回避出来ましたが・・・最初からインナークラスにしなければこんなことにはなってなかったです。
発想を変えて、実装者がインナークラスを意識しなくてよく、I/Fだけ知ってればよい状況なら使える設計になるかも・・・。
// フレームワークから業務ロジックにコマンドがワイドニングされてコールバックされる
protected takeCommand(command: ICommand): void {
// 業務ロジックで設定後、フレームワークでexecute()される
let c: ICommandB = <ICommandB>command;
c.setFileName("hoge.txt");
}
でも最初から型を持った定義にしていても得られるメリットは同じですね。
- Typescriptのインナークラスは型を持てない
- 広範囲になると型判別出来ないので使用は局所的に
- 最悪、個別にI/Fを持たせれば判別は出来る
という話でした。