対象読者層
- 値オブジェクトの設計を知らない方
- 安全なクラス設計を検討している方
はじめに
本記事は、値オブジェクト設計を導入しよう!の記事の続きとなっていますが、本記事だけでも完結する内容になっています。
より安全なオブジェクトを設計するひと工夫
前回は、値オブジェクトの概要について説明してきました。
ここからは、値オブジェクトをより安全に設計するために、完全コンストラクタという設計テクニックをお伝えします。
値オブジェクト設計に限らず、どのクラスにおいても完全コンストラクタの考え方は参考になるはずです。
完全コンストラクタで不完全なオブジェクトを作らない
完全コンストラクタは、オブジェクトを生成する時点でそのオブジェクトが必ず正しく機能するための設計方法です。
値オブジェクトと完全コンストラクタはセットで使用されることが多い設計パターンです。
ここで言う不完全なオブジェクトとは、以下の特徴を持ちます。
- オブジェクトが持つ項目に、不正な値(null等)が設定できてしまう
- オブジェクトを生成後、オブジェクトが持つ項目の値を変更できてしまう
完全コンストラクタのメリット
NullPointerExceptionを心配する必要がない
おそらくプログラマーの遭遇する頻度が最も高いエラーです。
NullPointerExceptionが発生すると、プログラマーはどこでnullが設定されたのか 処理を遡って解析する事になってしまいます。
// メールアドレスというデータを渡さないと使えないクラスだがオブジェクトを生成できてしまう
EmailAddress mailAddress = new EmailAddress();
// オブジェクトが完全に初期化できていないため、NullPointerExceptionが発生する可能性がある
mailAddress.getValue();
完全コンストラクタは、null等の不正な値をオブジェクトに設定されるのを防ぐことができます。
具体的には、不正な値を使ってオブジェクトを生成しようとすると例外をスローします。
例外をスローすることで、どこで不正な値が設定されたのかひと目でわかるようになります。
また、完全コンストラクタで生成されるオブジェクトは正常に機能することが条件になります。
オブジェクトが生成された時点で問題なく利用できるため、プログラマーも安心してオブジェクトを利用する処理を書くことができます。
オブジェクトを不変にすることで予期せぬ動作を防ぐ
変更されることがない変数やオブジェクトは取り扱いが楽です。
定数が良い例ですね。
// 円周率
public static final double PI = 3.14
予期せぬ変更ができてしまうと、プログラムの他の部分で想定していた値と異なる結果になる可能性があります。
また、バグの原因を特定することが難しくなります。特に、変数が複数の場所で変更される場合、どの時点で値が変わったのかを追跡するのが困難になります。
そのため、一度生成した変数、オブジェクトは不変にすることが理想です。
ある値を不変にしたい場合、変数であればfinalを付けるだけで成立します。
しかし、オブジェクトの場合はそうとも限りません。
具体例を挙げてみましょう。
final EmailAddress mailAddress = new EmailAddress("QiitaTaro@gmail.com");
mailAddress.setValue("ZennHanako@gmail.com")
mailAddressオブジェクトはfinalで不変と定義していますが、
セッターメソッドを経由すると値が変更できてしまうのです。
こういった変更を防ぐために、完全コンストラクタで設計したオブジェクト(クラス)は以下の特徴を持ちます。
- 一度生成したオブジェクトは、いかなる場合でも書き換えることは不可能
- 違う値にしたい場合は、新しくオブジェクトを生成する
完全コンストラクタの実装方法
ここからは、以下のサンプルコードをベースに完全コンストラクタの実装方法について解説します。
/**
* メールアドレスクラス
*/
public class MailAddress {
private String mMailAddress;
/**
* コンストラクタ
*
* @param mailAddress メールアドレス
*/
public MailAddress(String mailAddress) {
mMailAddress = mailAddress;
}
public String getValue() {
return mMailAddress;
}
public void setValue(String mailAddress) {
mMailAddress = mailAddress;
}
}
メンバ変数はfinal修飾子をつける
メンバ変数にfinal修飾子をつけることで、メンバ変数の初期化をコンストラクタのみにすることができます。
これにより、コンストラクタでの初期化以降は値の変更を禁止することができます。
/**
* メールアドレスクラス
*/
public class MailAddress {
private final String mMailAddress;
セッターメソッドは追加しない
メンバ変数の初期化をコンストラクタ経由のみできるようにしたため、
セッターメソッドは削除します。
/**
* メールアドレスクラス
*/
public class MailAddress {
private final String mMailAddress;
/**
* コンストラクタ
*
* @param mailAddress メールアドレス
*/
public MailAddress(String mailAddress) {
mMailAddress = mailAddress;
}
public String getValue() {
return mMailAddress;
}
}
セッターメソッドが定義されていることでオブジェクト生成後に値の変更が可能になっていました。
予期せぬ変更を防止するために、セッターメソッドの追加はしないほうが良いです。
値を変更したい場合は、オブジェクトを再度生成して変更したい値で初期化するようにしましょう。
正常値が渡されたときだけ、オブジェクトを生成する
コンストラクタに引数チェック処理を記載することで、正常値が渡されたときだけオブジェクトを生成するようにします。不正な値が渡されたら、例外をスローするようにしましょう。
import org.apache.commons.lang3.StringUtils;
/**
* メールアドレスクラス
*/
public class MailAddress {
private final String mMailAddress;
/**
* コンストラクタ
*
* @param mailAddress メールアドレス
*/
public MailAddress(String mailAddress) {
if (StringUtils.isEmpty(mailAddress)) {
// nullまたは空文字が渡された場合、オブジェクトは生成しない
throw new IllegalArgumentException("invalid mailAddress");
}
mMailAddress = mailAddress;
}
public String getValue() {
return mMailAddress;
}
}
システムによっては、文字数制限を設けている場合もあります。
例として、メールアドレスは30文字まで設定可能 という条件があるとしましょう。
つまり、30文字以上のメールアドレスは、不正な値として扱います。
/**
* メールアドレスクラス
*/
public class MailAddress {
private final String mMailAddress;
/**
* コンストラクタ
*
* @param mailAddress メールアドレス
*/
public MailAddress(String mailAddress) {
if (StringUtils.isEmpty(mailAddress)) {
throw new IllegalArgumentException("invalid mailAddress");
}
// 文字数が30文字を超えるメールアドレスは禁止
if (mailAddress.length() > 30) {
throw new IllegalArgumentException("mailAddress characters over");
}
mMailAddress = mailAddress;
}
public String getValue() {
return mMailAddress;
}
}
これで、30文字を超えるメールアドレスのオブジェクトはシステム上存在しなくなりました。
これにより、他の処理ではメールアドレスが30文字を超えるケースを考慮する必要がなくなります。
不正な値が渡されたら早めに処理を止めるように設計しましょう。
ここでは、例外をスローして処理を止めるようにしています。
早めに処理を止めることで、プログラム内のどこで異常が起きたのか解析するのが楽になるからです。
ファクトリーメソッドを追加してオブジェクトの生成を楽にする
最後に、オブジェクトの生成を楽にするひと工夫として、ファクトリーメソッドを導入します。
ファクトリーメソッドとはオブジェクトを生成するための専用メソッドだと考えてください。
先程のメールアドレスクラスを例としましょう。
メールアドレスには様々なドメインが存在します。
以下はその一例です。
- gmailアドレス
- 社内用メールアドレス
- サポート用メールアドレス
メールアドレスクラスでこれらのメールアドレスに対応するオブジェクトを生成しようとすると、
以下の書き方になります。
final MailAddress gmailAddress = new MailAddress("QiitaTaro@gmail.com");
final MailAddress companyMailAddress = new MailAddress("QiitaTaro@qita.com");
final MailAddress supportMailAddress = new MailAddress("QiitaTaro@support.example.com");
呼び出し側で@以降のドメイン指定する必要があります。
これでは、呼び出し側でスペルミスをした場合、誤ったオブジェクトが生成されてしまいます。
これをファクトリーメソッド(オブジェクトを生成するための専用メソッド)を使うと以下のように変更できます。
/**
* メールアドレスクラス
*/
public class MailAddress {
private final String mMailAddress;
/**
* コンストラクタ
*
* @param mailAddress メールアドレス
*/
public MailAddress(String mailAddress) {
if (StringUtils.isEmpty(mailAddress)) {
throw new IllegalArgumentException("invalid mailAddress");
}
// 文字数が30文字を超えるメールアドレスは禁止
if (mailAddress.length() > 30) {
throw new IllegalArgumentException("mailAddress characters over");
}
mMailAddress = mailAddress;
}
/**
* Gmailアドレスを生成するファクトリーメソッド
*
* @param localPart ローカルパート(@より前の部分)
* @return Gmailアドレスのインスタンス
*/
public static MailAddress createGmail(String localPart) {
return new MailAddress(localPart + "@gmail.com");
}
/**
* 社用メールアドレスを生成するファクトリーメソッド
*
* @param localPart ローカルパート(@より前の部分)
* @return 社用メールアドレスのインスタンス
*/
public static MailAddress createCorporate(String localPart) {
return new MailAddress(localPart + "@qita.com");
}
/**
* サポート用メールアドレスを生成するファクトリーメソッド
*
* @param localPart ローカルパート(@より前の部分)
* @return サポート用メールアドレスのインスタンス
*/
public static MailAddress createSupport(String localPart) {
return new MailAddress(localPart + "@support.example.com");
}
public String getValue() {
return mMailAddress;
}
}
各ドメインのメールアドレスに対応するオブジェクトを生成するファクトリーメソッドを追加しました。
- createGmail()
- createCorporate()
- createSupport()
各ドメインのオブジェクトを生成するときは以下のように書きます。
final MailAddress gmailAddress = MailAddress.createGmail("QiitaTaro");
final MailAddress companyMailAddress = MailAddress.createCorporate("QiitaTaro");
final MailAddress supportMailAddress = MailAddress.createSupport("QiitaTaro");
ファクトリーメソッドを導入することで、各用途に合ったオブジェクトを生成することが楽になります。
これで、呼び出し側で引数の受け渡しミスも減らすことができます。
さいごに
完全コンストラクタの根底には、以下の考え方があります。
- 正常な値のときだけ、オブジェクトを生成する
- 一度生成したオブジェクトは変更させない
完全コンストラクタの設計は値オブジェクトとセットで用いられることが多いです。
しかし、完全コンストラクタの考え方は他の場面でも非常に有効です。
是非、参考にしてみてください。
ここまで読んでいただきありがとうございます。