読者です 読者をやめる 読者になる 読者になる

.NET のクラスライブラリ設計

C# Book

.NETのクラスライブラリ設計 開発チーム直伝の設計原則、コーディング標準、パターン (Microsoft.net Development Series)

.NETのクラスライブラリ設計 開発チーム直伝の設計原則、コーディング標準、パターン (Microsoft.net Development Series)

とっくに読み終わっていたんだけど、まとめる時間がなかったのでかなり時間が空いてしまった・・・
ということで基本的には「・・・ん?」って思ったところとかのまとめです。

アセンブリと名前空間

よく Java の package と C# の namespace を同じようなものとして扱っている人はいるけど、この本では、

アセンブリ
パッケージング及び配置の境界
名前空間
開発者に対する論理的なグループ

としている*1
Java の package はこの 2 つを合わせたもの*2だし、Java名前空間はファイルの配置という物理的なグループも作り出すので、まったく同じものというわけではない。


なので、Java の package と C# の namespace の使い方は違ったものであるべきなのに、どうもそう考えていない人は多いっぽい・・・
この話はまた機会があればまとめようかな。時間があれば、だけど。
Java の package については、

アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技

アジャイルソフトウェア開発の奥義 第2版 オブジェクト指向開発の神髄と匠の技

の 20 章を熟読すること。

一般的な型名を採用してもいいんじゃ?

Element、Node、Log、および Message のような一般的な型名を採用してはいけません。

一般的なシナリオにおいて、非常に高い可能性で型名の競合が誘発されます。汎用的な型名は修飾すべきです (FormElement、XmlNode、EventLog、SoapMessage)。

.NET のクラスライブラリ設計 (P.48)

うーん、こういうことを回避するために名前空間があるんじゃないの?
それぞれ、Form.Element、Xml.Node、Event.Log、Soap.Message な感じで名前空間で区切って、競合する場合は直前までを using すればいい話で、むしろ一般的な型名はどんどん付けるべき。

メソッド名の付け方

メソッドには動詞または動詞句の名前を付けます。

.NET のクラスライブラリ設計 (P.56)

ファクトリメソッドの名前は、"Create" という単語に作成される型の名前を連結したものにすることを検討します。

.NET のクラスライブラリ設計 (P.288)

これらはよくある規約だけど、もう古くなっていて、これだけじゃ足りないと思う。
例えば、Effective Java の第二版では、

valueOf
大ざっぱに言えば、パラメータと同じ値を持つインスタンスを返します。〜略
of
EnumSet (項目 32) で広まった、valueOf に対する簡潔な代替です。
getInstance
〜略
newInstance
getInstance に似ていますが、newInstance は返される個々のインスタンスはすべて別々のインスタンスである点が異なります。
getType
getInstance に似ていますが、ファクトリーメソッドが対象のクラスと異なるクラスにある場合に使用されます。Type はファクトリーメソッドから返されるオブジェクトの型を示しています。
newType
newInstance に似ていますが、ファクトリーメソッドが対象のクラスと異なるクラスにある場合に使用されます。Type はファクトリーメソッドから返されるオブジェクトの型を示しています。
Effective Java 第 2 版 項目 1 コンストラクタの代わりに static ファクトリーメソッドを検討する (P.10)

何らかの処理を行うメソッドは、一般に動詞あるいは (目的語を含む) 動詞句で命名されます。たとえば、append や drawImage です。
〜略〜
特別に述べておくべきメソッド名が多少あります。オブジェクトの型を変換し、別の型の無関係なオブジェクトを返すメソッドは、たいていは toType と呼ばれます。例えば、toString、toArray です。レシーバーオブジェクトの型と異なる型を持つビュー (view) (項目 5) を返すメソッドは、たいていは asType と呼ばれます。たとえば、asList です。メソッドが呼び出されたオブジェクトと同じ値を持つ基本データを返すメソッドは、たいていは typeValue と呼ばれます。たとえば、intValue です。static ファクトリーメソッドに対する共通の名前は、valueOf、of、getInstance、newInstance、getType、newType です (項目 1、10 項)。

Effective Java 第 2 版 項目 56 一般的に受け入れられている命名規約を守る (P.231 - 232)

などとある。
例えば、タプルを生成するメソッドが Tuple.CreateTuple とか、Tuple.Create とかはどうなんだ、みたいな。Tuple.Of の方が短いし分かりやすいと思うんだけど・・・


このほかにも、流れるようなインターフェイスを設計する場合なども古い規約では対応できない。

