Readable Code その1

いいコード書いてますか?
最近読んだ「Readable Code」という本が面白かったので、その感想とためになったことを紹介したいと思います。

リーダブルコード ―より良いコードを書くためのシンプルで実践的なテクニック (Theory in practice)

この本ではいいコードを読み易いコードと定義付けて、そういったコードを書くためにはどうすればいいかという事を具体的なコード例を交えて解説しています。

コードのインデントや書式などの見た目を整えるといった事から、変数の命名や意味付け、関数の構造化、デザインパターンの導入まで、その内容は多岐に渡ります。今回はその中でも特に使えそうな内容について説明したいと思います。

と、その前に個人的にいいコードの条件として考えているポイントについて説明しておきます。

俺的いいコードの条件

いいコードの条件としては、まずバグが無いというのが理想でしょう。
しかしプログラムは書いた通りに完璧に動作しますが、それを実装する人間は完璧とは程遠い不完全な代物です。バグの無いプログラムを書く事は不可能と言えます。
では、いいコードの条件としてバグが無いというのを挙げるのは非現実なことでしょうか?
確かにそうかもしれません、しかしこう考える事もできます。

バグを無くす事は出来ないがバグが発生した時、原因の発見修正の迅速化修正による影響範囲を局所化する事はできる。
間違いは必ず起こることを認めて、それをいかにして迅速に確実に収束させるかを目指す。

これがエンジニアとして目指すべき姿勢だと思いませんか?そのためにできる事ならなんだってする。それが在るべきエンジニア像でしょう。


少し話がそれますが、プログラミングはどこまで行っても結局のところ職人技です。そこにはどうしても属人性が含まれてしまいます。
しかし、チーム・組織で働く以上、属人性はなるべく入り込まない方が不確定要素が減り、色々とコントロールし易くなります。
実際私もフレームワークやドキュメントを作る時は、誰でも使えるように、誰でも理解出来るように注意しています。
これはチーム・企業で働く以上、当たり前の行動規範でしょう。

しかし、いちエンジニア・いちプログラマとして見た場合、こんなことはクソくらえとなります。
属人性が排除されたコードなんてものは、魂のこもらない人形みたいなもの、ようするにクソだということです。
こんなことは公には言えないですが、いち企業人といちエンジニアとで思想や信条を使い分ける必要はあるが、エンジニアとしての魂や誇りといったものは捨てる必要はなく、持ち続けて欲しい、持ち続けたいと思う今日この頃です。


閑話休題

では、ここから本書に書かれている内容に触れていきます。

目次

Readable Codeの目次を以下に示します。
1.表面上の改善

  • 名前に情報を詰め込む
  • 誤解されない名前
  • 美しさ
  • コメントすべきことを知る
  • コメントは正確で簡潔に

2.ループとロジックの単純化

  • 制御フローを読みやすくする
  • 巨大な式を分割する
  • 変数と読みやすさ

3.コードの再構成

  • 無関係の下位問題を抽出する
  • 一度に1つのことを
  • コードに思いを込める
  • 短いコードを書く

4.選抜テーマ

  • テストと読みやすさ

目次を見ただけでどういった内容の事が書かれているかだいたい把握できるでしょう。

前置きが長くなってしまったので、本編はまた次回に続きます。

つづく…

アーキテクト宣言

長い間お休みしてましたが、転職して東京に来てから早二年、ようやくアーキテクトとしての道を歩みはじめました。
プログラマーとアーキテクトの違いは、本人がアーキテクトと宣言したかどうかなんて事がよく言われたりしますが、実際の所、周りが認めてくれなければアーキテクトもクソもないわけで。。。

まぁ、そんなこんなで周りに認めてもらってアーキテクトとして働ける環境や仕事がやっとこさ構築できたというわけです。

これからは今までと同様に技術的なネタも書いていきますが、それプラス、アーキテクチャとは何か?とか、アーキテクチャをどのようにして他者に伝えるか?とか、アーキテクチャに関する勉強ネタなんかも書いていきたいと思います。

それでは、
俺はアーキテクトだ!

NDesk.Options(Mono) - コマンドラインパーサー

Monoでコマンドラインパーサーがあったので使ってみた。

使い方はこんな感じ

string data = null;
bool help   = false;
int verbose = 0;

var p = new OptionSet () {
  { "file=",      v => data = v },
  { "v|verbose",  v => { ++verbose } },
  { "h|?|help",   v => help = v != null }
};
List<string> extra = p.Parse(args);

OptionSetAddメソッドが定義されていて、以下のようなシグネチャのメソッドが定義されている。

  • Add(string prototype, Action action);
  • Add(string prototype, string description, Action action)

prototype引数には、コマンドラインのオプション引数の引数名を指定する。|(パイプ)はorを意味する。

