クラスの継承とデフォルト値処理の設計

最近 TypeScript を使ってプログラムを組むことが多くなり、継承や移譲などについて考える機会も増え、複雑な引数やデフォルト値処理などを試行錯誤する機会も増えてきました。

ここに書くのはベストプラクティスとはまだ言えない、ただの私的なプラクティスですが、ひとまず頭の外に出すことを目的に、オプションとデフォルト値処理に焦点を置いたクラス設計についてまとめてみたいと思います。

## コンストラクタのパラメータと継承の方針

__複雑になりがちなコンストラクタの引数とオプション処理を設計する際にいちいち悩まなくて済むこと__を、第一に考えていたら、以下のような方針にまとまりました。これでなんとなく上手く行くことが多いような気がします。

- コンストラクタの引数は単独のオブジェクト `Params` とします
- オプション(コンストラクト時に省略可能なプロパティ)はすべて `Params.options` に押し込みます
    - デフォルト値処理をまとめて行えるので楽ですし、コードもスッキリします
    - `Params.options` の内部はフラットなオブジェクトにします
- `Params` や `Options` インタフェースはクラスと同じ名前空間に定義します
- 更に継承時のお約束として、コンストラクタの引数 `Params` も `Options` も継承でプロパティが増えることはあっても減ることはないものとして設計しています。

具体的なコードの概観を書くと以下のようになります。ひとつのクラスのごとに `Params` と `Options` を定義しています。

```ts
class Base {
  constructor(params: Params) { ... }
}

namespace Base {
  interface Params {
    param1: string;
    options?: Options;
  }
  interface Options {
    option1?: string;
    option2?: string;
  }
}
```


## クラスの定義例

実際に継承をするコードを例に、もう少し具体的に示してみます。

### デフォルト処理を定義する

まず下準備として、オプションのデフォルト値処理を行うためのメソッドを用意しておきます。目的としては

- TypeGuard 付きの `Object.assign` を行いたい
- 行頭に近いオブジェクトから優先的にプロパティを適用したい

という個人的嗜好なので不要でしたら `Object.assign` をそのまま使っても問題ないと思います。

```ts:class.ts
function defaults<T>(...v: T[]) {
  v.reverse();
  return Object.assign({}, ...v) as T;
}
```

今のところフラットなオプションオブジェクトしか想定していないのと、値としてはプリミティブ型しか想定していないのでこんな実装です。もう少し込み入った要求があれば `deepcopy` などで対応することになるのかな...などと思っています。

### Base クラスを定義する

親クラスとなる `Base` クラスを定義します。オプションとコンストラクタのみからなるシンプルなクラスです。

```ts:class.ts
export class Base {

  // クラス変数にデフォルトオプションを定義
  static defaultOptions: Base.Options = {
    o1: 'Base.default', // ユーザ上書き用
    o2: 'Base.default', // 継承先上書き用
    o3: 'Base.default',
  };

  // インスタンス変数にオプションを定義
  options: Base.Options;

  constructor(public params: Base.Params) {

    // 引数 > デフォルト値の優先順でオプション値を拾ってくる
    this.options = defaults(params.options, Base.defaultOptions);
  }
}

// namespace でクラスと同一の名前空間にインタフェースを登録する
export namespace Base {
  export interface Params {
    p1: string;
    options?: Options;
  }
  export interface Options {
    o1?: string;
    o2?: string;
    o3?: string;
  }
}
```

ポイントを挙げておきますと

- `namespace` で `class` を拡張するテクニックを利用しています
    - 同じ使用感でアクセスできていますが `Base.Options` と `Base.defaultOptions` は微妙に質が異なっていて `Base.Options` は `namespace` で定義されているのに対して `Base.defaultOptions` はクラス変数として定義されています
    - [TypeScriptのクラスをnamespaceで拡張する](http://qiita.com/ConquestArrow/items/5d885128f896844698dd) を参考にしました
- `Params` と `Options` は継承先の `Params` と `Options` にそれぞれ拡張する予定なので `public` で定義します
    - `private` 指定したプロパティを継承先で別の型で再定義しようとすると多重定義で怒られます

### Extended クラスを定義する

`Base` クラスを継承した `Extended` クラスを定義します。こちらもオプションとコンストラクタのみの構成です。

```ts:class.ts
export class Extended extends Base {

  static defaultOptions: Extended.Options = {
    e_o1: 'Extended.default', // ユーザ上書き用
    e_o2: 'Extended.default',
    o2: 'Extended.default overwrite Base.default', // Base のデフォルト値を上書き
  };

  options: Extended.Options;

  constructor(public params: Extended.Params) {

    // `super` が終わった時点で `this.options` には `Base` のデフォルト値処理がなされている
    super(params);

    // 引数 > デフォルト値 > 親クラスで処理された状態の `this.options`
    // の優先順でオプション値を拾ってくる
    this.options = defaults(params.options, Extended.defaultOptions, this.options);
  }
}

export namespace Extended {
  export interface Params extends Base.Params {
    e_p1: string;
    options?: Options;
  }
  export interface Options extends Base.Options {
    e_o1?: string;
    e_o2?: string;
  }
}
```

コンストラクト時の流れは以下の通りです。

- `super` が `Base` のコンストラクタの実装に従い `this.options` のデフォルト値処理を行う
- その後 `Extended` のコンストラクタの処理に従い `this.options` のデフォルト値処理を行う

### 実行する

実行してみると、継承によるデフォルト値処理も、ユーザからの引数による指定もうまく処理できているようです。

```ts
import { Base, Extended } from './class'

const b = new Base({ p1: 'val1', options: { o1: 'user-opt1' }});
console.log(b.options);
// { o1: 'user-opt1', o2: 'Base.default', o3: 'Base.default' }

const e = new Extended({ p1: 'val1', e_p1: 'val2', options: {
  o1: 'user-opt1', e_o1: 'user-opt2' }
});
console.log(e.options);
// { o1: 'user-opt1',
//   o2: 'Extended.default overwrite Base.default', <- 上書きされてる
//   o3: 'Base.default',
//   e_o1: 'user-opt2',
//   e_o2: 'Extended.default' }
```

## 心残りと所感

- この方法で何段階か継承をした場合 `params.options` から `this.options` への `defaults` 処理が継承階数分発生するが、その無駄は許容されるか
- そもそも `options` と `params` を分ける必要があるのか
    - `params` はオブジェクトでない、バラバラの引数でもよかったかもしれない
    - 引数の構成がコード的にも見やすくなると思ったのですが、やっぱり混ぜてもいいのかなと、考えると止まりません...

あと、そもそもなのですが、親クラスのプロパティ空間を侵襲しながらプログラムを組む際の気遣いに疲れたので、やっぱり泥臭くても移譲で実装したいな...と思いました。気にしなければならない範囲は少ない方が楽だなぁと(カプセル化)。

と言ってもメソッドが多いと大変なので、何か移譲を効率的に実装するいい枠組みがあればいいのですが。まだしばらくコードや表現の分割化手法については学ぶところが多いです。