CanXxx

CanRead は Readable よりも理解しやすいものです。しかしながら、Created は実際に IsCreated よりも読みやすいものです。プレフィックスを持つことは多くの場合、特にコードエディタ内で IntelliSense に表示されるときに、冗長過ぎ、かつ不要です。
〜略〜

受動態よりも能動態を優先して選択すべきです。

if (stream.CanSeek) // こちらのほうが良い
if (stream.IsSeekable)
.NET のクラスライブラリ設計 (P.57 - 58)

個人的には、IntelliSense で bool を返すプロパティがまとまっていた方が分かりやすいので、Is プレフィックス推奨派なんだけど・・・
それと、C# では not が「!」なので視認性が悪いため、プロパティで実装せずにメソッドとして実装して拡張メソッドとして Not 版を用意するというのもありだと思う。

public interface ISomeInterface
{
    bool IsHoge();
}

public static class ISomeInterfaceExtension
{
    public static bool IsNotHoge(this ISomeInterface self) { return !self.IsHoge(); }
}

・・・拡張プロパティが欲しいところ。

Before/After

事前および事後のイベントを示すために、"Before" および "After" というプレフィックスおよびサフィックスを使用してはなりません。

.NET のクラスライブラリ設計 (P.58)

その代わりに現在進行形によって事前のイベントを、過去形によって事後のイベントを表せ、といっているんだけど・・・
Before/After の方が分かりやすくない?事前のイベントについては特に。

イベントハンドラアンチパターン

イベントハンドラに対して、"sender" および "e" という名前の 2 つのパラメータを使用します。

sender パラメータはイベントを発生させたオブジェクトを表します。sender パラメータは、より特定的な型を採用可能であったとしても、一般的に object 型になります。このパターンは .NET Framework 全体で一貫して採用されています。

.NET のクラスライブラリ設計 (P.59)

イベントハンドラの最初のパラメータには、型として object を使用し、名前を sender にします。

〜略〜

なぜ?みんないつもこう尋ねてきます。結局、これは単なるパターンなのです。

.NET のクラスライブラリ設計 (P.134)

これはアンチパターンなんじゃないの?過去の遺産があることは分かるけど、これをパターンと言い張るには無理があるんじゃないだろうか?
sender が object だと、いつもいつも sender の型の確認とキャストが必要になって、DRY に違反しまくり。
だからこれには従うべきじゃないんじゃないかな。

class と struct

ライブラリで頻繁に使用されることが予測される型をプロファイルし、実際に発生しないかもしれないような思い込みではなく実際のデータに基づいて、参照型を値型に変更することを推奨します。

.NET のクラスライブラリ設計 (P.70)

これはリファクタリング手順を確立させる必要があるかも。

参照型は参照によって渡され、逆に値型は値によって渡されます。

.NET のクラスライブラリ設計 (P.70)

とても誤解を生みそうな表現の気が・・・

スマートポインタ?

"スマートポインタ" な値型を作成することによって、それらが通常のオブジェクトであるかのように配列内の要素を参照することも可能です。これらの型は 2 つのフィールドを持ちます。最初のフィールドは巨大な配列に対する参照、2 番目のフィールドは配列のインデックスです。ユーザーの視点からは、これらの "スマートポインタ" の使用は通常の参照型の使用のように見えますが、メモリ使用量は大きく削減されます。

.NET のクラスライブラリ設計 (P.71)

スマートポインタ・・・用語の再利用はさけて欲しいんだけど・・・
スマートポインタというと普通は C++ のスマートポインタだよね。

構造体の設計

変更可能 (mutable) な値型を定義してはいけません。

.NET のクラスライブラリ設計 (P.83)

これは是非従うべき!

public struct Point
{
    public int X { get; set; }
    public int Y { get; set; }
    // 以下略
}

class Program
{
    static void Hoge(Point p)
    {
        p.X = 10;
    }
    
    static void Main()
    {
        var p = new Point() { X = 1, Y = 1 };
        Hoge(p);
        Console.WriteLine(p.X);
    }
}

とか、罠過ぎる。

コンストラクタが常に最善とは限らない

System.DateTime はいくつかのコンストラクタオーバーロードを持ちます。最も強力でありながら、同時に最も複雑なオーバーロードは、8 つのパラメータを取ります。ありがたいことに、コンストラクタのオーバーロードによって、この型は 3 つのシンプルなパラメータ、すなわち hour、minute および second を取る、より短いコンストラクタもサポートします。

