ドメインモデル駆動開発をやってみる(5)
画面を作成する
ユースケースコントローラまで作成できたので画面を作ることにします。シナリオは伝言の確認・登録、従業員マスタ・メンテをターゲットにします。
リポジトリの改訂
マスタ・メンテ画面を作るにあたって、データの追加・削除をリポジトリに追加します。また、検索処理をよりプリミティブなIQueryableを返すGetTableメソッド用意して、Findメソッドは拡張メソッドで定義するようにしました。
これによって、リポジトリ経由でもLINQが持っているソートやグルーピングなどの機能をフル活用できるようになります。このあたりのパターンは今までと発想を変えないと変な制限を与えてLINQの良いところを殺してしまうので注意が必要ですね。
public interface IRepository{ IQueryable GetTable(); TEntity Add(TEntity entity); void Remove(TEntity entity); } public static class RepositoryExtender { public static List Find ( this IRepository repository, Expression > query) { return repository.GetTable().Where(query).ToList(); } ... }
画面の実装
伝言登録の画面構成はデザイナを使って簡単に作成できます。新しいListViewというコントロールが加わったようです。試しに使ってみました。検索結果からオブジェクトをナビゲートして関連データもEval("ReceiptPerson.Name") のようにして表示することができます。また、今回新しいデータソースにLinqDataSourceが追加されているのですが更新処理はDataContextを利用する必要があるようなので今回は採用しませんでした。
<asp:ListView ID="ListView1" runat="server" DataSourceID="ObjectDataSource1"> ... <ItemTemplate> <tr id="Tr4" runat="server" style="background-color:#DCDCDC;color: #000000;"> <td id="Td3" runat="server"> <asp:LinkButton ID="LinkButton1" runat="server" CommandArgument='<%# Eval("Id") %>' CommandName="Done" OnCommand="LinkButton1_Command" Text='<%# Eval("Id") %>'></asp:LinkButton> </td> <td id="Td4" runat="server"> <asp:Label ID="ReceiptDateTimeLabel" runat="server" Text='<%# Eval("ReceiptDateTime", "{0:t}") %>' /> </td> <td id="Td5" runat="server"> <asp:Label ID="ActionLabel" runat="server" Text='<%# Eval("Action") %>' /> </td> <td id="Td6" runat="server"> <asp:Label ID="ReceiptPersonLabel" runat="server" Text='<%# Eval("ReceiptPerson.Name") %>' /> </td> <td id="Td7" runat="server"> <asp:Label ID="TargetPersonLabel" runat="server" Text='<%# Eval("TargetPerson.Name") %>' /> </td> ... </ItemTemplate> </asp:ListView>
伝言を登録する処理の実装です。画面(aspx)側のコードは最小限で、ユースケースをキックする程度コードになります。
public partial class EntryMessage : MessageUseCasePage { ... protected void EntryButton_Click(object sender, EventArgs e) { if (!Page.IsValid) return; ExecuteAction(delegate() { BuildMessage(); UseCaseService.EntryMessage(PhoneMessage); Response.Redirect("MessageList.aspx"); }); } }
アプリケーションの処理は、ユースケースコントローラからドメインモデルを利用し実行されます。複雑な検証処理もこの通り動きます。
画面ヘルパー
1つのユースケースを複数の画面で構成することはよくあります。この場合画面間でデータを共有する仕組みが必要になりますが、ユースケースで共有するデータを保持するヘルパーを利用すると便利です。さらに検索処理などをヘルパー側に切り出して実装することで、画面(aspx)での処理を最小限にできます。ヘルパー自体は特別なベースクラスを必要しないように実装可能で単体テストが可能です。何度も言いますがテストの難しいコード部を最低限にできます。
public class MessageUseCasePageHelper ... public Employee ReceiptPerson { get { return (Employee)Session["ReceiptPerson"]; } set { Session["ReceiptPerson"] = value; } } public Message PhoneMessage { get { return (Message)Session["PhoneMessage"]; } set { Session["PhoneMessage"] = value; } } public ListGetCurrentReceiptPersonMessageList(bool todayOnly) { if (todayOnly) { var now = DateTime.Now; return Runtime.MessageRepository.Find( p => p.ReceiptPerson == ReceiptPerson && p.ReceiptDateTime.Day == now.Day && p.ReceiptDateTime.Month == now.Month && p.ReceiptDateTime.Year == now.Year); } else { return Runtime.MessageRepository.Find( p => p.ReceiptPerson == ReceiptPerson); } } ... }
個々の画面おいて、更新系の処理はをユースケースコントローラをキックする処理、検索系は画面ヘルパーの利用してDataSourceに設定する処理でほぼ記述できてしまいます。
public partial class MessageList : MessageUseCasePage { ... protected void Button1_Click(object sender, EventArgs e) { ExecuteAction(delegate() { PhoneMessage = UseCaseService.CreateMessage(); Response.Redirect("EntryMessage.aspx"); }); } protected void LinkButton1_Command(object sender, CommandEventArgs e) { ExecuteAction(delegate() { var msg = Runtime.MessageRepository.FindById(int.Parse((string)(e.CommandArgument))); UseCaseService.Confirm(msg); RefreshGrid(); }); } private void RefreshGrid() { adapter.DataSource = Helper.GetCurrentReceiptPersonMessageList(CheckBox1.Checked); this.ListView1.DataBind(); } }
残るはデータベースの実装
画面まで作成できました。データベースはありませんがアプリケーションを実際に作成して実行することができました。あとは永続化を実装するだけです。既存コードにどの程度影響するかやってみないとわかりませんが、興味深いところです。