v|verbose

と指定した場合は、コマンドライン引数として、

PS > コマンド名 -v
PS > コマンド名 -verbose

のように渡すことができる。

末尾に=(イコール)を付けた場合は、

file=

以下のように引数名と値という形で指定できるようになる。

PS > コマンド名 -file=100.txt

description引数はその名の通り、オプション引数の説明になる。これはOptionSetクラスに定義されているWriteOptionDescriptionsメソッドというコマンドラインの説明文を出力するメソッドを呼び出した時にその出力に含まれるようになる。

action引数には、コマンドラインをパースする中で実際にオプション引数が見つかった時にその値をどのように処理するかを委譲するデリゲートを指定する。

これらの設定を行ってから、Parseメソッドを呼び出すと戻り値として、オプション以外のコマンドライン引数がコレクションとして返ってくる。

使い易いといえば使い易いけど、値の設定にデリゲートを渡すという発想が、完全に一般のデベロッパを無視しているので、もう少し使いやすくするために属性ベースで値を取得できるラッパを作ってみた。

CommandlineParser

使い方は以下のようになる。
まず、オプション引数を格納するクラスを定義し、値を格納するプロパティをCommandlineOption属性でマークする。

MyOptions.cs

class MyOptions {
    [CommandlineOption("n", "name", Description = "名前を指定します。")]
    public string Name { get; set; }
    [CommandlineOption("s", "switch", Description = "スイッチ", OptionType = CommandlineOptionType.Switch)]
    public bool Switch { get; set; }
    [CommandlineOption("value")]
    public int Value { get; set; }
}

後はCommandlineParserクラスをインスタンス化し、Parseメソッドを呼び出すだけ。

[Test]
public void コマンドラインをパースできる() {
    // コマンドライン引数
    var args = new[] { "-n=hoge", "-switch", "value1", "value2", "-value=20" };

    var result = cmdlineParser.Parse<MyOptions>(args);

    Assert.That(result, Is.Not.Null);
    Assert.That(result.Values, Has.Count.EqualTo(2));
    // Valuesプロパティにオプション引数以外の値が入っている。
    Assert.That(result.Values[0], Is.EqualTo("value1"));
    Assert.That(result.Values[1], Is.EqualTo("value2"));

    // オプション引数がカスタム属性でマークしたプロパティに設定されている。
    Assert.That(result.Options.Name, Is.EqualTo("hoge"));
    Assert.That(result.Options.Switch, Is.True);
    Assert.That(result.Options.Value, Is.EqualTo(20));

    // コマンドラインの説明文を取得できる。
    Console.WriteLine(cmdlineParser.GetCommandlineUsage<MyOptions>());
}

戻り値としてCommandlineParseResultが返ってくるので、そこからオプション引数の情報を取得できるようになっている。

探せば色々とあるものですね。

以下ソース

続きを読む

DataGridViewにデータバインドして編集・削除するのに最適な方法は?

WindowsFormで業務アプリを開発する場合、DataGridViewの様にデータの表示と編集作業を実際のデータソースの種類を問わずに一括してやってくれるコントロールがあると非常に便利である。

単純な編集作業ならば、これにデータバインドしてちょこちょこっとコードを書いてやるだけで、目的の仕様を達成できるのだから、ありがたくて涙が出てくる。

.NETにはデータベースにアクセスする技術が何種類かあって、どれを選ぶかによってDataGridViewとの連携のし易さもかなり変わってくる。

その辺の違いを調べる為に以下の仕様をいくつかのデータベースアクセス技術で実装して、その違いを比べてみた。

  • 表示するレコードは複数のテーブルをJOINしている(JOINで対応できない場合はビューを使う)。
  • 属性を変更できる。
  • レコードを削除できる。
ER図

ためした技術は以下、

  1. DataSet
  2. 型付けDataSet
  3. LINQ to SQL
  4. ORM(iBATIS.NET)
画面

画面ロード時にレコードを全件検索して、画面を閉じる時にレコードに対する変更をコミットする。

DataSet

まずはDataSetから(コードを書くスタイル)

MainForm.vb

Imports System.Text
Imports System.Data.SqlClient

