ドメインモデルの設計

システムの中核となるドメインモデルを設計します。ここでは、「システムが何をするか」といった視点から入るではなく、 「システムが対象としているもの」に着目して開発を進めます。システムの画面イメージなどは描いても良いですが、そ れはあくまでドメインの設計を手助けするものであり、ここではまだ決定しません。

まずは全体的な構成を考察します。ブログがいくつかのブログ記事を持ち、さらにブログ記事が複数のコメントを持つと 考え、次のような概念クラス図を描きました。

ブログ

Blog

それでは、ブログのドメインモデル Blog から考察します。ブログはその所有者やアクセス制御を持つ資源と考えられますから、AbstractResource を継承することにしました。同時に、このオブジェクトがドメインモデルであることを表す Domain 、 永続化可能であることを表す Serializable 、 ドメインオブジェクトを一意に決定できることを表す Identifiable 、 データベースに保存可能であることを表す Storable 、 データベースから削除可能であることを表す Deletable という基本インターフェースを実装します。プロパティとしては、ブログを一意に表すための「 ID 」と「コード」、「概要」、 および「コメント投稿に対するアクセス制御」を用意しました。

プロパティ名 説明・解説
id Integer ID 。このオブジェクトを一意に表すことのできる整数。データベースに登録する前の値は null
code String コード。このオブジェクトを一意に表すことのできる文字列。ユーザが認識することができ、主にこのブログの URL の一部として用いられる。
overview Message 概要。概要はブログのタイトルと説明文を持つ。概要の所有者 ID 、参照アクセス権限、編集アクセス権限は、ブログのそれと同一です。概要の著者 ID は所有者 ID と同一です。
ownerId Integer このブログの所有者のユーザ ID 。AbstractResource から継承したプロパティ。
readAccessControl AccessControl 参照アクセス制御。AbstractResource から継承したプロパティ。所有者が自由に選択可能です。
writeAccessControl AccessControl 編集アクセス制御。AbstractResource から継承したプロパティ。OWNER_ONLY で固定です。
commentAccessControl AccessControl コメント投稿に対するアクセス制御。所有者が自由に選択可能です。

ここまでの考察をソースコードに反映させます。この時点で全てのメソッドが実装される必要はありません。getter や setter 、 identify などの単純なメソッドは実装しておきます。「コード」プロパティは一意な値 でありながらユーザから入力される値でもあるため、コードが重複した際に発生する例外 BlogCodeUsedByOtherException を用意し、store メソッドの throws 句で宣言しています。

/**
 * ブログを表すドメインモデルです。
 *
 * @invariant ${this.writeAccessControl} is
 * {@link org.unitedfront2.domain.accesscontrol.AccessControlType#OWNER_ONLY}
 * @invariant ${this.readAccessControl} is ${this.overview.readAccessControl}
 * @invariant ${this.writeAccessControl} is ${this.overview.writeAccessControl}
 * @invariant ブログ記事が存在するブログは削除できない。
 *
 */
public class Blog extends AbstractResource implements Domain, Serializable,
    Identifiable<Blog>, Storable, Deletable {

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

    /** ID */
    private Integer id;

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

    /** 概要 */
    private Message overview;

    /** コメント投稿に対するアクセス制御 */
    private AccessControl commentAccessControl;

    // store のドキュメントを長々と書く必要はない。何故なら、Java では JavaDoc も継承されるから。
    /**
     * @throws BlogCodeUsedByOtherException 指定したコードが他のブログで既に使用されている
     */
    @Override
    public void store() throws BlogCodeUsedByOtherException {
        // TODO
    }

    @Override
    public boolean identify(Blog blog) {
        if (id == null) {
            return false;
        }
        return id.equals(blog.getId());
    }

    @Override
    public void delete() {
        // TODO
    }

    // getters and setters
}

さらに、equalshashCodetoString を実装しま す。ブログが継承している AbstractResource クラスはこれらのメソッドの実装を支援するための仕組みを提供していますので、それを利用することにします。ここでは toString を実装するために提供される buildToStringBuilder の実 装例を示します。

public class Blog ... {

    // ...

    @Override
    protected void buildToStringBuilder(ToStringBuilder tsb) {
        tsb.append("id", id).append("code", code).append("overview", overview);
        super.buildToStringBuilder(tsb);
        tsb.append("commentAccessControl", commentAccessControl);
    }

    // ...
}

BlogEntry

続いてブログの記事に相当するドメインクラス BlogEntry の考察に入ります。ブログ記事もブログと同様基本インターフェースを実装し、プロパティには Message 型の「記事」を持たせています。ブログ記事は Message 型を継承して実現することが適切のように思えるかもしれませんが、ここでもブログの「概要」プロパティと同様、委譲さ せる形をとっています。具象ドメインクラスの継承はオブジェクト指向設計を破壊する原因に繋がりやすいので、極力避 けた方が良いでしょう。