public struct DateTime {
    public DateTime(int year, int month, int day,
               int hour, int minute, int second,
               int millisecond, Calendar calendar) { ... }
    
    public DateTime(int hour, int minute, int second) { ... }
}
.NET のクラスライブラリ設計 (P.102)

何もありがたくないよ!いや、確かに下のコンストラクタがないと困るけど、そうじゃないだろ。
コンストラクタのオーバーロードでは、year, month, day を引数に取るバージョンは作れない。
なんで、ここは素直にファクトリにすべき場所だと思うんだけどなぁ。

特別な構築メカニズムと比較すると、一般的にはコンストラクタのほうがユーザビリティ、一貫性、利便性に優れているので、ファクトリよりもコンストラクタを優先して使用します。

.NET のクラスライブラリ設計 (P.285)

それはない。DateTime のコンストラクタのユーザビリティとかもうね・・・
常にファクトリを用意しろ、って言うつもりはないけど、ユーザビリティとリーダビリティは一般的にはファクトリのほうが上じゃないかな?
ただまぁ、ファクトリに対してもうちょっとサポートが欲しいのは確か。

戻り値型が異なるオーバーロードについての言及はないのか・・・

できれば避けて欲しいんだけど。
たしか標準ライブラリの中にこれがあってはまったことがあるんだけど、なんだったか忘れてしまった・・・


一番近いのは、105 ページの「異なるセマンティクスを持つにもかかわらず同じ位置に似たような型があるパラメータを持つオーバーロードを定義してはいけません。」かな。

null について

オプションのパラメータに対しては null を渡すことを認めます。

〜略〜

このガイドラインが、魔法の定数として null を使用することを開発者に推奨することを意図しているのではなく、むしろ先ほど説明したように明示的なチェックを避けることを意図していることに注意してください。実際に、API を呼び出すときにリテラルの null を使用する必要があるときはいつでも、コードにエラーがあるか、またはフレームワークが適切なオーバーロードを提供していないことを示します。

.NET のクラスライブラリ設計 (P.106)

あー、オプションのパラメータに null を渡すようなシナリオがあるなら、それはそのパラメータを削ったオーバーロード版を提供すべき、って考え方は面白いな。


null を生成するメソッド・プロパティを極力避ける、という項目も欲しかったけど、どうも見つからない。

メソッドかプロパティか

経験則として言えるのは、メソッドは動作を表現すべきであり、そしてプロパティはデータを表現すべきであるということです。

.NET のクラスライブラリ設計 (P.112)

流れるようなインターフェイス・・・

イニシャライザ前提のプロパティ

設定専用のプロパティ、またはゲッタよりも幅広いアクセス可能性を持つセッタを持つプロパティを提供してはいけません。

.NET のクラスライブラリ設計 (P.116)

ゲッタよりも可視性の広いセッタは、イニシャライザで使うことを前提としてます、って意志表明に使えないかな・・・

public class Hoge
{
    public int X { private get; set; }
}

として、

var h = new Hoge() { X = 10 };

みたいな!

拡張メソッド

拡張メソッドの使用は、以下のシナリオのいずれかで検討します。

  • インターフェイスのすべての実装に関係するヘルパ機能がコアインターフェイスの観点から書くことが可能である場合に、そのようなヘルパ機能を提供するため。
  • ある型に依存するインスタンスメソッドを導入するとき、そのような依存性が依存性の管理ルールを破壊することになる場合。
.NET のクラスライブラリ設計 (P.138)

個人的には拡張メソッドによって null 安全なメソッドが作れる、というのも大きいのだけれど・・・
極力 null は排除したいんだけど、現実はそうあまくないよね、という。


あと次のページで知ったんだけど、VB って System.Object の拡張メソッド呼び出せないんだ・・・

演算子のオーバーロード

演算子のオーバーロードを定義するときは、推測が難しいものにしてはいけません。

〜略〜
ストリームに書き込むためにシフト演算子を使用することは適切ではありません。

.NET のクラスライブラリ設計 (P.144)

あ、C++ を Dis ってますか?
・・・というのはおいといて、C# にも Boost.Xpressive 的なものが欲しいなぁ、なんて思ってたり。
あと拡張メソッド的に演算子オーバーロードを定義したいなぁ、とか。
4.0 では dynamic 使って・・・ってのもできなくはないみたいだけど・・・使う側で意識させたくないよね・・・

演算子に対応する名前

各オーバーロードされた演算子に対応するフレンドリな名前のメソッドを提供することを検討します。

