質問
C#の内部クラスの使用と構造に関するベストプラクティスは何ですか。
たとえば、非常に大きな基本クラスと2つの大きな内部クラスがある場合、それらを別々の(部分クラス)コードファイルに分割するか、1つの非常に扱いにくいコードファイルとして残す必要がありますか?
また、パブリッククラスを継承した内部クラスを持つ抽象クラスを持つことは悪い習慣ですか?
解決
通常、2つの目的のいずれかのために内部クラスを予約します。
-
親クラスが1つ以上の抽象メソッドを持つ抽象基本実装であり、各サブクラスが特定の実装を提供する実装である親クラスから派生するパブリッククラス。フレームワークの設計とガイドラインを読んだ後、これは「回避」とマークされていることがわかりましたが、列挙型に似たシナリオで使用しています-おそらく悪い印象を与えているかもしれません
-
内部クラスはプライベートであり、ビジネスロジックの単位であるか、または他のクラスによって消費または使用されたときに基本的に破損する方法で親クラスに密結合されています。
他のすべてのケースでは、それらをコンシューマ/論理親と同じ名前空間と同じアクセシビリティレベルに維持しようとします。多くの場合、「main」よりもやや使いにくい名前を使用します。クラス。
大規模なプロジェクトでは、最初または主な目的であるために強く結合されたコンポーネントを最初に構築することに気付くことがありますが、それはロックする非常に良いまたは技術的な理由がない限り見えないように隠すと、クラスを公開しても他のコンポーネントが消費できるように害はほとんどありません。
編集サブクラスについて話している場合でも、それらは多かれ少なかれ適切に設計され、疎結合されたコンポーネントである必要があることに注意してください。それらがプライベートであり、外界から見えない場合でも、最小限の「表面積」を維持します。クラス間で、将来の拡張または変更のためにコードの保守性を大幅に緩和します。
他のヒント
手元に本はありませんが、クライアントがクラス名を参照する必要がない限り、Framework Design Guidelinesは public
内部クラスの使用を推奨しています。 private
内部クラスは問題ありません。誰もこれらに気付かないでしょう。
悪い: ListView.ListViewItemCollection collection = new ListView.ListViewItemCollection();
良い: listView.Items.Add(...);
大規模なクラスについて:通常、このようなものを、それぞれが特定の機能を備えた小さなクラスに分割する価値があります。最初に分割するのは難しいですが、後であなたの人生が楽になると予測しています...
通常、内部クラスはプライベートであり、それらを含むクラスでのみ使用可能でなければなりません。それらの内部クラスが非常に大きい場合、独自のクラスにする必要があることを示唆します。
通常、大きな内部クラスを取得した場合、内部クラスは含まれているクラスと密に結合されており、プライベートメソッドにアクセスする必要があるためです。
これはかなり主観的であると思いますが、「ホスト」を作成することで、おそらくそれらを別々のコードファイルに分割します。クラス部分的。
このようにすることで、プロジェクトファイルを編集して、さらに概要を取得できます。 Windows Formsのデザイナークラスのようにファイルグループを作成します。これを自動的に行うVisual Studioアドインを見たことがあると思いますが、どこにいるかは覚えていません。
編集:
しばらく見てから、VSCommandsと呼ばれるVisual Studioアドインを見つけました
そのような獣をどのように構成するかについてのみ...
部分クラスを使用して、メインクラスとネストされたクラスを分割できます。その際、ファイルに適切な名前を付けることをお勧めします。これにより、何が起こっているかが明らかになります。
// main class in file Outer.cs
namespace Demo
{
public partial class Outer
{
// Outer class
}
}
// nested class in file Outer.Nested1.cs
namespace Demo
{
public partial class Outer
{
private class Nested1
{
// Nested1 details
}
}
}
ほぼ同じ方法で、多くの場合、独自のファイルに(明示的な)インターフェイスが表示されます。例えば #region
を編集するエディターのデフォルトではなく、 Outer.ISomeInterface.cs
。
プロジェクトのファイル構造は次のようになり始めます
/Project/Demo/ISomeInterface.cs /Project/Demo/Outer.cs /Project/Demo/Outer.Nested1.cs /Project/Demo/Outer.ISomeInterface.cs
通常、これを実行しているのは、Builderパターンのバリエーションです。
私は個人的に、ファイルごとに1つのクラスを持ち、そのファイルの一部として内部クラスを持つことを好みます。内部クラスは通常(ほぼ常に)プライベートであり、クラスの実装の詳細であると考えています。それらを別のファイルに入れると物事が混乱します、IMO。
コード領域を使用して内部クラスをラップし、詳細を非表示にすると、この場合はうまく機能し、ファイルの操作が難しくなりません。コード領域は内部クラス「非表示」を保持しますが、それはプライベートな実装の詳細であるため、私には問題ありません。
個人的に内部クラスを使用して、クラス内部でのみ使用される概念と操作の一部をカプセル化します。このようにして、そのクラスの非公開APIを汚染せず、APIをクリーンでコンパクトに保ちます。
部分クラスを利用して、これらの内部クラスの定義を別のファイルに移動して、組織化を改善できます。 VSは、ASP.NET、WinFormフォームなどのテンプレート化されたアイテムの一部を除き、部分クラスファイルを自動的にグループ化しません。プロジェクトファイルを編集して、そこにいくつかの変更を加える必要があります。そこにある既存のグループ化の1つを見て、どのように行われるかを確認できます。ソリューションエクスプローラーで部分クラスファイルをグループ化できるマクロがいくつかあると思います。
私の意見では、内部クラスは、必要に応じて小さくして、そのクラスのみが内部で使用する必要があります。 .NETフレームワークでRelfectorを使用すると、その目的のためだけにRelfectorが頻繁に使用されることがわかります。
内部クラスが大きくなりすぎている場合は、保守性のためだけに、どうにかしてそれらを別のクラス/コードファイルに移動します。誰かが内部クラス内で内部クラスを使用するのは素晴らしいアイデアだと思った既存のコードをサポートする必要があります。その結果、4〜5レベルの深さで内部クラス階層が実行されました。言うまでもなく、コードは理解できず、何を見ているかを理解するのに時間がかかります。
ネストされたクラスの実用的な例を参照してください。ネストされたクラスを使用すると、いくつかの単体テストが追加されます。
namespace CoreLib.Helpers
{
using System;
using System.Security.Cryptography;
public static class Rnd
{
private static readonly Random _random = new Random();
public static Random Generator { get { return _random; } }
static Rnd()
{
}
public static class Crypto
{
private static readonly RandomNumberGenerator _highRandom = RandomNumberGenerator.Create();
public static RandomNumberGenerator Generator { get { return _highRandom; } }
static Crypto()
{
}
}
public static UInt32 Next(this RandomNumberGenerator value)
{
var bytes = new byte[4];
value.GetBytes(bytes);
return BitConverter.ToUInt32(bytes, 0);
}
}
}
[TestMethod]
public void Rnd_OnGenerator_UniqueRandomSequence()
{
var rdn1 = Rnd.Generator;
var rdn2 = Rnd.Generator;
var list = new List<Int32>();
var tasks = new Task[10];
for (var i = 0; i < 10; i++)
{
tasks[i] = Task.Factory.StartNew((() =>
{
for (var k = 0; k < 1000; k++)
{
lock (list)
{
list.Add(Rnd.Generator.Next(Int32.MinValue, Int32.MaxValue));
}
}
}));
}
Task.WaitAll(tasks);
var distinct = list.Distinct().ToList();
Assert.AreSame(rdn1, rdn2);
Assert.AreEqual(10000, list.Count);
Assert.AreEqual(list.Count, distinct.Count);
}
[TestMethod]
public void Rnd_OnCryptoGenerator_UniqueRandomSequence()
{
var rdn1 = Rnd.Crypto.Generator;
var rdn2 = Rnd.Crypto.Generator;
var list = new ConcurrentQueue<UInt32>();
var tasks = new Task[10];
for (var i = 0; i < 10; i++)
{
tasks[i] = Task.Factory.StartNew((() =>
{
for (var k = 0; k < 1000; k++)
{
list.Enqueue(Rnd.Crypto.Generator.Next());
}
}));
}
Task.WaitAll(tasks);
var distinct = list.Distinct().ToList();
Assert.AreSame(rdn1, rdn2);
Assert.AreEqual(10000, list.Count);
Assert.AreEqual(list.Count, distinct.Count);
}