カプセル化を壊さずに依存性注入を使用できますか?


15

ここに私のソリューションとプロジェクトがあります:

  • BookStore (ソリューション)
    • BookStore.Coupler (プロジェクト)
      • Bootstrapper.cs
    • BookStore.Domain (プロジェクト)
      • CreateBookCommandValidator.cs
      • CompositeValidator.cs
      • IValidate.cs
      • IValidator.cs
      • ICommandHandler.cs
    • BookStore.Infrastructure (プロジェクト)
      • CreateBookCommandHandler.cs
      • ValidationCommandHandlerDecorator.cs
    • BookStore.Web (プロジェクト)
      • Global.asax
    • BookStore.BatchProcesses (プロジェクト)
      • Program.cs

Bootstrapper.cs

public static class Bootstrapper.cs 
{
    // I'm using SimpleInjector as my DI Container
    public static void Initialize(Container container) 
    {
        container.RegisterManyForOpenGeneric(typeof(ICommandHandler<>), typeof(CreateBookCommandHandler).Assembly);
        container.RegisterDecorator(typeof(ICommandHandler<>), typeof(ValidationCommandHandlerDecorator<>));
        container.RegisterManyForOpenGeneric(typeof(IValidate<>),
            AccessibilityOption.PublicTypesOnly,
            (serviceType, implTypes) => container.RegisterAll(serviceType, implTypes),
            typeof(IValidate<>).Assembly);
        container.RegisterSingleOpenGeneric(typeof(IValidator<>), typeof(CompositeValidator<>));
    }
}

CreateBookCommandValidator.cs

public class CreateBookCommandValidator : IValidate<CreateBookCommand>
{
    public IEnumerable<IValidationResult> Validate(CreateBookCommand book)
    {
        if (book.Author == "Evan")
        {
            yield return new ValidationResult<CreateBookCommand>("Evan cannot be the Author!", p => p.Author);
        }
        if (book.Price < 0)
        {
            yield return new ValidationResult<CreateBookCommand>("The price can not be less than zero", p => p.Price);
        }
    }
}

CompositeValidator.cs

public class CompositeValidator<T> : IValidator<T>
{
    private readonly IEnumerable<IValidate<T>> validators;

    public CompositeValidator(IEnumerable<IValidate<T>> validators)
    {
        this.validators = validators;
    }

    public IEnumerable<IValidationResult> Validate(T instance)
    {
        var allResults = new List<IValidationResult>();

        foreach (var validator in this.validators)
        {
            var results = validator.Validate(instance);
            allResults.AddRange(results);
        }
        return allResults;
    }
}

IValidate.cs

public interface IValidate<T>
{
    IEnumerable<IValidationResult> Validate(T instance);
}

IValidator.cs

public interface IValidator<T>
{
    IEnumerable<IValidationResult> Validate(T instance);
}

ICommandHandler.cs

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}

CreateBookCommandHandler.cs

public class CreateBookCommandHandler : ICommandHandler<CreateBookCommand>
{
    private readonly IBookStore _bookStore;

    public CreateBookCommandHandler(IBookStore bookStore)
    {
        _bookStore = bookStore;
    }

    public void Handle(CreateBookCommand command)
    {
        var book = new Book { Author = command.Author, Name = command.Name, Price = command.Price };
        _bookStore.SaveBook(book);
    }
}

ValidationCommandHandlerDecorator.cs

public class ValidationCommandHandlerDecorator<TCommand> : ICommandHandler<TCommand>
{
    private readonly ICommandHandler<TCommand> decorated;
    private readonly IValidator<TCommand> validator;

    public ValidationCommandHandlerDecorator(ICommandHandler<TCommand> decorated, IValidator<TCommand> validator)
    {
        this.decorated = decorated;
        this.validator = validator;
    }

    public void Handle(TCommand command)
    {
        var results = validator.Validate(command);

        if (!results.IsValid())
        {
            throw new ValidationException(results);
        }

        decorated.Handle(command);
    }
}