Public Class MainForm
    Private Const CONNECTION_STRING = "Data Source=localhost\SQLEXPRESS;Initial Catalog=Sample;Persist Security Info=True;User ID=sa;Password=*******"

    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        Dim dataSet = New DataSet()

        Using sqlCon = New SqlConnection() With {.ConnectionString = CONNECTION_STRING}
            sqlCon.Open()

            Dim sql = New StringBuilder()
            sql.AppendLine("SELECT t1.Id,t1.Name,t2.Name AS Organization")
            sql.AppendLine("FROM Person AS t1")
            sql.AppendLine("INNER JOIN Organization AS t2")
            sql.AppendLine("  ON t1.Organization = t2.Id")

            Using sqlDa = New SqlDataAdapter(sql.ToString(), sqlCon)
                sqlDa.Fill(dataSet)
            End Using
        End Using

        DataGridView1.DataSource = dataSet.Tables(0)

        MyBase.OnLoad(e)
    End Sub

    Protected Overrides Sub OnFormClosed(ByVal e As System.Windows.Forms.FormClosedEventArgs)
        Dim dataTable = DirectCast(DataGridView1.DataSource, DataTable).GetChanges()
        ' 変更無し
        If dataTable Is Nothing Then Return

        Using sqlCon = New SqlConnection() With {.ConnectionString = CONNECTION_STRING}
            sqlCon.Open()

            Using sqlDa = New SqlDataAdapter()
                sqlDa.UpdateCommand = New SqlCommand("UPDATE Person SET Name = @Name WHERE Id = @Id", sqlCon)
                sqlDa.UpdateCommand.Parameters.Add("@Name", SqlDbType.VarChar, 50, "Name")
                sqlDa.UpdateCommand.Parameters.Add("@Id", SqlDbType.BigInt, 8, "Id")

                sqlDa.DeleteCommand = New SqlCommand("DELETE Person WHERE Id = @Id", sqlCon)
                sqlDa.DeleteCommand.Parameters.Add( _
                    New SqlParameter("@Id", SqlDbType.BigInt, 8, "Id") With {.SourceVersion = DataRowVersion.Original} _
                )
                sqlDa.Update(dataTable)
            End Using
        End Using

        MyBase.OnFormClosed(e)
    End Sub
End Class

SqlDataAdapterを使い、最低限のコードで済ませているつもり。検索するレコードはJOINするので、SqlCommandBuilderによるUpdate文などの自動生成には対応していない。
なのでUpdateCommandDeleteCommandは独自に設定している。

思っていたよりもコードがすっきりしていて、わかり易い。

型付けDataSet

次、型付けDataSet。こちらはデザイナによるコード生成で全てまかなえるので、コード無し。

SampleDataSet.xsd


GetDataメソッド


UpdatePersonメソッド


DeletePersonメソッド

あとはこいつをデザイナでフォームにぽとぺたして、DataGridViewと関連付けるだけ。

これだけで動く。問題は自分で検索のタイミングや更新のタイミングを自由に制御しづらい点。

LINQ to SQL

DLINQ

まずはLINQデザイナでテーブルをぽとぺたする。

SampleDatabase.dbml

画面側

MainForm.vb

Imports System.Data.SqlClient

Public Class MainForm
    Private _db As SampleDatabaseDataContext = New SampleDatabaseDataContext()

    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        Dim dataSource = From p In _db.PersonView

        BindingSource1.DataSource = dataSource

        MyBase.OnLoad(e)
    End Sub

    Protected Overrides Sub OnFormClosed(ByVal e As FormClosedEventArgs)
        Dim changeSet = _db.GetChangeSet()
        Dim deleteSet = changeSet.Deletes.Select(Function(o As PersonView) o.Id)
        Dim updateSet = changeSet.Updates.Select( _
            Function(o As PersonView) New With {.Id = o.Id, .Name = o.Name} _
        )
        For Each id In deleteSet
            _db.ExecuteCommand("DELETE Person WHERE Id = {0}", id)
        Next

        For Each o In updateSet
            _db.ExecuteCommand("UPDATE Person SET Name = {0} WHERE Id = {1}", o.Name, o.Id)
        Next

        MyBase.OnFormClosed(e)
    End Sub
End Class

更新するところがLINQでなくなっているのが痛いけど、仕方がない。*1

以外とすっきりしている。しかし、DataSetに比べ若干敷居が高いかもしれない。

ORM(iBATIS.NET)

iBATIS.NETを使った場合

まずはPersonクラスを定義する。

Person.vb
Public Class Person

    Private _id As Long
    Public Property Id() As Long
        Get
            Return _id
        End Get
        Set(ByVal value As Long)
            _id = value
        End Set
    End Property

    Private _name As String
    Public Property Name() As String
        Get
            Return _name
        End Get
        Set(ByVal value As String)
            _name = value
        End Set
    End Property

    Private _organization As String
    Public Property Organization() As String
        Get
            Return _organization
        End Get
        Set(ByVal value As String)
            _organization = value
        End Set
    End Property

End Class

マッピングファイル

