jsでリファクタリングを学びたい(Nullオブジェクト編)

リファクタリングについて学びたく、「Java言語で学ぶリファクタリング」を読んでる。タイトル通り、Javaで記述されているが、js(ES6) + flowで書いてみて、リファクタリングの技術とflowの理解を深めようと思う.

今回取り上げるリファクタリング内容は、4章のNullオブジェクトについてである。

書籍にはNullオブジェクトの良いたとえ話が記載されている。一日おきに飲まなければならない薬があるとき、本日飲むべき日かどうか考えないといけない。その判断を避けるために、本来の薬とダミーの薬を交互に飲み続ければよい。これをプログラミングでも行おうというものが、Nullオブジェクトの目的である。

では、実際null判定の入った次のソースをリファクタリングしていこう。

リファクタリング

// Person.js
// @flow
import Label from './Label';

export default class Person {
  _name: Label;
  _mail: Label;

  constructor(name: Label, mail: Label) {
    this._name = name;
    this._mail = mail;
  }

  display() {
    // nullチェック
    if (this._name !== null) {
      this._name.display();
    }

    // nullチェック
    if (this._mail !== null) {
      this._mail.display();
    }
  }

  toString(): string {
    let result: string = '[ Person:';
    result += ' name=';

    // nullチェック
    if (this._name === null) {
      result += '(none)';
    } else {
      result += this._name;
    }
    result += " mail=";
    if (this._mail === null) {
      result += '(none)';
    } else {
      result += this._mail;
    }
    result += ' ]';
    return result;
  }
}

nullかどうかでdisplay処理を行うかどうかを分けてるが、Nullオブジェクトを導入すると、nullかどうかを意識せずメソッドを呼び出せばよいのだ。

リファクタリング

// Person.js 
// @flow
import Label from './Label';

export default class Person {
  _name: Label;
  _mail: Label;

  // Nullオブジェクトの導入
  constructor(name: Label, mail: Label = Label.newNull()) {
    this._name = name;
    this._mail = mail;
  }

  display() {
    this._name.display();
    this._mail.display();
  }

  toString(): string {
    return `[Person: name=${this._name.toString()} mail=${this._mail.toString()}]`;
  }
}

コンストラクタの第二引数に、mailアドレスが与えれなかった時、Label.newNull()によりNullオブジェクトが代わりに与えられる。
このNullオブジェクトは、Label型のサブクラスとして実装されるため、型一致している。 そして、NullオブジェクトはLabelオブジェクト同様displayメソッドをもつが、何も影響与えないよう設計されている。

以上の要件からNullオブジェクトに要求されるものが、
- 本来のクラスを継承する
- 本来のクラスのもつメソッドを無効化する
という2点にある。

実装は次の通り。

Nullオブジェクト

// Label.js
// @flow
export default class Label {
  _label: string;

  constructor(label: string) {
    this._label = label;
  }

  // 本来行いたい処理
  display() {
    console.log(`display: ${this._label}`);
  }

  toString(): string {
    return this._label;
  }

  // Nullオブジェクトを返すfactoryメソッド --- (a)
  static newNull(): Label {
    return NullLabel.getInstance();
  }
}

// Nullオブジェクトクラス
class NullLabel extends Label { // --- (1)
  // シングルトンの実装 --- (b)
  static singleton = new NullLabel();
  static getInstance(): NullLabel {
    return NullLabel.singleton;
  }

  constructor() {
    super('(none)');
  }

  // オーバーライドして何も影響を与えないようにする --- (2)
  // @override
  display() {

  }
}

本来のLabelクラスを継承するNullLabelサブクラスがある(1)
またサブクラス内でメソッドを無効化するためにオーバーライドを利用している(2)
そして、メモリ節約のためのシングルトン(b)や、new演算子をさけるためのFactoryメソッド(a)を利用して、LabelクラスからNullオブジェクトを呼び出している。

まとめ

今回は、リファクタリングとしてNullオブジェクトについて学んだことを残してみた。Nullオブジェクトを一言でいうと、本物と同じ動きをするが影響を及ぼさないダミーオブジェクトのことを指すのであろう。
それを、継承とオーバライドによる無効化によって実現する技術なんだなと理解できた。

気になったこと

javaオーバーロードが実装できるので、nullオブジェクトの検討がわかりやすいが、jsではオーバーロードが実装できない。
代わりに、デフォルト引数を利用すれば補えるのではないかと気づいた!

public class Person {
  private Label _name;
  private Label _mail;

  public Person(Label name, Label mail) {
    _name = name;
    _mail = mail;
  }

  public Person(Label name) {
    this(name, Label.newNull());
  }

  ...
}
export default class Person {
  _name: Label;
  _mail: Label;

  constructor(name: Label, mail: Label = Label.newNull()) {
    this._name = name;
    this._mail = mail;
  }

}

Java言語で学ぶリファクタリング入門

Java言語で学ぶリファクタリング入門