ドメイン層

ドメイン層には United Front 2 の世界をオブジェクト指向によってモデル化したクラス群が配置されます。主要な ロジックはこの層に集中しており、システムの核(ドメインロジック)を形成しています。ドメイン 層のクラスは オブジェクト指向プログラミング を最大限活用するために、POJO で設計され ています。POJO にデータアクセスの機能を持たせることで、シンプルでありながら 単一責任の原則 を守った凝縮度の高いクラス群となっています。

ドメイン層のクラスは、システムがどのように振舞うかではなく、システムが対象としている「もの」に着目して設計され ます。対象となる「もの」の抽象的なデータ型を設計し、その型からきめ細かなメソッド群を導きます。この考え方はオブ ジェクト指向の原点であり、United Front 2 の開発スタイルはおのずと ボトムアップアプローチ になります。

ドメイン層のクラスは org.unitedfront2.domain 以下のパッケージに収録されています。以 降はドメイン層に配置される代表的なクラスについて説明します。

Note: なぜオブジェクト指向か?

なぜオブジェクト指向アプローチなのでしょうか?実際のところ、トランザクションスクリプト に 代表される手続き型のアプローチを採用しても、同様の機能を持つアプリケーションシステムは構築可能です。むしろ開 発の初期フェーズでは、手続き型アプローチの方が理解しやすく扱いやすいかもしれません。オブジェクト指向アプロー チの利点は、システムが成長し複雑化していく過程で現れます。オブジェクト指向アプローチで開発されたソフトウェア は、システム機能(実際にユーザが触れる機能)ではなく、より安定したデータ(ドメイン)を中心に構成されるため、コー ドの再利用性が高まるのです。一貫したオブジェクト指向アプローチの採用は、コードの重複や矛盾を低らし、高品質な ソフトウェアを導きます。

一方で、オブジェクト指向のソフトウェア開発は大変難しいと考えられます。データと振る舞いを適切な粒度でクラスとし て抽出し、そのインターフェースや継承構造、クラス間の関係を定義しなければなりません。この設計を最初から完成さ せることはほとんど不可能であり、何度も再利用を試み、修正を加えていく必要があります。この活動に際限はありませ んが、適度な区切りを付けながら絶えず進めていきます。オブジェクト指向アプローチを採用する際には、まずこのような 考え方をプロジェクトチームに浸透させる必要があります。

Note: トップダウンアプローチ vs ボトムアップアプローチ

オブジェクト指向開発により、より再利用性が高く安定したドメイン層を構築できるようになります。システム機能からトッ プダウンアプローチによって構築したシステムは、ビジネスルールや業務フローに強く依存してしまうため、結果的に柔 軟性や拡張性が低いソフトウェアになってしまう可能性が高いといえます。オブジェクト指向開発では、一旦システム機 能については忘れ、システムが対象としている「もの」に集中します。

Note: Java 言語

Java はオブジェクト指向のコンパイル型言語です。Java の能力を最大限発揮できるのは、厳密に型付けされたオブ ジェクト指向アーキテクチャを採用したときだと考えています。中途半端なオブジェクト指向アーキテクチャでは Java の 能力を引き出せないでしょう。

ドメインモデル

ドメインモデルは、振る舞いとデータの両方を一体化させたオブジェクトモデルです。多くの場合、リレーショナルデータ ベースの1行に対応しています。ドメインモデルは JavaBeans としての性質を持っており、インスタンス化されたオブジェクト(以下、ドメインオブジェクト)は状態を持っています。ドメイ ンオブジェクトはデータを保持する性質から、レイヤーを横断して参照され、特に以下のような場面で利用されます。

  • クライアントからのリクエストのオブジェクト表現
  • コントローラ層でのシステム機能の実装
  • データアクセス層のインターフェース
  • ビュー層でのデータ出力

自身が保持するデータに関する全てのロジックを持ちます。これらのロジックでは、データアクセス処理を行うこともあり ます。データアクセスロジックの実装は DAO パターンを使ってドメインオブジェクトそのものから分離しています。ドメイン オブジェクトにデータアクセス機能を与えるためには、new 演算子によってインスタンス化した後、ド メインオブジェクトのデータアクセスオブジェクトプロパティに値を設定(インジェクション)する必要があります。この操作 は後述するドメインファクトリを利用して行います。