Person.xml
<?xml version="1.0" encoding="utf-8" ?>
<sqlMap namespace="OrmSample"
        xmlns="http://ibatis.apache.org/mapping"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">

    <resultMaps>
        <resultMap id="Person" class="OrmSample.Person, OrmSample">
            <result column="Id" property="Id" />
            <result column="Name" property="Name" />
            <result column="Organization" property="Organization" />
        </resultMap>
    </resultMaps>
    
    <statements>
        <select id="selectPerson" resultMap="Person">
            SELECT t1.Id, t1.Name, t2.Name AS Organization
            FROM Person AS t1
            INNER JOIN Organization AS t2
              ON t1.Organization = t2.Id
        </select>
    </statements>
</sqlMap>

画面側

MainForm.vb

Imports System.ComponentModel

Imports IBatisNet.DataMapper
Imports IBatisNet.DataMapper.Configuration

Public Class MainForm
    Private _sqlMapper As ISqlMapper

    Protected Overrides Sub OnLoad(ByVal e As System.EventArgs)
        Dim sqlMapBuilder = New DomSqlMapBuilder()
        _sqlMapper = sqlMapBuilder.Configure()

        BindingSource1.DataSource = _sqlMapper.QueryForList(Of Person)("selectPerson", Nothing)

        MyBase.OnLoad(e)
    End Sub

    Protected Overrides Sub OnFormClosed(ByVal e As FormClosedEventArgs)
        ' 面倒くさい

        MyBase.OnFormClosed(e)
    End Sub
End Class

どのオブジェクトが変更されたかを調べるのが面倒くさかったので更新系は実装していない。だいぶ面倒くさいのは確か。

結果をまとめると、

技術 参照系 更新系 総合点
(5点満点)
雑感
DataSet 4 動的なSQLには不向き
型付けDataSet 4 動的なSQLには不向き。
デザイナを使用するため、画面のコードに直接SQLを書き込む必要がある。
LINQ to SQL 3.5 イカしてる。
ORM 2.5 とにかく面倒くさい。
使うならば変更を検知するなんらかの仕組みが必要

参照系ならばどの技術を使っても大差は無い(動的な条件に強いか弱いかぐらい)。

逆に更新系では、変更を検知する仕組みが初めから組み込まれているDataSetやDLINQはかなり強いので、ORMなんかのPONOをエンティティとして使うフレームワークはどうしても弱くなってしまう。

まぁ、DataGridView使って楽したかったらDataSetを使えって事ですね。結論でました。

*1:LINQはビューの更新に対応していない。

MVPパターンを業務アプリに適用する − 画面遷移

MVPパターンにおける画面遷移のやり方を調べていたが、具体的な情報があまりなく、これだというのが無かったので自分なりに考えてみた。
業務アプリでやるからには極力シンプルで、誰にでもわかるやり方にする必要がある*1

とりあえず考えたのは、下図のようにプレゼンター側で遷移するビューを取り出し、必要な値を設定し表示するというやり方。

概要図

これくらい単純なのが一番いい。

ビューに値を設定する方法としては、

  1. コントロールを参照して直接値を設定する。
  2. プロパティを定義して、間接的に値を設定する。

が考えられるが、プロジェクト的には1.を採用したい。簡単だし、受け入れられ易い。プロパティってなに?って言われちゃうレベルだからね。

しかし、いくつか問題点がある。

  1. コントロールを直接参照するので、コントロール名を変更されたり、削除されたりするとコンパイルエラー
  2. 型の不一致(例えばTextBoxのTextプロパティはString型だけど、欲しいのはInteger型)

1.は、画面がころころ変わるのは事前に設計がちゃんとできていないからなので、それ以前の問題
事前の設計がきちんと行われていれば、コントロール名が変更されたり、削除される事は(ほとんど)無い。
2.は、利用する側でその都度変換をかけていては、あちこちに同じ様なコードが記述されてしまうので、
バグの温床になりかねない。

結果として、一番安全なのは、

  1. コントロールprivateにして、外部からアクセスできないようにする*2
  2. コントロールの値に外部からアクセスする為のプロパティを用意する。
  3. その際、型変換が必要な場合は行う。

3.は、プロパティを定義してもらう為の説得材料にもなりそう。こうしないと危険でしょ?みたいな。

実装

ということで、プレゼンターからビューにアクセスする為の変更を稚拙のMVPフレームワークに施していく。

まずは、PresenterBaseにコンテナからビューを取得する為のメソッドを追加する。

PresenterBase.vb(一部省略)
Public MustInherit Class PresenterBase(Of TView As Form)
    Private _container As IUnityContainer

    Public Sub SetContainer(ByVal container As IUnityContainer)
        _container = container
    End Sub

    Protected Function GetView(Of T As Form)() As T
        Return _container.Resolve(Of T)()
    End Function
End Class

コンテナの設定は後でやるとして、GetViewというメソッドを定義した。