〜略〜

C#演算子のシンボル メタデータ上の名前 フレンドリな名前
... ... ...
+ (二項) op_Addition Add
- (二項) op_Subtraction Subtract
... ... ...
!= op_Inequality Equals
... ... ...
.NET のクラスライブラリ設計 (P.144 - 145)

+ に Add、- に Subtract ってのはどうだろう。単純に Plus/Minus で良くないか?
例えば

Java Puzzlers 罠、落とし穴、コーナーケース

Java Puzzlers 罠、落とし穴、コーナーケース

には、

Java の不変型のメソッド名には、誤解させるものがあります。add、subtract、negate などの名前は、これらのメソッドが呼び出されたインスタンスに対して変更を行うような印象を与えます。もっと良い名前は plus、minus、negation です。
そうすると、API 設計者に対する教訓は、不変型に対するメソッドの名前付けを行う場合には、動詞よりも前置詞と名詞を選ぶということです。

Java Puzzlers 罠、落とし穴、コーナーケース パズル 56: 大問題 (Big Problem) (P.132)

とあるし。
あと、!= が Equals になってるのは、!= 版は用意せずに == 版を ! しろ、ってことだろうけど・・・うーん。

PowerCollections

PowerCollections プロジェクトは System.Collections.Generic 名前空間を拡張するライブラリです。これはその名前空間に含まれる抽象に対する素晴らしいフィードバックおよび検証のソースです。

.NET のクラスライブラリ設計 (P.173)

こんなものあったのか!知らなかった!!
Deque、Set、Bag/MultiDictionary などに加え、Algoritms の充実っぷりも素晴らしい・・・

例外の再スロー

新しい例外をスローする時、実際に発生したエラーとは異なるエラーを報告しています。これも同様にアプリケーションをデバッグする能力を損ねます。したがって、常にスローするよりも再スローが望ましく、かつキャッチアンドスロー (および再スロー) はどちらも避けるようにしてください。

.NET のクラスライブラリ設計 (P.196)

例外をラップするときには内部例外を指定します。

throw new ConfigurationFileMissingException(..., e);

このことをどの程度注意深く考え抜く必要があるのかについて十分に強調されていない可能性があります。よくわからないときは、例外を他の例外にラップしてはいけません。CLR において、ラップ処理があらゆる種類のトラブルを引き起こすことが知られている例はリフレクションです。リフレクションを使用してメソッドを実行したとき、メソッドが例外をスローすると、CLR はそれをキャッチし、新しい TargetInvocationException をスローします。実際のメソッドおよびメソッド内の問題のある場所を隠ぺいするので、これは信じがたいほどに迷惑なものです。

.NET のクラスライブラリ設計 (P.198)

上のはちょっと後半よくわからない。
下のは、TargetInvocationException とかモロに Java の影響を受けてるよな・・・
Java と違って C# には検査例外がないんだから、例外をラップする意味はほとんどないはず*3。だから C# では基本的には例外をラップすべきではない。

DebuggerDisplay 属性

そんなものあったのね・・・(233 ページ)
EditorBrowsable 属性なんてのもはじめて知った (66 ページ)

用語

ファクトリには 2 つの主要なグループがあります。すなわち、ファクトリメソッド (factory method) とファクトリ型 (factory type) です。ファクトリ型は「アブストラクトファクトリ (abstract factory)」とも呼ばれます。

.NET のクラスライブラリ設計 (P.284)

ぎゃー!これはひでぇ!
これだと「Factory Method パターンはファクトリ型だからアブストラクトファクトリ」っつー意味不明な状況に・・・

結論

色々書いたけど、とてもいい本ですよ!
名前的には「.NET」なんて付いてるけど、他の言語を使っている場合でも参考になる部分は多々あるので、C# 使わないよ、って人でも読んでみるといいかも。


ただ、ちょっと高いのと、誤植的な物も結構あるのでまずは会社で買ってもらえばいいんじゃないかな。
間違い部分をメールしたら、増刷で修正されるみたいですし。
あと正誤表も用意されるようだから、値段がネックにならないなら是非買いましょう!
値段分の価値はあるかって?あるある。絶対にある。

*1:45 ページあたり

*2:更に可視性の境界でもある

*3:Java では検査例外があるので、あまりにも低レベルな検査例外は実装の詳細をさらけだすことになるので、高レベルな検査例外か、RuntimeException 系の例外でラップして適切な粒度の例外に変換することはよくあるし、すべき