ドメインオブジェクトの transient でないプロパティを、ここでは 永続プロパティ と呼びます。永続プロパティはデータベースと同期することを前提としたプロパ ティになります。例えば、store メソッド実行時にデータベース側で ID が自動生成された場合、 ドメインオブジェクト側の ID プロパティにも値が設定されます。逆に、 transient が付いている プロパティは 非永続プロパティ と呼びます。非永続プロパティは、特に注意書きがない限り 永続化の対象になりません。ドメインモデルは、このような性質をよく考慮した上で設計する必要があります。

Note: ドメインオブジェクトとデータアクセス

ドメインオブジェクトにデータアクセス機能を持たせる設計は、単一責任の原則を守るために必要です。データにアクセス できないドメインオブジェクトでは、必要なロジックの多くをデータアクセス可能な外部クラスに任せざるを得なくなり、責 任範囲が拡散してしまいます。ドメンオブジェクト生成時にデータアクセスオブジェクトを透過的に設定する方法としては、 AspectJ の Load-Time Weaving や Spring Framework の AspectJ を使ったインジェクション などが考えられますが、どちらも一般的でない手法です。そこで、ここでは GoF の プロトタイプパターン を応用した軽量な手法を採用しています。この仕組みの詳細は ドメインファクトリ の項を参照ください。

Note: エンタープライズアーキテクチャパターンのドメインモデルとの違い

United Front 2 のドメインモデルは エンタープライズ アプリケーションアーキテクチャパターン で定義される ドメインモデル を模範としており、データアクセスをカプセル化することで アクティブレコード としての性質も合わせ持っています。エンタープライズアプリケーションアー キテクチャパターンと異なる点は、実質的にはデータアクセスロジックを保持していない点です。データアクセスロジック は DAO パターン によって分離しており、データアクセスオブジェクト はドメインオブジェクトの生成時に 依存性の注入 によって設定しています。

主要メソッド

メソッド名 説明
toString java.lang.Object からオーバーライド。
equals java.lang.Object からオーバーライド。
hashCode java.lang.Object からオーバーライド。
identify 引数に指定したドメインオブジェクトが、このドメインオブジェクトと同一であるかどうかを判定します。このメソッドは、 equals メソッドとは異なり、そのドメインオブジェクトを一意に識別できる最小のプロパティを用いて 同一性を判別します。特に説明がない限り、比較には主キーとなるプロパティが利用されます。このメソッドを実装するク ラスはインターフェース Identifiable を実装します。
store 自身を永続化します。永続化の対象は、transient 修飾子が付いていない、全ての永続プロパ ティです。主キーが設定されていなければ新規のデータとして登録し、主キーが設定されていれば既存のデータを更新 します。永続化の過程で主キーなどが自動生成された場合、自身の対応するプロパティに自動生成された値が設定さ れます。データに不整合が生じた場合、例外が発生します。このメソッドを実装するクラスはインターフェース Storable を実装します。
delete 自身をデータベースから削除します。データに不整合が生じた場合、例外が発生します。このメソッドを実装するクラスは インターフェース Deletable を実装します。
retrievePropertyName 非永続プロパティをデータベースから復元します。
getPropertyName getter メソッドです。
setPropertyName setter メソッドです。
setClassName Dao データアクセスオブジェクトを設定します。
Note: retrieve メソッド vs レイジーロード

非永続プロパティを復元する他の手法としては、レイジーロード が考えられます。レイジー ロードを使えば、非永続プロパティにアクセスしようとした段階でデータベースへアクセスし、データを復元できます。これ はデータベースを意識することなく非永続化プロパティを扱える点で便利ですが、逆に言えば予期しないタイミングで データアクセス処理が発生してしまう危険性をはらんでいます。例えば、トランザクションの境界外のビュー層でデータア クセスが行われてしまう可能性があります。この問題を防ぐために、ここでは retrieve メソッドを明示的に呼び出して、データベースアクセスを確実にトランザクション境界内で行う方式を採用しました。

レイジーロードの実装例
public void getTransientProperty() {
    if (this.transientProperty == null) {
        this.transientProperty = dao.findTransientProperty(this.id);
    }
    return this.transientProperty;
}
            