Global.asax

// inside App_Start()
var container = new Container();
Bootstrapper.Initialize(container);
// more MVC specific bootstrapping to the container. Like wiring up controllers, filters, etc..

Program.cs

// Pretty much the same as the Global.asax

問題のセットアップが長くなって申し訳ありません。実際の問題を詳しく説明する以外に、これを説明する良い方法はありません。

CreateBookCommandValidatorを作成したくありませんpublic。私はむしろそれを望みますinternalが、私がそれを作るとinternal私はそれを私のDIコンテナに登録することができなくなります。私が内部にしたいのは、IValidate <>実装の概念を持つ必要がある唯一のプロジェクトがBookStore.Domainプロジェクトにあるためです。他のプロジェクトはIValidator <>を使用する必要があるだけで、すべての検証を満たすCompositeValidatorを解決する必要があります。

カプセル化を壊さずに依存性注入を使用するにはどうすればよいですか?それとも私はこれについて間違っていますか?


ちょっとした注意:使用しているのは正しいコマンドパターンではないため、コマンドを呼び出すことは誤った情報である可能性があります。また、CreateBookCommandHandlerはLSPを破壊しているように見えます。オブジェクトを渡すと、CreateBookCommandから派生したものはどうなりますか?ここであなたがしていることは、実際には貧血領域モデルのアンチパターンだと思います。保存のようなものはドメイン内にあり、検証はエンティティの一部である必要があります。
陶酔

1
@ユーフォリック:それは正しい。これはコマンドパターンではありません。実際のところ、OPはコマンド/ハンドラーパターンという異なるパターンに従います。
スティーブン

たくさんの良い答えがあったので、もっと答えとしてマークできたらいいのにと思います。皆さん、助けてくれてありがとう。
エヴァンラーセン

@Euphoric、プロジェクトのレイアウトを再考した後、CommandHandlersはDomainにあるべきだと思います。なぜインフラストラクチャプロジェクトに入れたのかわかりません。ありがとう。
エヴァンラーセン

回答:


11

作るCreateBookCommandValidatorために、国民はカプセル化に違反しません

カプセル化は、クラス内の構造化データオブジェクトの値または状態を隠すために使用され、権限のない者がそれらに直接アクセスすることを防ぎます(ウィキペディア

あなたCreateBookCommandValidatorはそのデータメンバーへのアクセスを許可していないので(現時点では何も持っていないようです)、カプセル化に違反していません。

このクラスを公開しても、他の原則(SOLID原則など)に違反することはありません。理由は次のとおりです。

  • そのクラスには、明確に定義された単一の責任があり、したがって、単一責任原則に従います。
  • システムに新しいバリデータを追加するには、コードを1行も変更せずに行うことができるため、Open / Closed Principleに従います。
  • このクラスが実装するIValidator <T>インターフェイスは狭く(メンバーが1つのみ)、インターフェイス分離の原則に従います。
  • 消費者はそのIValidator <T>インターフェースのみに依存するため、依存関係反転の原則に従います。

CreateBookCommandValidatorクラスがライブラリの外部から直接消費されない場合にのみ内部を作成できますが、ユニットテストはこのクラス(およびシステム内のほぼすべてのクラス)の重要なコンシューマであるため、ほとんどそうではありません。

クラスを内部にし、[InternalsVisibleTo]を使用して単体テストプロジェクトがプロジェクトの内部にアクセスできるようにすることができますが、なぜ面倒なのでしょうか?

クラスを内部にする最も重要な理由は、外部の関係者(あなたが制御できない)がそのようなクラスに依存するのを防ぐためです。言い換えると、これは、再利用可能なライブラリ(依存関係注入ライブラリなど)を作成している場合にのみ有効です。実際、Simple Injectorには内部のものが含まれており、単体テストプロジェクトはこれらの内部をテストします。

ただし、再利用可能なプロジェクトを作成していない場合、この問題は存在しません。依存しているプロジェクトを変更でき、チームの他の開発者はガイドラインに従う必要があるため、存在しません。そして、1つの簡単なガイドラインがあります。抽象化へのプログラム。実装ではありません(依存関係反転の原理)。

要するに、再利用可能なライブラリを作成しているのでなければ、このクラスを内部にしないでください。

ただし、このクラスを内部にしたい場合は、次のように問題なくSimple Injectorに登録できます。

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    AccessibilityOption.AllTypes,
    container.RegisterAll,
    typeof(IValidate<>).Assembly);

