質問
.Net 3.5 の新しい拡張機能により、機能をインターフェイスから分割できるようになります。
たとえば、.Net 2.0 では
public interface IHaveChildren {
string ParentType { get; }
int ParentId { get; }
List<IChild> GetChildren()
}
(3.5 では) 次のようになります。
public interface IHaveChildren {
string ParentType { get; }
int ParentId { get; }
}
public static class HaveChildrenExtension {
public static List<IChild> GetChildren( this IHaveChildren ) {
//logic to get children by parent type and id
//shared for all classes implementing IHaveChildren
}
}
これは、多くのインターフェースにとってより良いメカニズムであるように私には思えます。このコードを共有するために抽象ベースは必要なくなり、コードは機能的には同じように動作します。これにより、コードの保守性が向上し、テストが容易になる可能性があります。
唯一の欠点は、抽象ベースの実装が仮想である可能性があることですが、それは回避できます (インスタンス メソッドは同じ名前の拡張メソッドを隠しますか?)そうすることは混乱を招くコードになるでしょうか?)
このパターンを定期的に使用しない理由が他にありますか?
説明:
そうですね、拡張メソッドはどこにでも使われる傾向にあります。私は、多くのピアレビューを行わずに .Net の値型を使用することに特に注意します (文字列上にあるのは、 .SplitToDictionary()
- に似ている .Split()
ただし、キーと値の区切り文字も使用します)
そこにはベストプラクティスに関する議論があると思います ;-)
(ちなみに:ダニー・スマーフ、あなたの首相は怖そうです。)
ここで特に質問しているのは、以前はインターフェイス メソッドがあった場所での拡張メソッドの使用についてです。
私は、多くのレベルの抽象基本クラスを避けようとしています。これらのモデルを実装するクラスのほとんどは、すでに基本クラスを持っています。このモデルは、オブジェクト階層をさらに追加するよりも保守しやすく、過剰結合も少ないと思います。
これは MS が Linq の IEnumerable と IQueryable に対して行ったことですか?
解決
拡張メソッドを賢明に使用すると、インターフェイスが(抽象)基本クラスとより同等の位置に置かれると思います。
バージョン管理。 基本クラスがインターフェイスに比べて優れている点の 1 つは、新しい仮想メンバーを新しいバージョンに簡単に追加できることですが、インターフェイスにメンバーを追加すると、古いバージョンのライブラリに対して構築された実装が機能しなくなることです。代わりに、新しいメンバーを含む新しいバージョンのインターフェイスを作成する必要があり、ライブラリは元のインターフェイスのみを実装するレガシー オブジェクトへのアクセスを回避または制限する必要があります。
具体的な例として、ライブラリの最初のバージョンは次のようにインターフェイスを定義します。
public interface INode {
INode Root { get; }
List<INode> GetChildren( );
}
ライブラリがリリースされると、現在のユーザーに影響を与えずにインターフェイスを変更することはできません。代わりに、次のリリースでは、追加の機能を追加するために新しいインターフェイスを定義する必要があります。
public interface IChildNode : INode {
INode Parent { get; }
}
ただし、新しいライブラリのユーザーのみが新しいインターフェイスを実装できます。レガシーコードを扱うには、古い実装を適応させる必要がありますが、これは拡張メソッドで適切に処理できます。
public static class NodeExtensions {
public INode GetParent( this INode node ) {
// If the node implements the new interface, call it directly.
var childNode = node as IChildNode;
if( !object.ReferenceEquals( childNode, null ) )
return childNode.Parent;
// Otherwise, fall back on a default implementation.
return FindParent( node, node.Root );
}
}
新しいライブラリのすべてのユーザーは、従来の実装と最新の実装の両方を同様に扱うことができるようになりました。
過負荷。 拡張メソッドが役立つもう 1 つの分野は、インターフェイス メソッドにオーバーロードを提供する場合です。アクションを制御するためにメソッドに複数のパラメーターを指定する場合がありますが、90% の場合、最初の 1 つまたは 2 つだけが重要です。C# ではパラメーターのデフォルト値を設定できないため、ユーザーは完全にパラメーター化されたメソッドを毎回呼び出すか、すべての実装でコア メソッドの簡単なオーバーロードを実装する必要があります。
代わりに、拡張メソッドを使用して簡単なオーバーロード実装を提供できます。
public interface ILongMethod {
public bool LongMethod( string s, double d, int i, object o, ... );
}
...
public static LongMethodExtensions {
public bool LongMethod( this ILongMethod lm, string s, double d ) {
lm.LongMethod( s, d, 0, null );
}
...
}
これらのケースは両方とも、インターフェイスによって提供される操作の観点から記述されており、簡単な、またはよく知られているデフォルトの実装が含まれることに注意してください。とはいえ、クラスから継承できるのは 1 回だけであり、拡張メソッドを対象を絞って使用することで、インターフェイスに欠けている基本クラスによって提供される機能の一部に対処する貴重な方法を提供できます :)
編集: Joe Duffy による関連投稿: デフォルトのインターフェースメソッド実装としての拡張メソッド
他のヒント
拡張メソッドはまさに次のように使用する必要があります。拡張子。重要な構造/設計関連のコードまたは重要な操作は、クラスまたはインターフェイスに組み込まれるか、クラスまたはインターフェイスから継承されるオブジェクトに入れる必要があります。
別のオブジェクトが拡張されたオブジェクトを使用しようとすると、そのオブジェクトには拡張が表示されないため、再度実装/再参照する必要がある場合があります。
従来の常識では、拡張メソッドは次の場合にのみ使用する必要があります。
- Vaibhav が述べたユーティリティ クラス
- シールドされたサードパーティ API の拡張
拡張メソッドで置き換えられる最も優れたものは、すべてのプロジェクトにあるユーティリティ クラスすべてだと思います。
少なくとも現時点では、拡張メソッドを他の方法で使用すると職場に混乱が生じると思います。
私の2ビット。
拡張メソッドを使用してドメイン/モデルと UI/ビューの機能を分離することは、特にこれらが別の名前空間に存在できるため、良いことだと考えています。
例えば:
namespace Model
{
class Person
{
public string Title { get; set; }
public string FirstName { get; set; }
public string Surname { get; set; }
}
}
namespace View
{
static class PersonExtensions
{
public static string FullName(this Model.Person p)
{
return p.Title + " " + p.FirstName + " " + p.Surname;
}
public static string FormalName(this Model.Person p)
{
return p.Title + " " + p.FirstName[0] + ". " + p.Surname;
}
}
}
このようにして、拡張メソッドを XAML データ テンプレートと同様に使用できます。クラスのプライベート/保護されたメンバーにアクセスすることはできませんが、アプリケーション全体でコードが過度に重複することなくデータの抽象化を維持できます。
インターフェイスを拡張することに問題はありません。実際、LINQ はこのようにして拡張メソッドをコレクション クラスに追加します。
そうは言っても、実際にこれを行うべきなのは、そのインターフェイスを実装するすべてのクラスに同じ機能を提供する必要があり、その機能が派生したインターフェイスの「公式」実装の一部ではない (そしておそらくそうすべきではない) 場合にのみ行うべきです。クラス。新しい機能を必要とするすべての派生型に対して拡張メソッドを記述することが非現実的である場合には、インターフェイスを拡張することも効果的です。
共通の機能を共有するために基本クラスを使用することを推奨している人がたくさんいます。これには注意してください。継承よりも合成を優先する必要があります。継承は、モデリングの観点から意味がある場合にのみ、ポリモーフィズムに使用する必要があります。コードの再利用には適したツールではありません。
質問に関しては:これを行うときは制限に注意してください。たとえば、示されているコードでは、拡張メソッドを使用して GetChildren を実装すると、この実装が効果的に「封印」され、必要に応じて IHaveChildren impl が独自の実装を提供できなくなります。これで問題ない場合は、拡張メソッドのアプローチはそれほど気にしません。これは決まったものではなく、後でさらに柔軟性が必要になったときに通常は簡単にリファクタリングできます。
柔軟性を高めるには、戦略パターンを使用することをお勧めします。何かのようなもの:
public interface IHaveChildren
{
string ParentType { get; }
int ParentId { get; }
}
public interface IChildIterator
{
IEnumerable<IChild> GetChildren();
}
public void DefaultChildIterator : IChildIterator
{
private readonly IHaveChildren _parent;
public DefaultChildIterator(IHaveChildren parent)
{
_parent = parent;
}
public IEnumerable<IChild> GetChildren()
{
// default child iterator impl
}
}
public class Node : IHaveChildren, IChildIterator
{
// *snip*
public IEnumerable<IChild> GetChildren()
{
return new DefaultChildIterator(this).GetChildren();
}
}
もうちょっと。
複数のインターフェイスが同じ拡張メソッド シグネチャを持つ場合は、呼び出し元を 1 つのインターフェイス タイプに明示的に変換してから、メソッドを呼び出す必要があります。例えば。
((IFirst)this).AmbigousMethod()
ああ。インターフェースを拡張しないでください。
インターフェイスは、クラスが実装する必要があるクリーンなコントラクトであり、そのクラスの使用法 しなければならない これが正しく動作するには、コア インターフェイスにあるものに制限する必要があります。
そのため、常にインターフェイスを実際のクラスではなく型として宣言します。
IInterface variable = new ImplementingClass();
右?
機能を追加したコントラクトが本当に必要な場合は、抽象クラスが頼りになります。
Rob Connery (Subsonic および MVC Storefront) は、Storefront アプリケーションに IRepository のようなパターンを実装しました。上記のパターンとまったく同じではありませんが、いくつかの類似点があります。
データ層は IQueryable を返します。これにより、消費層はその上にフィルタリングと並べ替え式を適用できます。利点は、たとえば、単一の GetProducts メソッドを指定して、ソート、フィルタリング、さらには結果の特定の範囲のみをどのように行うかを消費層で適切に決定できることです。
伝統的なアプローチではありませんが、非常にクールで、間違いなく DRY のケースです。
私が認識している問題の 1 つは、大企業では、このパターンによりコードが誰にとっても (不可能ではないにしても) 理解して使用するのが難しくなる可能性があるということです。複数の開発者が、既存のクラスとは別に (そして、助けてください、BCL クラスにも) 既存のクラスに独自のメソッドを常に追加している場合、コード ベースが急速に制御不能になる可能性があります。
私自身の仕事でも、これが起こっているのがわかりました。私たちが取り組んでいるコードをすべて UI またはデータ アクセス層に追加したいという私の PM の願望により、彼が 20 または 30 のメソッドを追加することを主張しているのがよくわかりました。 System.String は文字列処理に直接関係するもののみです。
同様のものを解決する必要がありました:IIDable が長い getId() 関数を持つインターフェイスである拡張関数に List<IIDable> を渡したいと考えていました。GetIds(this List<IIDable> bla) を使用しようとしましたが、コンパイラによって許可されませんでした。代わりにテンプレートを使用し、関数内でインターフェイスの型に型をキャストしました。一部の linq to sql で生成されたクラスにはこの関数が必要でした。
これがお役に立てば幸いです:)
public static List<long> GetIds<T>(this List<T> original){
List<long> ret = new List<long>();
if (original == null)
return ret;
try
{
foreach (T t in original)
{
IIDable idable = (IIDable)t;
ret.Add(idable.getId());
}
return ret;
}
catch (Exception)
{
throw new Exception("Class calling this extension must implement IIDable interface");
}