コンストラクタ

ドメインモデルでは JavaBeans の特性の一つであるデフォルトコンストラクタは必須で、必要に応じて引数付きのコン ストラクタを用意します。引数付きコンストラクタとしては、新規ドメインオブジェクト生成用コンストラクタと既存ドメインオブ ジェクト復元用コンストラクタの二種を用意しておくと便利です。例はサンプルコードを参照ください。

インターフェース Domain

ドメインモデルは、自身がドメインモデルであることを示すためにインターフェース Domain を実装します。Domain はメソッドを定義しないマーカーインターフェースです。インターフェース Domain を実装したドメインモデルは、後述のドメインファクトリからインスタンス化可能でなければな りません。United Front 2 では、ドメインオブジェクトの生成を Spring Framework の DI コンテナを使って 管理しています。

静的構造図

次の図は、サンプルのドメインモデル SampleDomain が対応するデータアクセスインターフェース SampleDomainDao を保持している構造を示しています。

ドメインモデル

サンプルコード

ドメインモデルのサンプル SampleDomain を示します。SampleDomain は ID とコード、文字列、浮動点小数をプロパティとして持つ単純なドメインモデルです。メソッド equalshashCodetoStringApache Commons Lang を使って実装を簡略化しています。データアクセスを伴う storedelete メソッドのほか、データアクセスを伴わない multipleTextLengthAndD という固有メソッドを 持っています。SampleDomain の完全なコードは SampleDomain のソースコード を参照ください。

public class SampleDomain implements Identifiable<SampleDomain>, Storable,
    Deletable, Domain, Serializable {

    /** シリアル番号 */
    private static final long serialVersionUID = -2784958436581567548L;

    /** ログ */
    protected final transient Log logger = LogFactory.getLog(getClass());

    /** ID 。主キー。 */
    private Integer id;

    /** コード。一意な値。 */
    private String code;

    /** 浮動点小数 */
    private Double d;

    /** テキスト */
    private String text;

    // 非永続プロパティは transient で宣言
    /** サンプルドメインデータアクセスオブジェクト */
    private transient SampleDomainDao sampleDomainDao;

    /** デフォルトコンストラクタ */
    public SampleDomain() {
        super();
    }

    /** 新規オブジェクト作成用のコンストラクタ。ID は null 。 */
    public SampleDomain(String code, Double d, String text) {
        super();
        this.code = code;
        this.d = d;
        this.text = text;
    }

    /** 既存オブジェクト復元用のコンストラクタ */
    public SampleDomain(Integer id, String code, Double d, String text) {
        this(code, d, text);
        this.id = id;
    }

    // equals, hashCode, toString の実装には非永続プロパティを含めていません
    @Override
    public boolean equals(final Object other) {
        if (!(other instanceof SampleDomain)) {
            return false;
        }
        SampleDomain castOther = (SampleDomain) other;
        return new EqualsBuilder()
            .append(id, castOther.id)
            .append(code, castOther.code)
            .append(d, castOther.d)
            .append(text, castOther.text).isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder()
            .append(id)
            .append(code)
            .append(d)
            .append(text).toHashCode();
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this)
            .append("id", id)
            .append("code", code)
            .append("d", d)
            .append("text", text).toString();
    }

    // インターフェース Identifiable の実装
    @Override
    public boolean identify(SampleDomain other) {
        if (id == null) {
            return false;
        }
        return id.equals(other.getId());
    }

    // インターフェース Storable の実装
    // ここではコードの整合性チェックを store メソッドの役割としており、メソッドの例外に
    // DuplicateSampleDomainCodeException を定義しています。
    // コードが他と重複していないことを store メソッドの事前条件とする設計も考えられます。
    // その場合、実行時例外を発生させるような設計にします。
    @Override
    public void store() throws DuplicateSampleDomainCodeException {
        if (id == null) {
            if (sampleDomainDao.findByCode(code) != null) {
                // コードが一意でない
                String message = "The code '" + code + "' already exists.";
                logger.warn(message);
                throw new DuplicateSampleDomainCodeException(message);
            }
            sampleDomainDao.register(this);
        } else {
            SampleDomain found = sampleDomainDao.findByCode(code);
            if (found != null && !identify(found)) {
                // コードが一意でない
                String message = "The code '" + code + "' already exists.";
                logger.warn(message);
                throw new DuplicateSampleDomainCodeException(message);
            }
            sampleDomainDao.update(this);
        }
    }

    // インターフェース Deletable の実装
    @Override
    public void delete() {
        sampleDomainDao.delete(id);
    }

    // 独自メソッド
    /** テキスト文字数と浮動小数点の掛け算の結果 */
    public double multipleTextLengthAndD() {
        int textLength;
        if (text == null) {
            textLength = 0;
        } else {
            textLength = text.length();
        }
        return textLength * d;
    }

    // getters and setters ...

    // データアクセスオブジェクトは setter メソッドのみを持つ
    public void setSampleDomainDao(SampleDomainDao sampleDomainDao) {
        this.sampleDomainDao = sampleDomainDao;
    }
}
Warning

