Status Code 303 - See Other

サーバサイド、iOS・アンドロイドアプリ、インフラレベルの話まで幅広くやってます。情報の誤りや指摘・意見などは自由にどうぞ。

オブジェクト返却メソッドの異常系実装

概要

今回は、メソッド仕様の話。
オブジェクト値を返却するメソッドにおいて、入力値・プログラム状態によって異常な結果になったときにそれを示したい。
その方法として、以下の3通りが考えられる。

これらの使いどころやそれぞれの利点について記述する。

詳細内容

実装内容

コンソールアプリケーションでユーザが入力した文字列からコマンド(コード内に定義)を実行する。
実行例

ls
カレントのファイルリスト出力
ls /usr/local/bin
/usr/local/bin ディレクトリのファイルリスト出力
rm /usr/local/bin
/usr/local/binの削除
aaa
不正なコマンド aaa

基本実装

※ この後実装するため getCommandメソッドを空実装にしている。

package com.example1;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

	/**
	 * コマンド定義.
	 *
	 */
	public static enum Command {
		ls {
			@Override
			public void execute(String... params) {
				if(params.length == 1) {
					System.out.println("カレントのファイルリスト出力");
				}else {
					System.out.println(params[1] + " ディレクトリのファイルリスト出力");
				}
			}
		},
		rm {
			@Override
			public void execute(String... params) {
				if(params.length > 1) {
					System.out.println(params[1] + "の削除");
				}
			}
		};
		public abstract void execute(String... params);
	}

	/**
	 * メインロジック.標準入力からコマンドを入力して,登録された処理を実行する.
	 */
	public static void main(String[] args) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		String line;
		while ((line = reader.readLine()) != null && !line.isEmpty()) {
			String[] elements = line.split(" ");
			getCommand(elements[0]).execute(elements);
		}
	}
	/**
	 * 入力文字列から処理するコマンドを選択する.
	 * @param cmd コマンド文字列
	 * @return コマンド実体
	 */
	public static Command getCommand(String cmd) {
		return null; // TODO: 空実装
	}
}

getCommandメソッドの異常系処理、それに伴うロジック部の対応

登録されていないコマンドを指定した場合の処理を3通りの方法を実装する。

null を返却する場合

getCommandの修正

	public static Command getCommand(String cmd) {
		for(Command c : Command.values()) {
			if(c.name().equals(cmd)) {
				return c;
			}
		}
		return null;
	}

ロジック部修正

	public static void main(String[] args) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		String line;
		while ((line = reader.readLine()) != null && !line.isEmpty()) {
			String[] elements = line.split(" ");
			Command cmd = getCommand(elements[0]);
			if(cmd != null) {
				cmd.execute(elements);
			}else {
				System.out.println("不正なコマンド " + elements[0]);
			}
		}
	}

例外を発生させる場合

getCommandの修正

	public static Command getCommand(String cmd) {
		for(Command c : Command.values()) {
			if(c.name().equals(cmd)) {
				return c;
			}
		}
		throw new IllegalArgumentException(cmd + " is not defined.");
	}

ロジック部修正

	public static void main(String[] args) throws IOException {
		BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
		String line;
		while ((line = reader.readLine()) != null && !line.isEmpty()) {
			String[] elements = line.split(" ");
			try {
				getCommand(elements[0]).execute(elements);
			}catch (IllegalArgumentException e) {
				System.out.println("不正なコマンド " + elements[0]);
			}
		}
	}

NULL オブジェクトを返却する場合

NULLオブジェクトの定義

	public static enum Command {
		ls {
			@Override
			public void execute(String... params) {
				if(params.length == 1) {
					System.out.println("カレントのファイルリスト出力");
				}else {
					System.out.println(params[1] + " ディレクトリのファイルリスト出力");
				}
			}
		},
		rm {
			@Override
			public void execute(String... params) {
				if(params.length > 1) {
					System.out.println(params[1] + "の削除");
				}
			}
		},
		// NULL オブジェクト追加
		UNDEFINED {
			@Override
			public void execute(String... params) {
				System.out.println("不正なコマンド " + params[0]);
			}
		};
		public abstract void execute(String... params);
	}

getCommandメソッド修正

	public static Command getCommand(String cmd) {
		for(Command c : Command.values()) {
			if(c.name().equals(cmd)) {
				return c;
			}
		}
		return Command.UNDEFINED;
	}

ロジック部修正

変更不要

それぞれの実装利点/欠点

null 返却

利点

  • 結果が存在しないことを意図的に示せる。
  • オブジェクトを作成しないため、メモリ領域の節約になる。

欠点

  • メソッド利用者の処理で null チェックが必要になると、利用者の処理が複雑化する。
  • メソッド利用者が意図しない場所で NullPointerException が発生する可能性がありデバッグに大きなコストがかかる可能性あり。

例外発生

利点

  • 処理が続行できない場合、その後の処理を中断できるためメソッド利用者のコードがシンプルになる。
  • チェック例外をスローすれば、異常時対応をメソッド利用者に通知できる。

欠点

  • 実行時例外をスローすると、メソッド利用者が意図せず処理が中断することがある。
  • チェック例外をスローすると、異常時対応を強制するため、メソッド利用者のコードが複雑化する。

NULL オブジェクト返却

利点

  • null チェックが必要ないため、利用者のコードがシンプルになる。
  • 関連した処理に格納されるため、コードが見やすくなる
  • 利用者のコードが複数あっても、お決まりの処理の記述が其々で不要なため、冗長性がなくなり保守性が向上する。

欠点

  • 事前に定義する必要がある。

まとめ

null 返却の利点は基本的に大きな利点にならないため、意図的に null を返す場合は少ない。
また、null を返すことは思わぬバグを生む可能性があるため余程の意図がない限り行うべきではない。

基本的には、NULL オブジェクトで実装する方が保守性が良くなる。
しかし、全メソッドに NULL オブジェクトを作成しても、多くの NULL オブジェクト定義ができるだけで、あまり効果はない。
つまり、それほど重要でない処理にまでこのような実装をする必要はない。

例外を返す場合、その処理が「本質的にどういう処理ものであるべきか」に基づいて決定すべき。
その処理が仕様として、起こりえなかったり・明らかに不自然であれば、例外で実装するのが良い。
例外仕様設計 - Status Code 303 - See Other

また、異常時に返す値・その意図をドキュメントに示す方が良い(特に null を返す場合)。
そして、これらを意識することによって複数人開発においては以下の利点が見込まれる。

  • バグの少ない品質の良いコード
  • デバッグに要する時間が少ない保守面で優れたコード
  • 開発者に異常系を意識したコーディングの促進


一通りコードが書けるようになった人は、次はメソッド仕様として
このようなことを考えてコーディングできるようにしてみよう!