このメソッドは単に型パラメータで渡された型のインスタンスResolveメソッドで取り出して返すだけ。

プレゼンターへのコンテナの設定はPresenterExtensionが行う。

PresenterExtension.vb(一部省略)

Public NotInheritable Class PresenterExtension
    Inherits UnityContainerExtension

    Protected Overrides Sub Initialize()
        Me.Context.Strategies.Add( _
            New PresenterInitializeStrategy(Me.Container), UnityBuildStage.Setup _
        )
    End Sub

    Public NotInheritable Class PresenterInitializeStrategy
        Inherits BuilderStrategy

        Private _container As IUnityContainer

        Public Sub New(ByVal container As IUnityContainer)
            _container = container

            Initialize()
        End Sub

        Public Overrides Sub PostBuildUp(ByVal context As IBuilderContext)
            Dim view = context.Existing
            Dim viewType = view.GetType()

            If _presenterDefinitions.ContainsKey(viewType) Then
                Dim presenter = _container.Resolve(_presenterDefinitions(viewType))

                presenter.SetView(view)
                presenter.SetContainer(_container)
            End If

            MyBase.PostBuildUp(context)
        End Sub
    End Class
End Class

見ての通り。

サンプルプログラムには画面遷移が無いので、画面遷移するように下図のメニュー画面を作る。

画面イメージ

コードはこんな感じ

MenuView.vb
Public Class MenuView
    Public Event MenuClick(ByVal sender As Object, ByVal e As MenuClickEventArgs)

    Private Sub menu_Click(ByVal sender As Object, ByVal e As EventArgs) Handles btnCalc.Click
        RaiseEvent MenuClick(sender, New MenuClickEventArgs(DirectCast(sender, Button).Tag))
    End Sub
End Class

Public Class MenuClickEventArgs
    Inherits EventArgs

    Private _menuKind As MenuKind

    Public ReadOnly Property MenuKind() As MenuKind
        Get
            Return _menuKind
        End Get
    End Property

    Public Sub New(ByVal menuKind As MenuKind)
        _menuKind = menuKind
    End Sub
End Class

メニューをいくつか定義するつもりなので、プレゼンター側でボタンのイベントハンドラを登録させるのはやめて、メニューがクリックされた事を通知する為の専用のイベントを定義した。

クリックされたメニューはMenuClickEventArgsクラスのMenuKindプロパティで判断できるようになっている。

MenuKind列挙型の定義は以下

Enums.vb
Public Enum MenuKind
    計算機 = 1
End Enum

で、このメニュー画面のプレゼンター

MenuViewPresenter.vb
Public Class MenuViewPresenter
    Inherits PresenterBase(Of MenuView)

    Protected Overrides Sub OnViewSet(ByVal view As MenuView)
        If view IsNot Nothing Then
            AddHandler view.MenuClick, AddressOf view_MenuClick
        End If

        MyBase.OnViewSet(view)
    End Sub

    Private Sub view_MenuClick(ByVal sender As Object, ByVal e As MenuClickEventArgs)
        Select Case e.MenuKind
            Case MenuKind.計算機
                Dim calcView = GetView(Of CalcView)()
                calcView.StartPosition = FormStartPosition.CenterParent
                calcView.ShowDialog(Me.View)

            Case Else
        End Select
    End Sub
End Class

やっている事はMenuClickイベントにイベントハンドラを登録し、そのイベントハンドラGetViewメソッドを使ってCalcViewというビュー*3を取り出し表示しているだけ。

実行して「計算機」ボタンをクリックすると、こんな感じ

スクリーンキャプチャ

これで適当な値を入力して計算したりなんかして、計算画面を閉じて、もう一度「計算機」ボタンをクリックすると前の値は保持されるずに初期状態の計算画面が表示される。

GetViewメソッドはDIコンテナから新しいオブジェクトを取り出すだけなので、毎回違うオブジェクトだからこの動きは当然の結果。

でも、使い捨ての画面ならこれでもいいけど、画面の値をずっと保持していたい場合なんかがあった場合、別途画面のインスタンスをどこかに保持しておくか、毎回画面の値をセットし直すなどのなんらかの対処をする必要がある。

こういう事はあまりしたくないので、DIコンテナから毎回同じインスタンスを取得できる、つまりSingletonなビューを取り出せるメソッドを用意しておく。

PresenterBase.vb(一部省略)

Protected Function GetViewOfSingleton(Of T As Form)(ByVal viewName As String) As T
    Dim view = _container.Resolve(Of T)(viewName)
    Dim singletonView = TryCast(view, SingletonViewBase)
    ' オブジェクトのLifetimeをSingletonで管理するオブジェクト
    Dim lifetimeMgr = New ContainerControlledLifetimeManager()

    If singletonView IsNot Nothing Then
        singletonView.SetLifetime(lifetimeMgr)
    End If
    ' Singletonで登録する。
    ' 次回以降、コンテナからSingletonで取り出せるようになる。
    _container.RegisterInstance(viewName, view, lifetimeMgr)

    Return view
