OOコード養成ギブス - rants
最近、パブリックプロパティ (もしくはパブリックフィールド) の善悪について悩んでます。
例えば、ドメインオブジェクトを UI に表示する処理って、ドメインオブジェクトに持たせるわけにはいかないですよね?
かといって、DI で UI 表示処理を注入するってのも現実的じゃないと思います。
で、ドメインオブジェクトにパブリックプロパティを用意するとこうなります。
public sealed class Book
{
public Book(string id, string title)
{
this._id = id;
this._title = title;
}
private readonly string _id;
private readonly _title;
public string Id { get { return this._id; } }
public string Title { get { return this._title; } }
}
public sealed class BookDetail : Page
{
protected void Page_PreRender(object sender, EventArgs e)
{
this._idTextBox.Text = this._book.Id; // Book インスタンスの持ってき方はてきとー
this._titleTextBox.Text = this._book.Title;
}
}
でもこれだと、後から Book に出版社名を追加した時、BookDetail の修正漏れが発生する可能性があります。
そこで、こんな風にしてみます。
public sealed class Book
{
public Book(string id, string title)
{
this._id = id;
this._title = title;
}
private readonly string _id;
private readonly string _title;
public void Present(Receiver receiver)
{
receiver(this._id, this._title);
}
public delegate void Receiver(string id, string title);
}
public sealed class BookDetail : Page
{
protected void Page_PreRender(object sender, EventArgs e)
{
Book.Receiver receiver = delegate(string id, string title)
{
this._idTextBox.Text = id;
this._titleTextBox.Text = title;
};
this._book.Present(receiver); // Book インスタンスの持ってき方はてきとー
}
}
後から Book に出版社名を追加したら、Receiver デリゲートのシグネチャが変わるため、BookDetail を修正せずにコンパイルを通すことはできません。
でもホントにこっちの方がいいんだろーか。
タイトルだけを表示に使用する画面でも、(タイトルの getter はないので) Present メソッドを使用してタイトルを受け取る必要があります。
それでも良いような気もするし悪いような気もします。
あ、Identity (Book なら ID) に関しては、なんとゆーか独立した存在価値 (?) みたいなものを持っているので、どちらにせよパブリックプロパティを用意 (もしくはパブリックフィールド化) して良いと思います。
// 追記
Java で似たようなコード書いてみました。
public final class Book {
public Book(String id, String title) {
this.id = id;
this.title = title;
}
private final String id;
private final String title;
public void present(Receiver receiver) {
receiver.receive(this.id, this.title);
}
public interface Receiver {
void receive(String id, String title);
}
public static void main(String[] args) {
Book book = new Book("1234", "aiueo");
Book.Receiver receiver = new Book.Receiver() {
public void receive(String id, String title) {
System.out.println(id);
System.out.println(title);
}
};
book.present(receiver);
}
}
// 2009/02/02
Visitor より Receiver の方がいい感じなので変更。
直交座標系とか極座標系とかはよくわかってないけど、とりあえず C# でもできます。
まぁ、2つのインスタンスのメモリ上のサイズが等しくない場合はマズいことになりかねませんけどね…。
Cartesian と Polar はサイズが等しいので大丈夫です。
// 追記1 (2009/01/28)
対象オブジェクトのフィールドを辿るとマネージヒープ上のオブジェクトへの参照が含まれている、という場合も、場合によってはマズいことになります。
// 追記2 (2009/01/28)
NyaRuRu さんより、「何が起きても不思議ではない」とご指摘頂きました。
今回のコードは動作しましたが、インスタンスサイズを揃えて、フィールドにマネージヒープ上のオブジェクトへの参照を含めないようにしたとしても、確実に大丈夫だと断言することはできません。また、今回のコードが如何なる時でも確実に動作すると断言することもできません。
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
class Cartesian
{
public Cartesian(double x, double y)
{
this.X = x;
this.Y = y;
}
public double X { get; set; }
public double Y { get; set; }
public Polar ToPolar()
{
return new Polar(Math.Sqrt(X * X + Y * Y), Math.Atan2(Y, X));
}
}
class Polar
{
public Polar(double r, double theta)
{
this.R = r;
this.Theta = theta;
}
public double R { get; set; }
public double Theta { get; set; }
public Cartesian ToCartesian()
{
return new Cartesian(R * Math.Cos(Theta), R * Math.Sin(Theta));
}
}
class Program
{
static void Transmogrify(object a, object b)
{
Type aType = a.GetType();
Type bType = b.GetType();
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
FieldInfo[] aFieldInfos = aType.GetFields(flags);
FieldInfo[] bFieldInfos = bType.GetFields(flags);
var aFields = aFieldInfos.Select(item => new { Key = item, Value = item.GetValue(a) }).ToList();
var bFields = bFieldInfos.Select(item => new { Key = item, Value = item.GetValue(b) }).ToList();
ConvertType(a, bType);
ConvertType(b, aType);
aFields.ForEach(item => item.Key.SetValue(b, item.Value));
bFields.ForEach(item => item.Key.SetValue(a, item.Value));
}
static unsafe void ConvertType(object target, Type newType)
{
IntPtr newTypeHandle = newType.TypeHandle.Value;
GCHandle targetGCHandle = GCHandle.Alloc(target, GCHandleType.Normal);
try
{
void* entryPointer = (void*)GCHandle.ToIntPtr(targetGCHandle);
void* targetPointer = *((void**)entryPointer);
IntPtr* typeHandlePointer = (IntPtr*)targetPointer;
*typeHandlePointer = newTypeHandle;
}
finally
{
targetGCHandle.Free();
}
}
static void Main(string[] args)
{
Polar pos1 = new Polar(Math.Sqrt(2), Math.PI / 4);
Polar pos2 = pos1;
Console.WriteLine(pos1.GetType().Name); //=> Polar
Transmogrify(pos1, pos1.ToCartesian());
Console.WriteLine(pos1.GetType().Name); //=> Cartesian
Cartesian pos1AsCart = pos1 as object as Cartesian;
Console.WriteLine(pos1AsCart.X); //=> 1
Console.WriteLine(pos1AsCart.Y); //=> 1
Console.WriteLine(pos2.GetType().Name); //=> Cartesian
Transmogrify(pos1, pos1AsCart.ToPolar());
Console.WriteLine(pos1.GetType().Name); //=> Polar
Console.WriteLine(pos1.R); //=> 1.4142135623731
Console.WriteLine(pos1.Theta / Math.PI); //=> 0.25
Console.ReadLine();
}
}
[関連]
C#と諸々 オブジェクトの型を破壊的に変換
C#と諸々 ガベージコレクションを開始するには
個人向けデスクトップアプリで DB 使いたい時なんかに便利かもかも
方法 : SQL Server Compact 3.5 データベースをアプリケーションと共に配置する
極小SQL Server Compactでデータベース・アプリをお手軽作成 - @IT
再配布権の登録というのが必要らしいのでそこは気を付けよう (@IT の記事の3ページ目のコラム参照)
interfaceに物申す
interfaceに物申す(2)
売値やら原価なんてのは青果店や問屋が勝手にりんごに貼った値札のようなもの。「原価は客には見えるべきでない」というリアリティを追及するなら、そもそも売値やら原価をりんごに持たせるのが間違いな気がする。
この場合、原価を (りんごと関連付けて) 青果店が隠し持っていればいいだけの話。
少なくとも「原価を見せたくない」ということの表現は、アクセス修飾子ではなく、よりリアルな設計で表現できる。たぶん。
- TracLightning サービスが開始されている場合は停止する。
- "C:\TracLight\projects" フォルダを "D:\TracLight\projects" に移動する。
- "C:\TracLight\CollabNetSVN\httpd\conf\httpd.conf" ファイル内にある全ての "C:\TracLight\projects" を "D:\TracLight\projects" に置換する。
- "C:\TracLight\python\share\trac\conf\trac.ini" 内にある全ての "C:\TracLight\projects" を "D:\TracLight\projects" に置換する。
- 各プロジェクトフォルダ ("D:\TracLight\projects\trac" フォルダ内) の "conf\trac.ini" ファイル内にある全ての "C:\TracLight\projects" を "D:\TracLight\projects" に置換する。
- 各プロジェクトフォルダに対して Trac Lightning のコマンドプロンプトから resync を実行する。 (trac-admin.bat D:\TracLight\projects\trac\xxxxx resync)
- 環境変数に "TL_PROJECT_HOME" を作成し、"D:\TracLight\projects" を設定する。
- TracLightning サービスがインストールされている場合は、[スタート] - [すべてのプログラム] - [サービスのアンインストール] を実行する。
- [スタート] - [すべてのプログラム] - [サービスのインストール] を実行する。
ちょっと面倒だが、これらの手順を一度踏んでしまえば、以降は特に何も意識する必要がなくなる。
実践!ソフトウェアアーキテクチャ VisualStudioとASP.NETによる業務システム開発方法
この本でユニットテスト時に SMO を使ってテスト用のデータベースを構築しているのを見て、僕も取り入れてみたので自分のためにメモ。
# 本に載ってる方法とは少し違う方法。
ユニットテストだけでなく、インストーラのカスタム動作でも使えるので便利。
まず、データベース管理用のクラスライブラリプロジェクトを作成。ここでは Hoge.Databases と名付けることにする。
次に、全てのテーブルを (再) 作成するためのクエリーファイルをプロジェクト内に作成。ビルドアクションは "埋め込まれたリソース" に設定。ここでは Create.sql と名付けることにする。
SET ANSI_NULLS ON
GO
-- テーブルを削除。子テーブルを先に削除すること。
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Table2]') AND type in (N'U'))
DROP TABLE [dbo].[Table2]
GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[Table1]') AND type in (N'U'))
DROP TABLE [dbo].[Table1]
GO
-- テーブルを作成。親テーブルを先に作成すること。
CREATE TABLE [dbo].[Table1]
(
[PrimaryKey] [bigint] IDENTITY(1,1) NOT NULL,
[Id] [nvarchar](255) NOT NULL,
[Name] [nvarchar](255) NOT NULL,
CONSTRAINT [PK_Table1] PRIMARY KEY CLUSTERED ([PrimaryKey]),
CONSTRAINT [IX_Table1_Id] UNIQUE NONCLUSTERED ([Id])
) ON [PRIMARY]
GO
CREATE TABLE [dbo].[Table2]
(
[PrimaryKey] [bigint] IDENTITY(1,1) NOT NULL,
[Table1PrimaryKey] [bigint] NOT NULL,
[Value] [nvarchar](255) NOT NULL,
CONSTRAINT [PK_Table2] PRIMARY KEY CLUSTERED ([PrimaryKey]),
CONSTRAINT [FK_Table2_Table1] FOREIGN KEY([Table1PrimaryKey])
REFERENCES [dbo].[Table1] ([PrimaryKey])
ON UPDATE CASCADE
ON DELETE CASCADE
) ON [PRIMARY]
GO
次に、SMO を使ってデータベースを管理するクラスを作成。
using System;
using System.IO;
using System.Reflection;
using Microsoft.SqlServer.Management.Smo;
namespace Hoge.Databases
{
/// <summary>
/// Hoge データベースを管理します。
/// </summary>
public sealed class HogeDatabase
{
#region Static Members
/// <summary>
/// Hoge データベースに含めるテーブルを生成するための SQL クエリーファイルのリソースパスを取得します。
/// </summary>
private const string CreateScriptResourcePath = "Hoge.Databases.Create.SQL";
/// <summary>
/// 指定したリソースパスに含まれるリソースを文字列として読み出します。
/// </summary>
/// <param name="resourcePath">リソースパス。</param>
/// <returns></returns>
static private string ReadResourceText(string resourcePath)
{
Assembly thisAssembly = Assembly.GetExecutingAssembly();
using (Stream resourceStream = thisAssembly.GetManifestResourceStream(resourcePath))
using (StreamReader resourceReader = new StreamReader(resourceStream))
{
return resourceReader.ReadToEnd();
}
}
#endregion
#region Constructors
/// <summary>
/// HogeDatabase クラスの新しいインスタンスを初期化します。
/// </summary>
/// <param name="server">SQL Server のインスタンス名。</param>
/// <param name="database">データベース名。</param>
public HogeDatabase(string instanceName, string databaseName)
{
this._server = new Server(instanceName);
this._database = new Database(this._server, databaseName);
}
#endregion
#region Fields
/// <summary>
/// SQL Server のインスタンスの管理オブジェクトを取得します。
/// </summary>
private readonly Server _server;
/// <summary>
/// データベースの管理オブジェクトを取得します。
/// </summary>
private readonly Database _database;
#endregion
#region Properties
/// <summary>
/// Hoge データベースが存在するかどうかを示す値を取得します。
/// </summary>
/// <returns>Hoge データベースが存在するかどうかを示す値。</returns>
public bool Exists
{
get
{
this._database.Refresh();
return (this._database.State == SqlSmoState.Existing);
}
}
#endregion
#region Methods
/// <summary>
/// Hoge データベース、各テーブルを作成し、ASPNET アカウントをログイン登録します。
/// </summary>
/// <exception cref="InvalidOperationException">データベースは既に存在します。</exception>
public void Create()
{
this.CreateDatabase();
this.CreateTables();
this.CreateLoginForAspnet();
}
/// <summary>
/// Hoge データベースを作成します。
/// </summary>
/// <exception cref="InvalidOperationException">データベースは既に存在します。</exception>
public void CreateDatabase()
{
if (this.Exists)
{
throw new InvalidOperationException("データベースは既に存在します。");
}
this._database.Refresh();
this._database.Create();
}
/// <summary>
/// Hoge データベースの各テーブルを作成します。
/// テーブルが既に存在する場合は、テーブルを削除してから作成を行います。
/// </summary>
/// <exception cref="InvalidOperationException">データベースは存在しません。</exception>
public void CreateTables()
{
if (!this.Exists)
{
throw new InvalidOperationException("データベースは存在しません。");
}
string createScript = ReadResourceText(CreateScriptResourcePath);
this._database.ExecuteNonQuery(createScript);
}
/// <summary>
/// 結果を返さない SQL コマンドを実行します。
/// </summary>
/// <param name="sqlCommand">SQL コマンド。</param>
/// <exception cref="InvalidOperationException">データベースは存在しません。</exception>
public void ExecuteNonQuery(string sqlCommand)
{
if (!this.Exists)
{
throw new InvalidOperationException("データベースは存在しません。");
}
this._database.ExecuteNonQuery(sqlCommand);
}
/// <summary>
/// Hoge データベースを削除します。
/// </summary>
/// <exception cref="InvalidOperationException">データベースは存在しません。</exception>
public void Drop()
{
if (!this.Exists)
{
throw new InvalidOperationException("データベースは存在しません。");
}
this._server.Refresh();
this._server.KillDatabase(this._database.Name);
System.Data.SqlClient.SqlConnection.ClearAllPools();
}
/// <summary>
/// IIS 5.1 上に配置された ASP.NET アプリケーションから DB にアクセス (R/W) できるよう、
/// ASPNET アカウントをログイン登録します。
/// </summary>
public void CreateLoginForAspnet()
{
if (!this.Exists)
{
throw new InvalidOperationException("データベースは存在しません。");
}
string name = string.Format(@"{0}\{1}", Environment.MachineName, "aspnet");
this._server.Refresh();
if (!this._server.Logins.Contains(name))
{
Login login = new Login(this._server, name);
login.LoginType = LoginType.WindowsUser;
login.Create();
}
this._database.Refresh();
User user = new User(this._database, name);
if (!this._database.Users.Contains(name))
{
user.Login = name;
user.Create();
}
if (!user.IsMember("db_datareader"))
{
user.AddToRole("db_datareader");
}
if (!user.IsMember("db_datawriter"))
{
user.AddToRole("db_datawriter");
}
}
#endregion
}
}
ユニットテストクラスでの使用例
private const string InstanceName = @"localhost\sqlexpress";
private const string DatabaseName = "HogeDatabase_Test";
private HogeDatabase _testDatabase;
[TestFixtureSetUp]
public void CreateDatabase()
{
this._testDatabase = new HogeDatabase(InstanceName, DatabaseName);
if (this._testDatabase.Exists)
{
this._testDatabase.Drop();
}
this._testDatabase.Create();
}
[SetUp]
public void CreateTables()
{
this._testDatabase.CreateTables();
}
[TestFixtureTearDown]
public void DropDatabase()
{
if (this._testDatabase.Exists)
{
this._testDatabase.Drop();
}
}
private void InsertTable1(string id, string name)
{
const string insertQueryFormat = "INSERT INTO [dbo].[Table1] ([Id], [Name]) VALUES ('{0}', '{1}')";
string insertQuery = string.Format(insertQueryFormat, id.Replace("'", "''"), name.Replace("'", "''"));
this._testDatabase.ExecuteNonQuery(insertQuery);
}
[Test]
public void テスト1()
{
for (int i = 0; i < 3; i++)
{
InsertTable1(i.ToString(), string.Format("Test {0:000}", i));
}
// テストコード
}
// 2009/01/14
ユニットテストの SetUp と TearDown で DB の作成・削除を実行すると、2 度目以降の DB 作成に失敗してしまうため、DB の作成・削除は TestFixtureSetUp と TestTearDown で行い、テーブルの再作成だけ SetUp で行うように、HogeDatabase クラスとユニットテストを変更。
HogeDatabase クラスの実装方法も全体的に変更。
ついでに ASPNET アカウントの R/W 権限を登録するメソッドも追加。(IIS 5.1 のみ正常に機能する)
// 2009/02/02
テストケースが複数あって DB の作成・削除が2度行われる場合、SqlConnection のプールが残ってしまう関係でエラーが発生する。そのため、DB の削除時に ClearAllPools メソッドを呼び出すよう修正。
参考 : DBバックアップ→DBリストア→テーブル内容参照時にException - Insider.NET