同一のドメインオブジェクトを複数インスタンス化した場合、同期をサポートすることが困難になります。ID などによって 一意に識別されるドメインオブジェクトは、一つの処理の中では重複したインスタンスを持たないように気をつける必要が あります。

Warning

getter および setter メソッドにデータアクセスロジックを含ませることは極力避ける必要があります。getter および setter はビュー層から何度も呼び出される可能性があり、パフォーマンスやデータの整合性に悪影響を与え る恐れがあります。

Note: 内部表現を暴露しない設計

Date 型のように、値を素直に setter, setter メソッドで公開してしまうと内部表現を暴露してしまうオブジェクトのプロ パティは、防衛的コピーを行い複製するようにします。

private Date date;

public Date getDate() {
    if (date == null) {
        return null;
    } else {
        return (Date) date.clone();
    }
}

public void setDate(Date date) {
    if (date == null) {
        this.date = null;
    } else {
        this.date = (Date) date.clone();
    }
}
Note: コレクションプロパティ

コレクションプロパティの内部構造を外部から変更できないようにするために、コレクションプロパティは final で定義し、内部の変更処理はメソッド介して行うようにします。また、getter は変更不可 能なコレクションを返します。この設計は永続化プロパティのみが対象で、非永続化プロパティに対しては行う必要はあ りません。

private final Set<String> keywords = new HashSet<String>();

public boolean addKeyword(String e) {
    return keywords.add(e);
}

public boolean addKeywords(Collection<? extends String> c) {
    return keywords.addAll(c);
}

public void clearKeywords() {
    keywords.clear();
}

public boolean removeKeyword(Object o) {
    return keywords.remove(o);
}

public boolean removeKeywords(Collection<?> c) {
    return keywords.removeAll(c);
}

public boolean retainKeywords(Collection<?> c) {
    return keywords.retainAll(c);
}

public Set<String> getKeywords() {
    return Collections.unmodifiableSet(keywords);
}

public void setKeywords(Set<String> keywords) {
    this.keywords.clear();
    this.keywords.addAll(keywords);
}
Note: 担当外のデータアクセスオブジェクトは保持しない

ドメインクラスは通常自分が担当し保持しているデータアクセスオブジェクトを持っています。ドメイン間で依存関係を持っ ている場合などに、他のドメインが担当するデータアクセスオブジェクトを使用したくなりますが、その際はデータアクセス オブジェクトではなくテーブルモジュールを参照するようにします。ドメインクラスはデータの整合性を保つ役割も持ってい るため、安全にデータを操作できます。

ドメイン間の依存関係

Spring Bean 定義

ドメインモデルは Spring Bean 定義ファイル appilcationContext-domain.xml に定義します。ドメインモデルは読み込みのたびにインスタンスを新しく生成するよう、スコープを prototype に で指定しています。

<bean id="sampleDomain" class="org.unitedfront2.domain.SampleDomain"
    scope="prototype">
  <property name="sampleDomainDao" ref="sampleDomainDao"/>
</bean>

ドメインファクトリ

ドメインファクトリはドメインオブジェクトの生成を担当するファクトリクラスです。ドメインファクトリが提供する prototype メソッドは、ドメインオブジェクトが正しく動作するために必要な初期処理を行った後、 新しく生成したドメインオブジェクトを返します。

public interface DomainFactory {