確認する唯一のことは、それらが内部であっても、すべてのバリデーターがパブリックコンストラクターを持っていることです。型に内部コンストラクターが必要な場合(その理由がわからない場合)、Constructor Resolution Behaviorをオーバーライドできます。

更新

Simple Injector v2.6以降のデフォルトの動作はRegisterManyForOpenGeneric、パブリック型と内部型の両方を登録することです。そのため、供給AccessibilityOption.AllTypesは冗長になり、次のステートメントはパブリックタイプと内部タイプの両方を登録します。

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    container.RegisterAll,
    typeof(IValidate<>).Assembly);

8

CreateBookCommandValidatorクラスが公開されていることは大したことではありません。

それを定義するライブラリの外部でインスタンスを作成する必要がある場合、パブリッククラスを公開し、そのクラスをの実装としてのみ使用するクライアントに頼るのはかなり自然な方法ですIValidate<CreateBookCommand>。(単に型を公開することは、カプセル化が壊れていることを意味するのではなく、クライアントがカプセル化を破るのを少し簡単にするだけです)。

それ以外の場合、本当にクラスをクライアントに知らないようにしたい場合は、クラスを公開する代わりにpublic static factoryメソッドを使用することもできます。例:

public static class Validators
{
    public static IValidate<CreateBookCommand> NewCreateBookCommandValidator()
    {
        return new CreateBookCommnadValidator();
    }
}

DIコンテナーへの登録に関しては、私が知っているすべてのDIコンテナーは、静的ファクトリーメソッドを使用して構築できます。


はい、ありがとうございます。この投稿を作成する前は、もともと同じことを考えていました。適切なIValidate <>実装を返すファクトリクラスを作成することを考えていましたが、IValidate <>実装のいずれかに依存関係がある場合、おそらくすぐに毛むくじゃらになるでしょう。
エヴァンラーセン

@EvanLarsenなぜですか?IValidate<>実装に依存関係がある場合は、これらの依存関係をパラメーターとしてファクトリメソッドに設定します。
ジョミナル


4

それを内部にして、InternalVisibleToAttribute msdn.linkを使用して、テストフレームワーク/プロジェクトがアクセスできるようにすることができます。

関連する問題-> リンクがありました

以下は、問題に関する別のStackoverflow質問へのリンクです。

そして最後にウェブ上の記事


私はその属性の存在について全く知りませんでした。ありがとう
エヴァンラーセン

1

別のオプションは、公開するが別のア​​センブリに配置することです。

基本的に、サービスインターフェースアセンブリ、サービス実装アセンブリ(サービスインターフェースを参照)、サービスコンシューマアセンブリ(サービスインターフェースを参照)、およびIOCレジストラーアセンブリ(サービスインターフェースとサービス実装の両方を参照してそれらを結び付ける)があります。 )。

強調する必要があります、これは常に最も適切な解決策ではありませんが、検討する価値のあるものです。


これにより、内部を可視化するというセキュリティ上のリスクを取り除くことができますか?
ヨハネス14年

1
@ヨハネス:セキュリティリスク?セキュリティを提供するためにアクセス修飾子に依存している場合、心配する必要があります。リフレクションを介して任意のメソッドにアクセスできます。しかし、参照されていない別のアセンブリに実装を配置することにより、内部への簡単/奨励されたアクセスを削除します。
pdr 14年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.