プロパティ名 説明・解説
entry Message 記事。記事はブログ記事のタイトルと説明文を持つ。記事の所有者 ID 、参照アクセス権限、編集アクセス権限は、ブログのそれと同一です。記事の著者 ID は所有者 ID と同一です。

ブログ記事には改めて ID プロパティを設けることはしませんでした。ブログ記事にとっての ID は、「記事」プロパティ が持っている ID (メッセージ ID )と同値になるからです。ID へアクセスしやすいように、「記事」の ID プロパティ へのショートカット getId メソッドを用意しました。同様に、「コード」プロパティへのショートカット getCode も用意しています。

/**
 * ブログの記事を表すドメインクラスです。
 *
 */
public class BlogEntry implements Serializable, Identifiable<BlogEntry>,
    Storable, Deletable {

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

    /** 記事 */
    private Message entry;

    // equals, hashCode, toString

    @Override
    public boolean identify(BlogEntry other) {
        if (entry == null) {
            return false;
        }
        return entry.identify(other.getEntry());
    }

    @Override
    public void store() {
        // TODO
    }

    @Override
    public void delete() {
        // TODO
    }

    /**
     * ブログ記事の ID を取得します。
     *
     * @return ブログ記事の ID
     * @ensure ${return} equals ${this.entry.id}
     */
    public Integer getId() {
        return entry.getId();
    }

    /**
     * ブログ記事のコードを取得します。
     *
     * @return ブログ記事のコード
     * @ensure ${return} equals ${this.entry.code}
     */
    public String getCode() {
        return entry.getCode();
    }

    // getters and setters
}

Comment

ブログ記事への投稿コメントは、 Comment クラスをそのまま用いることにします。

BlogValidator

各ドメインモデルの検証クラスを作成します。 Message 型のプロパティは既存の MessageValidator を利用すればよいので、ここで新たに作成する必要のあるクラスは Blog のプロパティ「コード」の検証を行う BlogValidator のみです。 BlogValidator の完全なソースコードは BlogValidator のソースコード を参照ください。

public class BlogValidator implements Serializable {

    /** コードの正規表現のデフォルト ([0-9a-z]+) */
    public static final String DEFAULT_CODE_REGEX = "[0-9a-z]+";

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

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

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

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

    // equals, hashCode, toString

    public void validateCode(String code) throws ValidationException {
        Validate.notBlank(code);
        Validate.maxLength(code, codeMaxLength);
        Validate.match(code, codeRegex);
    }

    public void validateCode(Blog blog) throws ValidationException {
        validateCode(blog.getCode());
    }

    // getters and setters
}

ドメインクラス間の関連

最後に、これまでのクラス同士の関連を定義します。

ブログとブログ記事、ブログ記事とコメントの関連は transient で定義している点に注意してくだ さい。これらの関係は常に(永続的に)保障する必要がないため、非永続プロパティとして扱っています。また、それらの 関連を保存するメソッド post と復元するメソッド retrieve* を定義します。 関連の保存は投稿時に行われると考えられるため、ブログおよびブログ記事に post メソッドを用 意しました。また、ブログ記事が存在するブログを安易に削除できないようにするために、 delete メソッドの throws 句に、記事を保持しているブログを削除しようとし たときに発生する例外 BlogEntryExistException を宣言することにしました。

public class Blog ... {

    // ...

    /** ブログ記事リスト */
    private transient List<BlogEntry> entries;

    // ...

    /**
     * 指定した範囲のブログ記事を復元します。記事番号は0から始まる整数で、ID の降順になります。
     *
     * @param start 開始点となる記事番号
     * @param end 終了点となる記事番号
     */
    public void retrieveEntries(int start, int end) {
        // TODO
    }

    /**
     * ブログ記事を投稿します。指定したブログ記事を保存し、このブログと関連付けます。
     *
     * @param blogEntry ブログ記事
     */
    public void post(BlogEntry blogEntry) {
        // TODO
    }

    /**
     * @throws BlogEntryExistException ブログ記事が存在している
     */
    @Override
    public void delete() throws BlogEntryExistException {
        // TODO
    }

    // getters and setters
}
public class BlogEntry ... {

    // ...

    /** コメントリスト */
    private transient List<Comment> comments;

    // ...

    /**
     * このブログ記事のブログコメントを復元します。
     */
    public void retrieveComments() {
        // TODO
    }

    /**
     * コメントを投稿します。コメントを保存し、このブログ記事と関連付けます。
     *
     * @param comment コメント
     */
    public void post(Comment comment) {
        // TODO
    }

    // getters and setters
}
Note

BlogBlogEntry の関係はプロパティによって実現しています。よって、 BlogEntry は、対応するデータベース表が持つであろう blogId プロパティ を持つ必要はありません。これにより、ドメインモデルとデータベース実装では参照関係が逆転しています。このように、 ドメインモデルのプロパティとデータベース表の列が必ずしも一致するとは限りません。