我可以在不破坏封装的情况下使用依赖注入吗?


15

这是我的解决方案和项目:

  • 书店 (解决方案)
    • 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会发生什么?我认为您在这里实际上是在使用Anemic Domain Model反模式。诸如保存之类的事情应该在域之内,而验证则应该是实体的一部分。
欣快感2013年

1
@Euphoric:是的。这不是命令模式。实际上,OP遵循不同的模式:命令/处理程序模式
史蒂文

我希望有很多好的答案,我希望可以将更多答案标记为答案。感谢大家的帮助。
埃文·拉森

@Euphoric,在重新考虑项目布局之后,我认为CommandHandlers应该在Domain中。不知道为什么将它们放在基础结构项目中。谢谢。
埃文·拉森

Answers:


11

使得CreateBookCommandValidator公众不破坏封装,因为

封装用于在类中隐藏结构化数据对象的值或状态,以防止未经授权的方直接访问它们(维基百科

CreateBookCommandValidator不允许访问其数据成员(当前似乎没有任何数据成员),因此不会违反封装。

公开此类不会违反任何其他原则(例如SOLID原则),因为:

  • 该类别具有明确定义的单一职责,因此遵循“单一职责原则”。
  • 无需更改一行代码即可向系统添加新的验证器,因此您可以遵循“开放/封闭原则”。
  • 此类实现的IValidator <T>接口很窄(只有一个成员),并且遵循接口隔离原理。
  • 您的使用者仅依赖于该IValidator <T>接口,因此遵循依赖性反转原则。

CreateBookCommandValidator如果不直接从库外部使用该类,则只能进行内部构造,但这几乎是不可能的,因为单元测试是该类(以及系统中几乎每个类)的重要使用者。

尽管您可以将类设置为内部类,并使用[InternalsVisibleTo]允许单元测试项目访问项目的内部,但为什么要麻烦呢?

将类设置为内部的最重要原因是防止外部方(您无法控制)依赖此类,因为这将阻止您将来更改该类而不会破坏任何内容。换句话说,这仅在创建可重用库(例如依赖项注入库)时成立。实际上,Simple Injector包含内部组件,其单元测试项目会测试这些内部组件。

但是,如果您不创建可重用的项目,则不存在此问题。它不存在,因为您可以更改依赖它的项目,并且团队中的其他开发人员必须遵循您的准则。一个简单的准则就可以做到:编写抽象程序;而不是实现(依赖倒置原则)。

长话短说,除非您正在编写可重用的库,否则请勿将此类内部化。

但是,如果您仍然想让此类成为内部类,则仍然可以在Simple Injector中注册它,而不会出现任何此类问题:

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

唯一要确保的是,即使所有验证器都是内部的,它们也都具有公共构造函数。如果您确实希望类型具有内部构造函数(不知道为什么会这样),则可以覆盖“ 构造函数解析行为”

更新

Simple Injector v2.6开始,默认行为RegisterManyForOpenGeneric是注册公共类型和内部类型。因此,提供AccessibilityOption.AllTypes现在是多余的,下面的语句将注册公共类型和内部类型:

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

8

CreateBookCommandValidator上课是公开的没什么大不了的。

如果您需要在定义它的库之外创建它的实例,这是一种很自然的方法,可以公开公共类,并且仅依靠将该类作为实现的实现来依靠客户端IValidate<CreateBookCommand>。(仅公开一个类型并不意味着封装被破坏,它只是使客户端更容易破坏封装)。

否则,如果您确实要强制客户端不了解该类,则也可以使用公共静态工厂方法来代替公开该类,例如:

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

至于在您的DI容器中进行注册,我所知道的所有DI容器都允许使用静态工厂方法进行构造。


是的,谢谢..在创建本文之前,我本来也想过同样的事情。我正在考虑制作一个Factory类,该类将返回适当的IValidate <>实现,但是如果任何IValidate <>实现具有任何依赖关系,它可能很快就会变得毛茸茸。
伊万·拉尔森

@EvanLarsen为什么?如果IValidate<>实现具有依赖项,则将这些依赖项作为参数添加到工厂方法中。
2013年



1

另一种选择是将其公开,但将其放在另一个程序集中。

因此,从本质上讲,您有一个服务接口程序集,一个服务实现程序集(引用了服务接口),一个服务使用者程序集(引用了服务接口)和一个IOC注册器程序集(引用了服务接口和服务实现,以将它们连接在一起) )。

我要强调,这并不总是最合适的解决方案,但这是一个值得考虑的解决方案。


这样可以消除使内部部件可见的严重安全风险吗?
约翰内斯

1
@Johannes:安全风险?如果您依靠访问修饰符来提高安全性,则需要担心。您可以通过反射访问任何方法。但这确实通过将实现放入另一个未引用的程序集中来消除对内部的轻松/鼓励访问。
pdr 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.