End Function

Protected Function GetViewOfSingleton(Of T As Form)(ByVal viewName As String) As T
    Dim view = _container.Resolve(Of T)(viewName)
    Dim lifetimeMgr = _context.GetLifetimeManager(view)

    If lifetimeMgr Is Nothing Then
        lifetimeMgr = New ContainerControlledLifetimeManager()
        ' Singletonで登録する。
        ' 次回以降、コンテナからSingletonで取り出せるようになる。
        _container.RegisterInstance(viewName, view, lifetimeMgr)

        Dim singletonView = TryCast(view, SingletonViewBase)
        ' SingletonViewBaseから継承していれば、Lifetimeの管理を任せる。
        If singletonView IsNot Nothing Then
            singletonView.SetLifetime(lifetimeMgr)
        End If
    End If
    Return view
End Function

GetViewOfSingletonメソッドで取得したビューはSingletonインスタンスになる。もちろんviewName引数で指定した名前単位でだけど。

ついでにこういうのも作っておいた。

SingletonViewBase.vb
Public Class SingletonViewBase
    Private _lifetimeMgr As ILifetimePolicy

    Friend Sub SetLifetime(ByVal lifetimeMgr As ILifetimePolicy)
        _lifetimeMgr = lifetimeMgr
    End Sub

    <System.Diagnostics.DebuggerNonUserCode()> _
    Protected Overrides Sub Dispose(ByVal disposing As Boolean)
        Try
            If disposing AndAlso components IsNot Nothing Then
                components.Dispose()
            End If
            ' singletonオブジェクトを破棄
            ' これやるとDisposeが呼ばれてStackOverflowを起こす
            'If _lifetimeMgr IsNot Nothing Then
                '_lifetimeMgr.RemoveValue()
                '_lifetimeMgr = Nothing
            'End If
        Finally
            MyBase.Dispose(disposing)
        End Try
    End Sub
End Class

GetViewOfSingletonメソッドで生成したビューがSingletonViewBaseを継承していた場合はILifetimePolicyインスタンスを設定しておき、SingletonViewBaseがDisposeされたタイミングでILifetimePolicyRemoveValueメソッドを呼び出す事で、DIコンテナ上からもインスタンスが削除されるようになっている。

これで次からは同じviewNameでも新しいインスタンスが生成されるようになる。

ビューがSingletonになる可能性ができたことでPresenterExtension側にも対応が必要になったけど、その部分は割愛しておく。

とまぁ、こんな感じでビューを生成して画面遷移していけばいいと思う。

*1:画面遷移するにはこの○○インターフェースを実装して、この○○メソッドを呼び出す必要があります〜なんてのは論外

*2:デフォルトではFriendアクセス

*3:以前のMainView

MVPパターンを業務アプリに適用する

業務アプリケーションを開発するにあたって、最低限守るべき事の一つとして「画面とロジック」の分離が挙げられると思う。
簡単そうに思えて、これが意外と難しい。
一人での開発ならば自分だけでやり方を決めてしまえばそんなに難しくはない。しかし、開発要員が増えれば、これが途端に難しくなる。
この原因としては、

  • どこからが「画面」の役割で、どこまでが「ロジック」の役割かの定義が個々人でばらつきがある。
  • 定義を決めても守らない奴がいる(そもそも意味が理解できていない)。
  • 言語自体がへぼくて「画面とロジック」の分離がそもそも(充分に)できない。

なんていうのがある。

ほとんど、「それって開発者としてどうなの?」というレベルに思えるけど、現実の開発ってそんなもんだよね。

いつの時代もこの問題は付きまとうようで、実際色んなデザインパターンが考えられてきた。

知ってるやつでは、

  • MVC(モデル・ビュー・コントローラ、Strutsとか)
  • MVVM(モデル・ビュー・ビューモデル、WPFSilverlight
  • MVP(モデル・ビュー・プレゼンター、後述)
  • ドキュメント・ビュー アーキテクチャMFCのあれ)

なんてのがある。

他にも知らないだけで、いっぱいあるんでしょうね。

まぁ、そんなこんなで今回VB.NETを使って業務アプリを作る事になったんですけど、これがなんか20〜30人で開発するらしいんですよね(実装だけで)。

で、そこで開発サブリーダー的な役割を与えられるわけですけど、アサインする開発者の人たちはほとんど(というか全く).NETとか知らないわけですよ。ありがちな話ですけど。