    /**
     * ドメインオブジェクトが動作するために必須となるいくつかの非永続プロパティに値が設定された後の、新しい
     * ドメインオブジェクトを返します。
     */
    <D extends Domain> D prototype(Class<D> domainClass);

    /**
     * ドメインオブジェクトが動作するために必須となるいくつかの非永続プロパティに値が設定された後の、新しい
     * ドメインオブジェクトを返します。返されるドメインオブジェクトには、引数で渡したドメインオブジェクトのプロパ
     * ティが設定されます。引き渡したコピー元のドメインオブジェクトの状態は変化しません。
     */
    <D extends Domain> D prototype(D domainObject);
}
Note: ドメインファクトリパターン

ドメインファクトリクラスを使ってドメインを生成する手法を ドメインファクトリパターン と呼ぶこと にします。ドメインファクトリパターンは GoF の プロトタイプパターン を応用した軽量な手法で す。「new 演算子ではなく prototype メソッドを使う」というのは、United Front 2 の重要なプログラミングルールとなります。このルールは重要な制約ですが、オブジェクト指向設計を扱い やすくするという大きな対価をもたらします。

この設計の欠点は、独自アーキテクチャとなってしまう点です。例えば、ドメインのインスタンス化に new 演算子を使う O / R マッピングソリューションを採用した場合には不都合が生じるかもしれません。United Front 2 で採用しているような良質のライブラリはドメインオブジェクトの生成に関する拡張点を提供しているため問題ありませ んが、United Front 2 以外のアプリケーションに本パターンを適用する際には注意が必要です。

Note: ドメインファクトリの実装

DomainFactory はインターフェースですので、実装クラスが必要です。United Front 2 で は、Spring Framework の DI コンテナを利用した SpringBeanDomainFactory を使っています。

テーブルモジュール

テーブルモジュールは、一連のドメインオブジェクト群に対し横断的な操作を行うクラスです。多くの場合、リレーショナル データベースの表と1対1に対応します。テーブルモジュールオブジェクトはアプリケーションの実行中、ただ一つ存在す る シングルトン としてインスタンス化されます。データアクセスのロジックはデータアクセスオ ブジェクトを使っています。

Note: エンタープライズアーキテクチャパターンのテーブルモジュールとの違い

United Front 2 のテーブルモジュールは エンタープライズ アプリケーションアーキテクチャパターン で定義される テーブルモジュール を模範としています。ただし注意しなければならないの は、ここでは ドメインモデル と同時に使用しているという点です。テーブルモジュールとドメイ ンモデルの役割は明確に分かれています。一つのドメインオブジェクトに関するロジックはドメインモデルに配置し、複数 のドメインオブジェクトに関するロジックはテーブルモジュールに配置します。

主要メソッド

メソッド名 説明
exist 主キーの値を引数として渡し、対応するドメインオブジェクトが存在するか判定します。存在する場合は true 、存在しない場合は false を返します。
find 主キーの値を引数として渡し、完全なドメインオブジェクトを取得します。もし対応するドメインオブジェクトが見つからな かった場合、null を返します。特に明示されない限り、transient となるプ ロパティには値が設定されません。
get 主キーの値を引数として渡し、完全なドメインオブジェクトを取得します。引数には、存在する主キーを指定してください。 ドメインオブジェクトが見つからなかった場合、例外 java.lang.IllegalArgumentException が発生します。特に明示されない限り、 transient となるプロパティには値が設定されません。
setDataAccessObjectName データアクセスオブジェクトを設定します。

上記の基本メソッドに加え、次のようなメソッドを持つことがあります。

