数人の開発者と私は、大きくなりすぎたクラスをリファクタリングしようとしています。現在、このクラスは約3K行です。リファクタリングの目的は、ロジックをより保守しやすくすることです。
クラスは検証ロジックを保持します。検証ロジックは、プロセスの次のステップに進む前に、さまざまなチェックを実行して問題がないことを確認します。
現在、各検証は検証クラスの関数です。それらはすべて同じメソッドシグネチャを持っています。
私たちが持っていた1つのアイデアは、実行する必要のあるすべての検証のリストを提供するファクトリを使用して、検証ごとにクラスを作成することです。
これの簡単な実装を以下に示します。
検証インターフェースは次のとおりです。
public interface IValidationType
{
bool Validate();
}
次のような実装の場合:
public class ValidationType1 : IValidationType
{
private readonly IDependency1 _dependency1;
public ValidationType1(IDependency1 dependency1)
{
_dependency1 = dependency1;
}
public bool Validate()
{
return _dependency1.SomeFunction();
}
}
IValidationTypeを依存関係として受け入れるファクトリクラスを使用する場合:
public class ValidationFactory : IValidationFactory
{
private readonly IList<IValidationType> _validations;
public ValidationFactory(
IValidationType validation1,
IValidationType validation2)
{
_validations = new List<IValidationType>
{
validation1,
validation2
};
}
public IEnumerable<IValidationType> RetrieveValidations()
{
return _validations;
}
}
現在、私たちの計画は、名前付きレジスタを使用したダーティな作業を処理するためにIOCコンテナを乱用することです。
public class ValidationAutofacModule : Module
{
protected override void Load(ContainerBuilder builder)
{
builder.RegisterType<ValidationType1>().Named<IValidationType>("Validation1");
builder.RegisterType<ValidationType2>().Named<IValidationType>("Validation2");
builder.Register<IValidationFactory>(
x => new ValidationFactory(
x.ResolveNamed<IValidationType>("Validation1"),
x.ResolveNamed<IValidationType>("Validation2")));
}
}
この実装に関する懸念の1つは、作成された新しい検証ごとに、開発者はそれをファクトリーとIOCコンテナーに登録する必要があるということです。また、これを機能させるためにIOCコンテナを乱用しなければならない臭いもします。
これはアンチパターンですか?より良い方法はありますか?
インターフェイスに自然な複合実装がある場合、これは拡張ポイントを許可する良い方法です。私はこれを「プラグイン」アプローチと考えています。誰でもあなたのインターフェースの新しい実装を書くことができ、その動作は他のすべての実装と自然に混合されます。
「自然なコンポジット」は、コンポーネントの実装に委任し、その目的に自然な方法で結果を組み合わせるインターフェースの有効な実装です。あなたのケースは簡単です:Validate
をコンポジットに実装するには、すべてのコンポーネントでValidate
を呼び出し、結果をandと組み合わせます。
はい、あなたはあなたのIOCコンテナを乱用しています。DIの原則は必要なものを尋ねることです。あなたはIEnumerable<IValidationType>
(ここではIList
は不適切です。ランダムに変更する必要はないためです。)しかし、単にIEnumerable<IValidationType>
、2つの異なる実装を求めています。ご指摘のとおり、新しい実装を思い付くたびにファクトリが変更されるため、これはオープン/クローズに違反します。
工場のポイントもわかりません。それは0の責任を持っているようです。ファクトリの代わりに、コンポジットを作成します。
public class CompositeValidationType : IValidationType
{
public CompositeValidationType(IEnumerable<IValidationType> components) { ... }
public bool Validate() { return components.All(o => o.Validate()); }
}
次に、IOCコンテナーを取得して、このコンポジットを正規のIValidationTypeとして適切なコンポーネント実装に登録します。Reflectionは、ここであなたの友達になることができます。
名前付きの依存関係を使用することには私は強く反対しています。メソッドパラメータの名前を変更することは常に安全なリファクタリングであるべきであるという賢明なルールに違反します。
すべてのバリデーターに単一のファクトリーを使用します。単一のファクトリーは、依存関係を直接取得する代わりに、適切なバリデーターを取得するためのメソッドを公開し、インジェクションを受け取るクラスから呼び出すことができます。例えば。これの代わりに
_public MyClass(IInjected injected) //Constructor
{
_dependency = injected;
}
_
...このパターンに従ってください:
_public MyClass(IInjected injected) //Constructor
{
_dependency = injected.RetrieveDependency(arguments);
}
_
One Factoryは、_IEnumerable<IValidationType>
_を解決することにより、IoCコンテナーからすべてのバリデーターを取得できます。リストを取得すると、完全に制御できるロジックを使用して、マスターリストから選択することにより、バリデーターを返すリクエストを実行できます。
ファクトリは、正しいバリデーターを見つけるためのさまざまなメソッドを公開できます。一般的なパターンは次の3つです。
今のやり方、例えばRetrieveValidations("Validation1")
型引数を渡します。 RetrieveValidations<ValidationType1>()
型システムが知っているいくつかの項目を渡すと、その項目に関連付けられます。例えば。テキストのバリデーターはRetrieveValidationsFor<string>()
を使用できます。
最後の方法をお勧めします。コンパイル時に他の2つの手法では検出できない多くのエラーが検出されるためです。
名前付き登録を取得するためにマジックストリングを渡さないようにするために、型システムを使用して、どのバリデーターがどの型に対応するかを示すことができます。この例では、文字列、int、およびDateTimesに適した3つのバリデーターがあります。このパターンに従って、ビジネスが関心のあるオブジェクトをサポートできます。
_public interface IValidationType
{
bool Validate();
}
public interface IValidatorFor<T> : IValidationType
{
}
public class ValidationType1 : IValidatorFor<string>, IValidationType
{
public bool Validate() { return true; }
}
public class ValidationType2 :IValidatorFor<int>, IValidationType
{
public bool Validate() { return true; }
}
public class ValidationType3 :IValidatorFor<DateTime>, IValidationType
{
public bool Validate() { return true; }
}
_
これはファクトリーの例です。ここでは、型情報を渡して正しいバリデーターを取得する3つの異なる方法を示します。
_public class ValidationFactory : IValidationFactory
{
private readonly IList<IValidationType> _validations;
public ValidationFactory(ILifetimeScope container)
{
_validations = container.Resolve<IEnumerable<IValidationType>>().ToList();
}
public IEnumerable<T> RetrieveValidations<T>() where T : IValidationType
{
return _validations.OfType<T>();
}
public IEnumerable<IValidationType> RetrieveValidations(string validationName)
{
return _validations.Where( v => v.GetType().FullName.Contains(validationName) ); //or some other logic depending on your problem domain
}
public IEnumerable<IValidatorFor<T>> RetrieveValidationsFor<T>()
{
return _validations.OfType<IValidatorFor<T>>();
}
}
_
そして、注入がどのように機能するかを示すいくつかのテストクラス:
_public class Example1
{
protected readonly IEnumerable<IValidationType> _validations;
public Example1(IValidationFactory factory)
{
_validations = factory.RetrieveValidations<ValidationType1>().ToList();
}
public void Run()
{
Console.WriteLine("Example validators to be used:");
foreach (var f in _validations)
{
Console.WriteLine("{0}", f.GetType().FullName);
}
}
}
public class Example2
{
protected readonly IEnumerable<IValidationType> _validations;
public Example2(IValidationFactory factory)
{
_validations = factory.RetrieveValidations("2").ToList();
}
public void Run()
{
Console.WriteLine("Example2 validators to be used:");
foreach (var f in _validations)
{
Console.WriteLine("{0}", f.GetType().FullName);
}
}
}
public class Example3
{
protected readonly IEnumerable<IValidationType> _validations;
public Example3(IValidationFactory factory)
{
_validations = factory.RetrieveValidationsFor<DateTime>().ToList();
}
public void Run()
{
Console.WriteLine("Example3 validators to be used:");
foreach (var f in _validations)
{
Console.WriteLine("{0}", f.GetType().FullName);
}
}
}
_
ここに小さなテストプログラムがあります:
_public class Application
{
protected readonly Example1 _example1;
protected readonly Example2 _example2;
protected readonly Example3 _example3;
public Application(Example1 example1, Example2 example2, Example3 example3)
{
_example1 = example1;
_example2 = example2;
_example3 = example3;
}
public void Run()
{
_example1.Run();
_example2.Run();
_example3.Run();
}
}
public class Program
{
public static IContainer CompositionRoot()
{
var builder = new ContainerBuilder();
builder.RegisterType<ValidationType1>().As<IValidationType>();
builder.RegisterType<ValidationType2>().As<IValidationType>();
builder.RegisterType<ValidationType3>().As<IValidationType>();
builder.RegisterType<ValidationFactory>().As<IValidationFactory>();
builder.RegisterType<Example1>();
builder.RegisterType<Example2>();
builder.RegisterType<Example3>();
builder.RegisterType<Application>();
return builder.Build();
}
public static void Main()
{
CompositionRoot().Resolve<Application>().Run();
}
}
_
そして出力:
_Example validators to be used:
Example.ValidationType1
Example2 validators to be used:
Example.ValidationType2
Example3 validators to be used:
Example.ValidationType3
_
DotNetFiddle の完全な動作例を参照してください
注:一部の人々は、ファクトリーがコンテナーにアクセスするのは「悪い」と言っているかもしれません。これは通常は当てはまります(サービスロケーターのアンチパターンのように非常に酷似するものにつながります)が、工場では問題ありません。ファクトリーの目的は、依存関係を組み立ててオブジェクトを構築することであり、それ自体がコンポジションルートの拡張として機能します。