ガード句っぽい選択
あー本題からそれてるような気もしますが。
あと型名勝手に変えちゃいましたが。
class Generation
{
public static readonly Generation Over60 = new Generation(60, null);
public static readonly Generation Over50 = new Generation(50, 59);
public static readonly Generation Over40 = new Generation(40, 49);
public static readonly Generation Over30 = new Generation(30, 39);
public static readonly Generation Over20 = new Generation(20, 29);
public static readonly Generation Under20 = new Generation(null, 19);
public static Generation GetGeneration(int age)
{
return
new[]
{
Over60,
Over50,
Over40,
Over30,
Over20,
Under20,
}
.Single(item => item.IsIncludeing(age));
}
private Generation(int? lowerLimit, int? upperLimit)
{
LowerLimit = lowerLimit;
UpperLimit = upperLimit;
}
public readonly int? LowerLimit;
public readonly int? UpperLimit;
public bool IsIncludeing(int age)
{
bool isWithinLowerLimit = LowerLimit.HasValue ? (LowerLimit <= age) : true;
bool isWithinUpperLimit = UpperLimit.HasValue ? (age <= UpperLimit) : true;
return isWithinLowerLimit && isWithinUpperLimit;
}
}
元ネタにあるような特定の使い方だけしかしない内は、
こんなクラス化なんてやりすぎですが、
色々な使い方が出てきたときにはこういう方向に進んでいくことかと思います。
ついでに、Static フィールド の種類を増やす時に変更箇所が一箇所で済むよう
リフレクションとカスタム属性を導入してみます。
class Generation
{
class ItemAttribute : Attribute { }
[Item]public static readonly Generation Over60 = new Generation(60, null);
[Item]public static readonly Generation Over50 = new Generation(50, 59);
[Item]public static readonly Generation Over40 = new Generation(40, 49);
[Item]public static readonly Generation Over30 = new Generation(30, 39);
[Item]public static readonly Generation Over20 = new Generation(20, 29);
[Item]public static readonly Generation Under20 = new Generation(null, 19);
private static ReadOnlyCollection<Generation> _items = CreateItems();
private static ReadOnlyCollection<Generation> CreateItems()
{
var query =
from field in typeof(Generation).GetFields(BindingFlags.Static | BindingFlags.Public)
where (field.IsDefined(typeof(ItemAttribute), false))
select (field.GetValue(null) as Generation);
return new ReadOnlyCollection<Generation>(query.ToList());
}
public static Generation GetGeneration(int age)
{
return _items.Single(item => item.IsIncludeing(age));
}
private Generation(int? lowerLimit, int? upperLimit)
{
LowerLimit = lowerLimit;
UpperLimit = upperLimit;
}
public readonly int? LowerLimit;
public readonly int? UpperLimit;
public bool IsIncludeing(int age)
{
bool isWithinLowerLimit = LowerLimit.HasValue ? (LowerLimit <= age) : true;
bool isWithinUpperLimit = UpperLimit.HasValue ? (age <= UpperLimit) : true;
return isWithinLowerLimit && isWithinUpperLimit;
}
}
若干やりすぎた感があります。
ドメインレイヤで集約毎に名前空間を切る時、今まではエンティティ (集約ルート) の名前を複数形にしてたんだけど、これも不適切だなぁ。例えば Book というエンティティは Hoge.Fuga.Domains.Books という名前空間に。
# Domains も Domain の方が適切だ ^^;
別に Book が数種類あるわけでもないのに Books というのはおかしな話。でも、かといって単数形で Book にしちゃうとクラス名とかぶってしまうわけで…。
この名前空間は Book の集約を含めるための名前空間だから BookAggregate が妥当なのかなー。それとも、そこまで気にしないで Books にしちゃうか…。
C# だと、明示的に virtual をくっつけないと仮想メソッドにはなりません。(Java だとデフォルトが仮想メソッドだった気がします。)
で、個人的に仮想メソッドはほいほい用意すべきものではないと思っています。決して、仮想メソッドが全く必要無いということではないですが。
まず、実装を完全に上書きすることができるってのがどうにも気持ち悪いです。
これが実際に行われる時は、主に継承ツリーが不適切である場合が多いです。
この場合、例えば仮想メソッドを抽象メソッドにして派生クラスの一つに実装を移すことで回避できたりします。
あと、base キーワード使って、基本クラスが実装したバージョンのメソッドを好きな位置で呼び出すことができますが (でも正しく動作させるにはちゃんと位置を考えないといけないことがよくある)、これも気持ち悪いです。
これが実際に行われる時は、クラスの内部設計が未熟である場合が多いです。
この場合、例えばテンプレートメソッドパターン等の検討を行い、派生クラスで実装する処理を的確に表現するメソッドを用意することで回避できたりします。
ただし、仮想メソッドが適切である場面もあります。
例えば、テンプレートメソッドパターン等によって用意されたメソッドだけれど、派生クラスで必ずしも実装する必要がないという場合は、抽象メソッドではなく実装が空の仮想メソッドとした方が良い場合もあります。
また、継承していくにつれ段階的に実装が膨らんでいく (あくまでも同じ目的の処理が増えていく) 可能性があるメソッド (例 : Dispose メソッド) なんかは仮想メソッドの方が適切です。
ということで、一概に仮想メソッドはダメとは言えないのですが、仮想メソッドを用意する時は他にもっと適切な方法がないかを考慮してみると良いかなと思います。
10 ~ 20 行となると、ちょっと長い。
20 ~ 30 行となると、長い。
30 ~ 40 行となると、かなり長い。
40 行以上はやばい。(※)
※ ごく稀に仕方のないものもある。例えば switch 文による何かしらの変換処理とか。
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 の方がいい感じなので変更。
interfaceに物申す
interfaceに物申す(2)
売値やら原価なんてのは青果店や問屋が勝手にりんごに貼った値札のようなもの。「原価は客には見えるべきでない」というリアリティを追及するなら、そもそも売値やら原価をりんごに持たせるのが間違いな気がする。
この場合、原価を (りんごと関連付けて) 青果店が隠し持っていればいいだけの話。
少なくとも「原価を見せたくない」ということの表現は、アクセス修飾子ではなく、よりリアルな設計で表現できる。たぶん。
方法は簡単で、HogeOperation<TResult> クラスが行っていた戻り値をフィールドに保持する処理を、各処理クラスが直接行えば良いだけ。
GetFugasOperation クラス
internal sealed class GetFugasOperation : HogeOperation
{
public GetFugasOperation(HogeService service)
: base(service)
{
}
private IList<Fuga> _result;
public IList<Fuga> Execute()
{
this.Processing();
return this._result;
}
protected override void MainProcessing()
{
this._result = this._service.GetFugas();
}
}
GetFugaOperation クラス
internal sealed class GetFugaOperation : HogeOperation
{
public GetFugaOperation(HogeService service)
: base(service)
{
}
private int _id;
private Fuga _result;
public Fuga Execute(int id)
{
this._id = id;
this.Processing();
return this._result;
}
protected override void MainProcessing()
{
var findFugaById =
from fuga in this._service.GetFugas()
where (fuga.Id == this._id)
select fuga;
this._result = findFugaById.Single();
}
}
どうせ引数は各処理クラスでフィールド化しなきゃいけないのだから、戻り値も各処理クラスでフィールド化した方が自然な気がする。
とても似ているメソッドの共通化
ロギングのような別の関心事に関する共通処理ならばアスペクト指向という手もある。
が、ここではそういうケースではなく、あくまでも同一の関心事であるケースについて考えてみようと思う。
同一の関心事であるケースとは、コアロジックに関係する事前処理・事後処理のことである。
基本的には、リンク先に書かれている方法で充分だ。しかし、各処理が複雑な場合、一つのクラスに大量のプライベートメソッドが出てくる。
これは共通の事前処理・事後処理があるかどうかは無関係な話だが、そうすると各処理をクラス化した方がよくなるわけだ。
各処理クラスは、共通の基本クラスから派生する。
そして事前処理・事後処理は基本クラスで実装し、テンプレートメソッドパターンを適用することで、派生クラスにはコアロジックの実装に専念させる。
引数、戻り値、例外の扱い方に色々な方法があるが、僕がお勧めする方法を例を交えて 1 つだけ紹介しようと思う。
まず、次のような 2 つのクラスがあるとする。(メソッドの中身は省略)
HogeService クラス
public class HogeService
{
public void Open();
public void Close();
public void AddFuga(Fuga target);
public IList<Fuga> GetFugas();
}
Fuga クラス
public class Fuga
{
public int Id { get; }
}
HogeService クラスの AddFuga(Fuga target) や GetFugas() を使用する場合、次の制約がある。
- 事前処理として、Open を呼び出さなければならない
- 事後処理として、Close を呼び出さなければならない
Hoge クラスでは、Open・Close を隠蔽し、また ID を元に単一の Fuga を取得するメソッドを新たに提供することにした。
Hoge クラス
public class Hoge
{
public void AddFuga(Fuga target)
{
HogeService service = new HogeService();
service.Open();
try
{
service.AddFuga(target);
}
finally
{
service.Close();
}
}
public IList<Fuga> GetFugas()
{
HogeService service = new HogeService();
service.Open();
try
{
return service.GetFugas();
}
finally
{
service.Close();
}
}
public Fuga GetFuga(int id)
{
HogeService service = new HogeService();
service.Open();
try
{
var findFugaById =
from fuga in service.GetFugas()
where (fuga.Id == id)
select fuga;
return findFugaById.Single();
}
finally
{
service.Close();
}
}
}
見ての通り、各処理は try ブロック内が異なるだけで他は全て同一の処理を行っている。
ではここで、各処理をクラス化してみよう。
まず、基本クラスである HogeOperation クラス。
HogeOperation クラス
internal abstract class HogeOperation
{
public HogeOperation(HogeService service)
{
this._service = service;
}
protected readonly HogeService _service;
protected void Processing()
{
this._service.Open();
try
{
this.MainProcessing();
}
finally
{
this._service.Close();
}
}
protected abstract void MainProcessing();
}
戻り値を持たない処理は、この HogeOperation を直接継承する。
また、外部から処理を実行できるよう、適切なパラメータを持つ Execute メソッドを用意する。受け取った引数は、MainProcessing メソッドで使用できるよう、フィールドに保持しておく。
AddFugaOperation クラス
internal sealed class AddFugaOperation : HogeOperation
{
public AddFugaOperation(HogeService service)
: base(service)
{
}
private Fuga _target;
public void Execute(Fuga target)
{
this._target = target;
return this.Processing();
}
protected override void MainProcessing()
{
this._service.AddFuga(this._target);
}
}
続いて、戻り値を持つ処理の基本クラスとなる、HogeOperation<TResult> クラス。
このクラスは HogeOperation クラスを継承するのだが、new 修飾子を使用して、Processing メソッドが戻り値を返すように再定義を行っている。MainProcessing メソッドも戻り値を返せるようにする必要があるが、抽象メソッドなので new 修飾子による再定義はできない。代わりに、out パラメータとして戻り値を返せるバージョンの MainProcessing メソッドを新たに定義している。
HogeOperation<TResult> クラス
internal abstract class HogeOperation<TResult> : HogeOperation
{
public HogeOperation(HogeService service)
: base(service)
{
}
private TResult _result;
protected new TResult Processing()
{
base.Processing();
return this._result;
}
protected override void MainProcessing()
{
this.MainProcessing(out this._result);
}
protected abstract void MainProcessing(out TResult result);
}
戻り値を持つ処理は、この HogeOperation<TResult> クラスを継承する。
こちらも、外部から処理を実行できるよう、適切なパラメータを持つ Execute メソッドを用意する。
GetFugasOperation クラス
internal sealed class GetFugasOperation : HogeOperation<IList<Fuga>>
{
public GetFugasOperation(HogeService service)
: base(service)
{
}
public IList<Fuga> Execute()
{
return this.Processing();
}
protected override void MainProcessing(out IList<Fuga> result)
{
result = this._service.GetFugas();
}
}
GetFugaOperation クラス
internal sealed class GetFugaOperation : HogeOperation<Fuga>
{
public GetFugaOperation(HogeService service)
: base(service)
{
}
private int _id;
public Fuga Execute(int id)
{
this._id = id;
return this.Processing();
}
protected override void MainProcessing(out Fuga result)
{
var findFugaById =
from fuga in this._service.GetFugas()
where (fuga.Id == this._id)
select fuga;
result = findFugaById.Single();
}
}
これらの処理クラスを使用することで、Hoge クラスは次のようにシンプルなコードとなる。
Hoge クラス
public class Hoge
{
public void AddFuga(Fuga target)
{
HogeService service = new HogeService();
new AddFugaOperaion(service).Execute(target);
}
public IList<Fuga> GetFugas()
{
HogeService service = new HogeService();
return new GetFugasOperaion(service).Execute();
}
public Fuga GetFuga(int id)
{
HogeService service = new HogeService();
return new GetFugaOperaion(service).Execute(id);
}
}
以上で処理のクラス化は完了となる。
// 追記 (2008/09/24)
HogeOperation<TResult> クラスは、ややトリッキーだったかもしれない。別解を記事にしたのでこちらも参照して頂きたい。
戻り値を持つ処理のクラス化の別解
// 追記ここまで
さて、最初にも書いたが、各処理が複雑でない内は、デリゲートの注入で充分だろう。この例では各処理が複雑なものではないため、各処理をクラス化したことによって逆に複雑になってしまっている。だが、処理が複雑化してきたときには、処理をクラス化することは非常に有効である。これは単一責任の原則を適用するということだ。例えば Hoge クラスに何らかの状態を持たせたりする場合、状態操作の処理が Hoge クラス内で行われる。各処理で行われる複雑な処理は、処理クラスに切り離されているため、Hoge クラスが責任を持つべき処理だけが Hoge クラス内に残るわけである。
ところで、HogeOperation クラス、HogeOperation<TResult> クラスには、protected な Processing メソッドを用意して、外部から処理を実行させるための Execute メソッドは実際の処理クラスに用意しているが、なぜわざわざこのようなことをしているのか。
例えば、引数はコンストラクタなり引数設定用のメソッドなりで設定できるようにしてしまえば、Processing メソッドの代わりに public な Execute メソッドを基本クラスに用意できるだろう。
しかし、そうすると各処理がスローする可能性のある例外を明示する時、つまり、XML コメントの再定義で困る。結局 XML コメントを再定義するためには Execute メソッドをオーバーライドするなり new 修飾子で再定義するなりしなければならない。ならば、初めから Execute メソッドは処理クラスで定義しなければならないようにした方が良いと考えたわけだ。
無論、XML コメントの再定義を行わないつもりならば、引数はコンストラクタなり引数設定用のメソッドなりで設定できるようにして、基本クラスで Execute メソッドを定義してしまっても良い。
引数設定用のメソッドを用意するのなら、フルエントインターフェイスを意識すると良い。
当初はこういった別の実装方法も取り上げるつもりだったのだが、収拾がつかなくなってしまったため省略した。
NAgilerの日記 - Agile Principles, Patterns, and Practices in C# (^o^)
なんと、書籍「アジャイルソフトウェア開発の奥義」の C# 版だそうです
ほ、ほしぃ・・・。でも洋書読めない ;-;
邦訳版が出てくれることを祈ります ( できれば、アジャイルソフトウェア開発の奥義と同じ人の翻訳で ^^ )
# よく見たらけっこう前 ( 2006年8月 ) に出てたんですね、知らなかった ^^;
例外について見てみる。基本クラスでスローするとされていない例外は、LSP により、派生クラスでもスローしてはいけないことになる。
ここで言う "基本クラスでスローするとされていない例外" とは "基本クラスでスローしない例外" ではない。.NET なら XML コメントの <exception> セクションで宣言されていない例外のことを指している。Java は詳しくないけど Java なら throws で宣言されていない例外。 ( ※ )
これを逆に言えば、基本クラスではスローしなくとも、派生クラスでスローされる可能性があるのなら、基本クラスで宣言しなければならないということになる。でなければ LSP に違反してしまう。
ここで注意しなければいけないのが、例外の抽象度だと思う。
例えば、書籍データをデータストアから取得するための GetBookData メソッドが IBookFinderインターフェイスに定義されているとする。
IBookFinder インターフェイスを利用するアプリケーションは今の所 2 つあるとする。片方はデータストアとして XML ファイルを利用し、BookFinderFromXml クラスが IBookFinder インターフェイスを実装している。もう片方はデータストアとして DB を利用し、BookFinderFromDB クラスが IBookFinder インターフェイスを実装している。
BookFinderFromXml クラスの GetBookData メソッドは、XML ファイルが見つからなかった時に FileNotFoundException をスローする。
一方、BookFinderFromDB クラスの GetBookData メソッドは、DB が見つからなかった時に DBNotFoundException をスローする ( こんな例外 .NET に用意されていないけど )。
では、IBookFinder インターフェイスの GetBookData メソッドにはこれらの例外がスローされる可能性があると宣言するのかというと、それは違う。そんなことしてたら、派生クラスが増えるたびに、IBookFinder インターフェイスに修正をすることになるかもしれない。
ここで抽象度が出てくる。例えば DataStoreNotFoundException といった例外クラスを定義し、IBookFinder インターフェイスの GetBookData メソッドでは、この例外をスローすると宣言するべきである。派生クラスでは、FileNotFoundException や DBNotFoundException の代わりに、この例外、またはこの例外を派生させた例外 ( DataStoreFileNotFoundException とか ) をスローできる。
ちなみに、LSP に準拠させる前の BookFinderFromXml クラスは FileNotFoundException をスローしていたが、FileNotFoundException のこういう使い方は不適切な気がする。LSP とか関係なく、FileNotFoundException ではなく「データストアが存在しない」という意味の例外をスローすべきだと思う。
LSP を適用することで、この点も解消されている所が面白い。
なお、この例で挙げた設計は、僕がこの記事を書くために適当に考えただけの設計であり、実際の業務アプリには適用できない ( または適用すべきでない ) 設計かもしれない。クラス名も適当だし。あくまでも LSP と例外について書きたかっただけなので。
※ .NET と Java では例外の扱い方に大きな差があります。ここでは .NET の <exception> セクションと Java の throws を並べてますが、全く性質の異なるものです。