メソッド名 説明
install データの初期処理を行います。あるオブジェクトに対して一度だけ実行されます。
findPropertyName 主キーの値を引数として渡し、ドメインオブジェクトのプロパティ propertyName の値を取得します。もし対 応するドメインオブジェクトが見つからなかった場合、null を返します。
findByPropertyName 任意のプロパティ propertyName の値を引数として渡し、その値を持った、完全なドメインオブジェクトまた はそのリストを取得します。もし対応するドメインオブジェクトが見つからなかった場合、null また は空のリストを返します。
findByPropertyName Like 任意のプロパティ propertyName の値の一部を引数として渡し、その値を一部に持った、完全なドメイン オブジェクトまたはそのリストを取得します。もし対応するドメインオブジェクトが見つからなかった場合、 null または空のリストをします。
findCondition 条件 Condition を完成させるために必要なプロパティの値を引数として渡し、条件に合った完全なドメイ ンオブジェクトまたはそのリストを取得します。もし対応するドメインオブジェクトが見つからなかった場合、 null または空のリストを返します。
getPropertyName 主キーの値を引数として渡し、ドメインオブジェクトのプロパティ propertyName の値を取得します。引数に は、存在する主キーを指定してください。値が見つからなかった場合、 java.lang.IllegalArgumentException が発生します。
getByPropertyName 任意のプロパティ propertyName の値を引数として渡し、その値を持つ完全なドメインオブジェクトまたは そのリストを取得します。引数には、存在する主キーを指定してください。ドメインオブジェクトが見つからなかった場合、 java.lang.IllegalArgumentException が発生します。
getPropertyName1 ByPropertyName2 任意のプロパティ propertyName2 の値を引数として渡し、その値を持つドメインオブジェクトのプロパティ propertyName1 の値を取得します。引数には、存在する主キーを指定してください。値が見つからなかっ た場合、java.lang.IllegalArgumentException が発生します。
count 永続化されているドメインオブジェクトの総数を返します。

静的構造図

SimpleTableModule はテーブルモジュールの実装を支援する抽象クラスです。SimpleTableModule は整数型の主 キーをただひとつ持ち、継承構造を持たないドメインモデルに対して有効です。この抽象クラスを継承することで、 existfindget などの基本メソッドの実装を再利用 できます。

テーブルモジュール

サンプルコード

テーブルモジュールのサンプル SampleDomainTable を示します。SampleDomainTable はドメインモデル SampleDomain に対応するテーブルモジュールで、基本的なメソッドに加え、 コードから SampleDomain オブジェクトを復元するメソッドと永続化されている SampleDomain オブジェクトの件数を取得する count メソッドを提供しています。SampleDomainTable の完全なコードは SampleDomainTable のソースコード を参照ください。

public class SampleDomainTable extends SimpleTableModule<SampleDomain> {

    private SampleDomainDao sampleDomainDao;

    // SimpleTableModule の抽象メソッドを実装
    @Override
    protected SimpleFindable<SampleDomain> getSimpleDao() {
        return sampleDomainDao;
    }

    public SampleDomain findByCode(String code) {
        return sampleDomainDao.findByCode(code);
    }

    public SampleDomain getByCode(String code) throws IllegalArgumentException {
        SampleDomain sampleDomain = findByCode(code);
        if (sampleDomain == null) {
            String message = "The sample domain code '" + code + "' not found.";
            logger.warn(message);
            throw new IllegalArgumentException(message);
        }
        return sampleDomain;
    }

    public boolean existByCode(String code) {
        return findByCode(code) != null;
    }

    /** 登録されているサンプルドメインの総数を取得します。 */
    public int count() {
        return sampleDomainDao.count();
    }

    public void setSampleDomainDao(SampleDomainDao sampleDomainDao) {
        this.sampleDomainDao = sampleDomainDao;
    }
}

Spring Bean 定義

テーブルモジュールはアプリケーション実行中ただ一つ存在するシングルトンオブジェクトで、Spring DI コンテナに よって管理されます。

<bean id="sampleDomainTable" class="org.unitedfront2.domain.SampleDomainTable">
  <property name="sampleDomainDao" ref="sampleDomainDao"/>
</bean>

検証クラス

検証クラスはドメインモデルと一対一に対応し、ドメインオブジェクトの状態を検証するメソッド群を提供します。検証クラ スのプロパティは検証データの制約条件として利用されます。検証オブジェクトは主にクライアントからの入力値を検証 する際に利用されます。また、検証オブジェクトをビューへ転送すれば、制約条件を表示させることができます。検証クラ スは JavaBean ですが、アプリケーション実行中ただ一つ存在する シングルトン として利 用されます。

主要メソッド

メソッド名 説明
toString java.lang.Object からオーバーライド。
equals java.lang.Object からオーバーライド。
hashCode java.lang.Object からオーバーライド。
validatePropertyName プロパティ propertyName の値を引数にとり、正しい値であるか検証します。正しくない場合、 org.unitedfront2.validation.ValidationException が発生します。
getPropertyName プロパティの getter メソッドです。
setPropertyName プロパティの setter メソッドです。

