Puis-je utiliser l'injection de dépendance sans interrompre l'encapsulation?

15

Voici ma solution et mes projets:

  • Librairie (solution)
    • BookStore.Coupler (projet)
      • Bootstrapper.cs
    • BookStore.Domain (projet)
      • CreateBookCommandValidator.cs
      • CompositeValidator.cs
      • IValidate.cs
      • IValidator.cs
      • ICommandHandler.cs
    • BookStore.Infrastructure (projet)
      • CreateBookCommandHandler.cs
      • ValidationCommandHandlerDecorator.cs
    • BookStore.Web (projet)
      • Global.asax
    • BookStore.BatchProcesses (projet)
      • 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

Désolé pour la longue configuration du problème, je n'ai pas de meilleur moyen d'expliquer cela que de détailler mon problème réel.

Je ne veux pas créer mon CreateBookCommandValidator public. Je préférerais que ce soit, internalmais si je le fais, internalje ne pourrai pas l'enregistrer avec mon conteneur DI. La raison pour laquelle je voudrais que ce soit interne est parce que le seul projet qui devrait avoir la notion de mes implémentations IValidate <> est dans le projet BookStore.Domain. Tout autre projet doit simplement consommer IValidator <> et le CompositeValidator doit être résolu, ce qui remplira toutes les validations.

Comment puis-je utiliser l'injection de dépendance sans interrompre l'encapsulation? Ou est-ce que je me trompe?

Evan Larsen
la source
Juste un petit mot: ce que vous utilisez n'est pas un modèle de commande correct, donc l'appeler commande peut être une désinformation. En outre, CreateBookCommandHandler semble rompre le LSP: que se passera-t-il, si vous passez un objet, qui dérive de CreateBookCommand? Et je pense que ce que vous faites ici est en fait un contre-modèle de modèle de domaine anémique. Des choses comme l'enregistrement doivent être à l'intérieur du domaine et la validation doit faire partie de l'entité.
euphorique
1
@ Euphoric: C'est exact. Ce n'est pas le modèle de commande . En fait, l'OP suit un modèle différent: le modèle de commande / gestionnaire .
Steven
Il y avait tellement de bonnes réponses que j'aurais aimé avoir marqué plus comme réponse. Merci à tous pour votre aide.
Evan Larsen
@Euphoric, après avoir repensé la disposition du projet, je pense que les CommandHandlers devraient être dans le domaine. Je ne sais pas pourquoi je les ai mis dans le projet d'infrastructure. Merci.
Evan Larsen
EN RELATION
Steven

Réponses:

11

Rendre le CreateBookCommandValidatorpublic ne viole pas l'encapsulation, car

L'encapsulation est utilisée pour masquer les valeurs ou l'état d'un objet de données structuré à l'intérieur d'une classe, empêchant les parties non autorisées d'y accéder directement ( wikipedia )

Votre CreateBookCommandValidatorne permet pas l'accès à ses membres de données (il ne semble pas en avoir actuellement), donc il ne viole pas l'encapsulation.

Rendre cette classe publique ne viole aucun autre principe (comme les principes SOLID ), car:

  • Cette classe a une seule responsabilité bien définie et suit donc le principe de responsabilité unique.
  • L'ajout de nouveaux validateurs au système peut se faire sans changer une seule ligne de code et vous suivez donc le principe ouvert / fermé.
  • Cette interface IValidator <T> que cette classe implémente est étroite (ne comporte qu'un seul membre) et suit le principe de ségrégation d'interface.
  • Vos consommateurs ne dépendent que de cette interface IValidator <T> et suivent donc le principe d'inversion de dépendance.

Vous ne pouvez faire de l' CreateBookCommandValidatorinterne que si la classe n'est pas consommée directement de l'extérieur de la bibliothèque, mais ce n'est presque jamais le cas, car vos tests unitaires sont un consommateur important de cette classe (et presque toutes les classes de votre système).

Bien que vous puissiez rendre la classe interne et utiliser [InternalsVisibleTo] pour permettre au projet de test unitaire d'accéder aux internes de votre projet, pourquoi s'embêter?

La raison la plus importante pour rendre les classes internes est d'empêcher des parties externes (sur lesquelles vous n'avez pas de contrôle) de prendre une dépendance sur une telle classe, car cela vous empêcherait de faire évoluer ultérieurement cette classe sans rien casser. En d'autres termes, cela ne s'applique que lorsque vous créez une bibliothèque réutilisable (telle qu'une bibliothèque d'injection de dépendances). En fait, Simple Injector contient des éléments internes et son projet de test unitaire teste ces éléments internes.

