LoginSignup
1
6

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-03-18

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

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

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

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

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

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

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

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

クラスの定義例

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

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

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

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

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

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

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

Base クラスを定義する

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

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;
  }
}

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

  • namespaceclass を拡張するテクニックを利用しています
    • 同じ使用感でアクセスできていますが Base.OptionsBase.defaultOptions は微妙に質が異なっていて Base.Optionsnamespace で定義されているのに対して Base.defaultOptions はクラス変数として定義されています
    • TypeScriptのクラスをnamespaceで拡張する を参考にしました
  • ParamsOptions は継承先の ParamsOptions にそれぞれ拡張する予定なので public で定義します
    • private 指定したプロパティを継承先で別の型で再定義しようとすると多重定義で怒られます

Extended クラスを定義する

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

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;
  }
}

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

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

実行する

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

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 処理が継承階数分発生するが、その無駄は許容されるか
  • そもそも optionsparams を分ける必要があるのか
    • params はオブジェクトでない、バラバラの引数でもよかったかもしれない
    • 引数の構成がコード的にも見やすくなると思ったのですが、やっぱり混ぜてもいいのかなと、考えると止まりません...

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

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

1
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
6