検証例外

プロパティが不正な値をとっている場合、validate メソッドは検証例外 ValidationException を発生させます。ValidationException は多言語対応したエラーメッセージを保持します。

throw new ValidationException("sample.SampleDomain.validation.error", null);

サンプルソース

検証クラスのサンプル SampleDomainValidator を示します。SampleDomainValidatorvalidateCode メソッドは、SampleDomain オブジェクトのプロパティ code の値を検証します。検証ロジックの実装にはユーティリティクラス Validate を使っています。最後にテーブルモジュール SampleDomains を使ってコードが一意のデータで あることを検証しています。SampleDomainValidator の完全なコードは SampleDomainValidator のソースコード を参照ください。

public class SampleDomainValidator implements Serializable {

    /** デフォルトのコードの最小文字数のデフォルト (2) */
    public static final int DEFAULT_CODE_MIN_LENGTH = 2;

    /** デフォルトのコードの最大文字数のデフォルト (32) */
    public static final int DEFAULT_CODE_MAX_LENGTH = 32;

    /** デフォルトのコードの正規表現 ([\p{Alnum}]*) */
    public static final String DEFAULT_CODE_REGEX = "[\\p{Alnum}]*";

    /** デフォルトのテキストの最大文字数 (128) */
    public static final int DEFAULT_TEXT_MAX_LENGTH = 128;

    /** シリアル番号 */
    private static final long serialVersionUID = 8383063072303370633L;

    /** コードの最小文字数 */
    private int codeMinLength = DEFAULT_CODE_MIN_LENGTH;

    /** コードの最大文字数 */
    private int codeMaxLength = DEFAULT_CODE_MAX_LENGTH;

    /** コードの正規表現 */
    private String codeRegex = DEFAULT_CODE_REGEX;

    /** テキストの最大文字数 */
    private int textMaxLength = DEFAULT_TEXT_MAX_LENGTH;

    /** サンプルドメインテーブル */
    private transient SampleDomainTable sampleDomainTable;

    @Override
    public boolean equals(final Object other) {
        if (!(other instanceof SampleDomainValidator)) {
            return false;
        }
        SampleDomainValidator castOther = (SampleDomainValidator) other;
        return new EqualsBuilder()
            .append(codeMinLength, castOther.codeMinLength)
            .append(codeMaxLength, castOther.codeMaxLength)
            .append(codeRegex, castOther.codeRegex)
            .append(textMaxLength, castOther.textMaxLength).isEquals();
    }

    @Override
    public int hashCode() {
        return new HashCodeBuilder()
            .append(codeMinLength)
            .append(codeMaxLength)
            .append(codeRegex)
            .append(textMaxLength).toHashCode();
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this)
            .append("codeMinLength", codeMinLength)
            .append("codeMaxLength", codeMaxLength)
            .append("codeRegex", codeRegex)
            .append("textMaxLength", textMaxLength).toString();
    }

    public void validateCode(SampleDomain sampleDomain)
        throws ValidationException {
        String code = sampleDomain.getCode();
        Validate.notBlank(code);
        Validate.lengthInRange(code, codeMinLength, codeMaxLength);
        Validate.match(code, codeRegex);
        SampleDomain found = sampleDomainTable.findByCode(code);
        if (found != null && !sampleDomain.identify(found)) {
            throw new ValidationException("sample.validation.codeUsedByOther",
                    new Object[] {code},
                    "The code '" + code + "' is used by other.");
        }
    }

    public void validateText(SampleDomain sampleDomain)
    	throws ValidationException {
        Validate.notBlank(sampleDomain.getText());
        Validate.maxLength(sampleDomain.getText(), textMaxLength);
    }

    // getters and setters ...

    public void setSampleDomainTable(SampleDomainTable sampleDomainTable) {
        this.sampleDomainTable = sampleDomainTable;
    }
}

Spring Bean 定義

検証クラスのプロパティはデフォルト値が設定されているため、変更しない限り特別なプロパティの設定は不要です。

<bean id="sampleDomainValidator" class="org.unitedfront2.domain.SampleDomainValidator">
  <property name="sampleDomainTable" ref="sampleDomainTable"/>
</bean>