Cependant, si vous ne créez pas de projet réutilisable, ce problème n'existe pas. Il n'existe pas, car vous pouvez modifier les projets qui en dépendent et les autres développeurs de votre équipe devront suivre vos consignes. Et une simple directive suffira: Programmer vers une abstraction; pas une implémentation (le principe d'inversion de dépendance).

Donc, pour faire court, ne faites pas de cette classe interne, sauf si vous écrivez une bibliothèque réutilisable.

Mais si vous voulez toujours rendre cette classe interne, vous pouvez toujours l'enregistrer avec Simple Injector sans aucun problème comme celui-ci:

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

La seule chose à vous assurer est que tous vos validateurs ont un constructeur public, même s'ils sont internes. Si vous voulez vraiment que vos types aient un constructeur interne (je ne sais pas vraiment pourquoi vous le voudriez), vous pouvez remplacer le comportement de résolution du constructeur .

MISE À JOUR

Depuis Simple Injector v2.6 , le comportement par défaut de RegisterManyForOpenGenericest d'enregistrer les types publics et internes. La fourniture AccessibilityOption.AllTypesest donc désormais redondante et la déclaration suivante enregistrera les types publics et internes:

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    container.RegisterAll,
    typeof(IValidate<>).Assembly);
Steven
la source
8

Ce n'est pas grave que la CreateBookCommandValidatorclasse soit publique.

Si vous devez en créer des instances en dehors de la bibliothèque qui la définit, c'est une approche assez naturelle pour exposer la classe publique et compter sur les clients utilisant uniquement cette classe comme implémentation de IValidate<CreateBookCommand>. (Le simple fait d'exposer un type ne signifie pas que l'encapsulation est rompue, cela permet simplement aux clients de rompre l'encapsulation un peu plus facilement).

Sinon, si vous voulez vraiment forcer les clients à ne pas connaître la classe, vous pouvez également utiliser une méthode d'usine statique publique au lieu d'exposer la classe, par exemple:

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

En ce qui concerne l'enregistrement dans votre conteneur DI, tous les conteneurs DI que je connais permettent la construction en utilisant une méthode d'usine statique.

jhominal
la source
Oui, merci .. Au départ, je pensais la même chose avant de créer ce post. Je pensais à créer une classe Factory qui retournerait l'implémentation IValidate <> appropriée, mais si l'une des implémentations IValidate <> avait des dépendances, elle deviendrait probablement assez velue rapidement.
Evan Larsen
@EvanLarsen Pourquoi? Si l' IValidate<>implémentation a des dépendances, placez ces dépendances en tant que paramètres dans la méthode d'usine.
2013
5

Vous pouvez déclarer CreateBookCommandValidatoren internaltant qu'application l' InternalsVisibleToAttribute afin de le rendre visible à l' BookStore.Couplerassembly. Cela aide également souvent lors des tests unitaires.

Olivier Jacot-Descombes
la source
Je n'avais aucune idée de l'existence de cet attribut. Merci
Evan Larsen
4

Vous pouvez le rendre interne et utiliser le msdn.link InternalVisibleToAttribute afin que votre framework / projet de test puisse y accéder.

J'ai eu un problème connexe -> lien .

Voici un lien vers une autre question Stackoverflow concernant le problème:

Et enfin un article sur le web.

Johannes
la source
Je n'avais aucune idée de l'existence de cet attribut. Merci
Evan Larsen
1

Une autre option est de le rendre public mais de le mettre dans une autre assemblée.

Donc, essentiellement, vous avez un assemblage d'interfaces de service, un assemblage d'implémentations de service (qui fait référence aux interfaces de service), un assemblage de consommateur de service (qui fait référence aux interfaces de service) et un assemblage de registraire IOC (qui fait référence à la fois aux interfaces de service et aux implémentations de service pour les connecter ensemble) ).

Je dois souligner que ce n'est pas toujours la solution la plus appropriée, mais elle mérite d'être envisagée.

pdr
la source
Cela éliminerait-il le risque de sécurité minime de rendre les internes visibles?
Johannes
1
@Johannes: Risque de sécurité? Si vous comptez sur des modificateurs d'accès pour vous assurer la sécurité, vous devez vous inquiéter. Vous pouvez accéder à n'importe quelle méthode via la réflexion. Mais il supprime l'accès facile / encouragé aux internes en plaçant l'implémentation dans un autre assembly qui n'est pas référencé.
pdr