だもんで、そのまま開発させたら画面のイベントハンドラにすべての処理を書いたりするわけですよ。

それを防ぐために何かしらのアーキテクチャが必要になってくるんですが、それに今回はMVPパターンを採用しようかと考えてます。WinFormアプリケーションなので、それとの相性とかも考えて、これが一番ベターかなと。

MVPパターンを実装するフレームワークとしてPatterns & Practiceの「Smart Client Software Factory」とかいうのがあるんですけど、試しにインストールしてプロジェクトを作ってみたら、山程ソースを生成しやがったので、こりゃ駄目だと思って採用は見送りました。

なので、単純なMVPパターンを実装するフレームワークを作ることにしました。

参考にしたのはこの辺

MVPパターンの特徴は、プレゼンターがビューとモデルの関連付けを行い、ビューとモデルはお互いに完全に独立しているという事。

このフレームワークで目指すところは、

  1. ビューやプレゼンター、モデルの関連付けは自動で行う。
  2. ビューの完全な分離(モデルやプレゼンターを参照させない、できない)

これをやるためにDIコンテナなんかの自動で関連付けを行うものが必要なので、「Unity Application Block」を使う事にする。

実装

前置きが長くなってしまったけど、実装を進めていく。今回、簡単なサンプルとして計算機アプリを作る。

画面イメージはこんな感じ。

画面イメージ

テキストボックスに数字を入れて、「計算」ボタンで計算結果を表示するだけのシンプルなもの。

とりあえず、これを実装したコードを先に紹介する。フレームワーク部分をのぞいたソースのみ。

まずは、実際の計算を行うCalculatorクラス。これが「モデル」

Calculator.vb
Public Class Calculator

    Function Add(ByVal value1 As Long, ByVal value2 As Long) As Long
        Return value1 + value2
    End Function

End Class

画面クラス。見てのとおり何もない*1。これが「ビュー」

MainForm.vb
Public Class MainForm

End Class

そして、ビューとモデルを結びつける「プレゼンター」クラス。プレゼンターはPresenterBase(Of TView)(後述)から継承する必要がある。

MainFormPresenter.vb
Public Class MainFormPresenter
    Inherits PresenterBase(Of MainForm)

    Private _calculator As Calculator
    <Dependency()> _
    Public Property Calculator() As Calculator
        Get
            Return _calculator
        End Get
        Set(ByVal value As Calculator)
            _calculator = value
        End Set
    End Property

    Protected Overrides Sub OnViewSet(ByVal view As MainForm)
        MyBase.OnViewSet(view)

        If view IsNot Nothing Then
            AddHandler view.btnCalc.Click, AddressOf btnCalc_Click
        End If
    End Sub

    Private Sub btnCalc_Click(ByVal sender As Object, ByVal eventArgs As EventArgs)
        With View
            .txtSum.Text = Calculator.Add( _
                Integer.Parse(.txtLValue.Text), Integer.Parse(.txtRValue.Text) _
            )
        End With
    End Sub
End Class

プレゼンターにビューが設定されるとOnViewSetメソッドが呼ばれるようになっているので、これをOverridesしてビューが設定された時に「計算」ボタンのClickイベントハンドラを登録している。

後は、イベントハンドラで画面から値を取ってきて、Calculatorオブジェクトで計算して結果を画面に設定している。

このプログラムのエントリポイントは以下のようにする必要がある。

Program.vb
Module Program
    Sub Main()
        Using dicon As New UnityContainer()
            dicon.AddNewExtension(Of PresenterExtension)()

            Application.Run(dicon.Resolve(Of MainForm)())
        End Using
    End Sub
End Module

UnityContainerインスタンス化して、PresenterExtensionAddNewExtensionメソッドで登録する。

後はResolveメソッドで画面をコンテナから取り出して、アプリケーションを開始するだけ。

これで「計算」ボタンが押されれば、プレゼンターのイベントハンドラが呼び出されて、そこからモデルが呼び出され、その結果がプレゼンター経由でビューに渡る事になる。

ビューが他の部分に一切関連していないのがわかると思う。

本来ならビューにもプレゼンターと関連付ける為のプロパティが必要になるけど、それをしたくない*2ので、UnityContainerの拡張機能を使って細工をした。

フレームワーク部分の解説

プレゼンター自体は何のひねりもない。

PresenterBase.vb
Public MustInherit Class PresenterBase(Of TView As Form)

    Private _view As TView
    Protected ReadOnly Property View() As TView
        Get
            Return _view
        End Get
    End Property

    Public Sub SetView(ByVal view As TView)
        _view = view

        OnViewSet(view)
    End Sub

    Protected Overridable Sub OnViewSet(ByVal view As TView)
    End Sub

