Posso usare l'iniezione di dipendenza senza rompere l'incapsulamento?

14

Ecco la mia soluzione e i miei progetti:

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

Ci scusiamo per la lunga configurazione del problema, non ho modo migliore di spiegare questo oltre a descrivere il mio problema reale.

Non voglio rendere il mio CreateBookCommandValidator public . Preferirei che fosse internal , ma se lo faccio internal allora non sarò in grado di registrarlo con il mio contenitore DI. La ragione per cui vorrei che fosse interna è perché l'unico progetto che dovrebbe avere la nozione del mio IValidate < > le implementazioni sono nel progetto BookStore.Domain. Qualsiasi altro progetto deve solo utilizzare IValidator < > e il CompositeValidator dovrebbe essere risolto che soddisferà tutte le convalide.

Come posso utilizzare Dependency Injection senza interrompere l'incapsulamento? O sto sbagliando tutto questo?

    
posta Evan Larsen 30.12.2013 - 22:50
fonte

5 risposte

11

Rendere pubblico CreateBookCommandValidator non viola l'incapsulamento, poiché

Encapsulation is used to hide the values or state of a structured data object inside a class, preventing unauthorized parties direct access to them (wikipedia)

Il tuo CreateBookCommandValidator non consente l'accesso ai suoi membri dei dati (al momento non sembra che ne abbia nessuno) quindi non incapsula l'incapsulamento.

Rendere pubblica questa classe non viola alcun altro principio (come i principi SOLID ) , perché:

  • Quella classe ha una singola responsabilità ben definita e pertanto segue il Principio di Responsabilità Unica.
  • L'aggiunta di nuovi validatori al sistema può essere eseguita senza modificare una singola riga di codice e quindi segui il principio Open / Closed.
  • Questo IValidator < T > interfaccia che questa classe implementa è stretta (ha un solo membro) e segue il Principio di Segregazione dell'Interfaccia.
  • I tuoi consumatori dipendono solo da quel IValidator < T > interfaccia e quindi seguire il principio di inversione delle dipendenze.

Puoi solo rendere CreateBookCommandValidator interno se la classe non viene consumata direttamente dall'esterno della libreria, ma questo non accade quasi mai, dal momento che i tuoi test unitari sono un consumatore importante di questa classe (e quasi ogni classe in il tuo sistema).

Sebbene sia possibile rendere la classe interna e utilizzare [InternalsVisibleTo] per consentire al progetto di test dell'unità di accedere agli interni del progetto, perché preoccuparsi?

Il motivo più importante per rendere le classi interne è impedire che parti esterne (di cui non si ha il controllo) prendano una dipendenza da tale classe, perché ciò impedirebbe di apportare modifiche future a quella classe senza rompere nulla. In altre parole, ciò vale solo quando si crea una libreria riutilizzabile (come una libreria di dipendenze). In effetti, Simple Injector contiene materiale interno e il suo test di unit test testa questi interni.

Tuttavia, se non stai creando un progetto riutilizzabile, questo problema non esiste. Non esiste, perché puoi cambiare i progetti che dipendono da esso e gli altri sviluppatori del tuo team dovranno seguire le tue linee guida. E una semplice linea guida farà: programma ad un'astrazione; non un'implementazione (il principio di inversione delle dipendenze).

Per farla breve, non rendere questa classe interna a meno che tu non stia scrivendo una libreria riutilizzabile.

Ma se vuoi ancora rendere questa classe interna, puoi comunque registrarla con Simple Injector senza alcun problema come questo:

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

L'unica cosa da accertare è che tutti i validatori hanno un costruttore pubblico, anche se sono interni. Se vuoi davvero che i tuoi tipi abbiano un costruttore interno (non sai davvero perché lo vorrai) puoi ignorare Constructor Resolution Behavior .

Aggiorna

Poiché Simple Injector v2.6 , il comportamento predefinito di RegisterManyForOpenGeneric è di registrare sia pubblico che interno tipi. Quindi fornire AccessibilityOption.AllTypes è ora ridondante e la seguente dichiarazione registrerà sia i tipi pubblici che quelli interni:

container.RegisterManyForOpenGeneric(typeof(IValidate<>),
    container.RegisterAll,
    typeof(IValidate<>).Assembly);
    
risposta data 31.12.2013 - 13:02
fonte
8

Non è un grosso problema che la classe CreateBookCommandValidator sia pubblica.

Se è necessario crearne delle istanze al di fuori della libreria che lo definisce, è un approccio abbastanza naturale per esporre la classe pubblica e contare sui client che utilizzano tale classe solo come un'implementazione di IValidate<CreateBookCommand> . (Esponendo semplicemente un tipo non significa che l'incapsulamento è rotto, rende solo un po 'più facile ai client interrompere l'incapsulamento).

Altrimenti, se vuoi davvero forzare i client a non conoscere la classe, puoi anche utilizzare un metodo statico pubblico anziché esporre la classe, ad esempio:

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

Come per la registrazione nel tuo contenitore DI, tutti i contenitori DI che conosco consentono la costruzione utilizzando un metodo di produzione statico.

    
risposta data 30.12.2013 - 23:49
fonte
5

Puoi dichiarare CreateBookCommandValidator come internal e applicare InternalsVisibleToAttribute per renderlo visibile all'assembly BookStore.Coupler . Questo spesso aiuta anche nel fare test di unità.

    
risposta data 30.12.2013 - 23:49
fonte
4

Potresti renderlo interno e utilizzare InternalVisibleToAttribute msdn.link in modo che il tuo framework / progetto di testing possa accedervi.

Ho avuto un problema correlato - > link .

Ecco un link a un'altra domanda StackOverflow riguardante il problema:

E infine un articolo sul Web.

    
risposta data 31.12.2013 - 03:55
fonte
1

Un'altra opzione è renderla pubblica ma inserirla in un altro assembly.

Quindi, in sostanza, si dispone di un assembly di interfacce di servizio, un assembly di implementazioni del servizio (che fa riferimento alle interfacce di servizio), un assembly del consumer del servizio (che fa riferimento alle interfacce di servizio) e un assembly del registrar IOC (che fa riferimento sia alle interfacce di servizio che alle implementazioni del servizio) agganciarli insieme.

Dovrei sottolineare, questa non è sempre la soluzione più appropriata, ma ne vale la pena.

    
risposta data 01.01.2014 - 03:46
fonte

Leggi altre domande sui tag