End Class

Viewプロパティ(Protected)とビューを関連付ける為のSetViewメソッドとそれを通知するOnViewSetメソッドがあるだけ。

細工はUnityContainerインスタンス化した後にAddNewExtensionで追加したPresenterExtensionクラスにある。

UnityContainerはオブジェクトの生成プロセスにユーザが干渉できるようにする為の機構をいくつか用意していて、その一つとして「ストラテジ」というのがある。

これをUnityContainerに設定する為の窓口としてUnityContainerExtensionというクラスが用意されているのだ。

以下ソース

PresenterExtension.vb
Public NotInheritable Class PresenterExtension
    Inherits UnityContainerExtension

    Protected Overrides Sub Initialize()
        Me.Context.Strategies.Add( _
            New PresenterInitializeStrategy(Me.Container), UnityBuildStage.Setup _
        )
    End Sub

    Public NotInheritable Class PresenterInitializeStrategy
        Inherits BuilderStrategy

        Private _container As IUnityContainer
        Private _presenterDefinitions As Dictionary(Of Type, Type)
        ''' <summary>
        ''' システム定義のアセンブリ名と思われる正規表現パターン
        ''' </summary>
        ''' <remarks></remarks>
        Private ReadOnly SysAssemblyPtrn As Regex = New Regex("^System.*|^Microsoft.*|mscorlib|vshost")

        Public Sub New(ByVal container As IUnityContainer)
            _container = container

            Initialize()
        End Sub

        Private Sub Initialize()
            ' 現在読み込まれているアセンブリからPresenterBaseを継承した型を検索する。
            Dim types = AppDomain.CurrentDomain.GetAssemblies() _
                .Where(Function(a) Not SysAssemblyPtrn.IsMatch(a.GetName().Name)) _
                .SelectMany(Function(a) a.GetTypes()) _
                .Where(Function(t) t.BaseType IsNot Nothing AndAlso t.BaseType.Name Like "PresenterBase`1")

            ' Presenterの型引数とPresenterの型でマップを作る。
            _presenterDefinitions = types.ToDictionary( _
                Function(tp) tp.BaseType.GetGenericArguments().First() _
            )
        End Sub

        Public Overrides Sub PostBuildUp(ByVal context As IBuilderContext)
            Dim view = context.Existing
            Dim viewType = view.GetType()

            If _presenterDefinitions.ContainsKey(viewType) Then
                Dim presenter = _container.Resolve(_presenterDefinitions(viewType))
                presenter.SetView(view)
                ' コンテナに登録
                _container.RegisterInstance(presenter)
            End If

            MyBase.PostBuildUp(context)
        End Sub
    End Class
End Class

何をしているかというと、

  1. ExtensionInitializeメソッドでPresenterInitializeStrategyをストラテジに追加する。
  2. PresenterInitializeStrategyの初期化時に現在読み込まれているアセンブリからPresenterBaseを継承している型を探して、その型パラメータと継承している型のペアでディクショナリを生成する。
  3. PostBuildUpメソッドでDIコンテナがオブジェクトを生成した後をフックして、そのオブジェクトの型が先程のディクショナリに存在すれば、対応するプレゼンターをDIコンテナから取り出す。
  4. プレゼンターにビューを設定する。

ということをやっている。

PostBuildUpメソッドの遅延バインディングでメソッド呼び出しをしている所は今のC#にはできない事なので、ちょっとだけVB.NETを見直した。

これだけでは現実の開発には適用できないので、まだ機能を作りこむ必要があるけど、基本的な考え方はまとまってきた。ビューがこれだけ分離してれば、画面とロジックを平行して作っていくのも楽になるんじゃないかな。

今考えている問題点としては、

  • ビューに実装する機能はどこまでか?
    • 画面の入力値チェック?
    • 入力値チェックをするために必要な情報(DBとか)は、いつどうやって取得するか?
  • 「計算」ボタンの前と後に画面側の処理を入れるにはどうするか?
  • プレゼンターが画面のコントロールを直接参照しているので、画面側の変更をもろに受けてしまう。
    • インターフェースを使って分離すべきか?
    • インターフェースを使った分離はメンテナンスが面倒なので、あまりやりたくない。

と挙げればいくらでも出てくるけど、その辺もこれから考えていきたい。

*1:もちろんパーシャルクラスが別にいるけど、それは割愛

*2:というよりさせたくない、ビューからプレゼンターを参照させたくない

フォームのエクスポート/インポート

AccessのVSSがよくぶっこわれるので、フォームのソースのエクスポート/インポート方法のメモ

' エクスポート
Call SaveAsText(acForm, "フォーム名", "パス")
' インポート
Call LoadFromText(acForm, "フォーム名", "パス")