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


15

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 installazione del problema, non ho modo migliore di spiegarlo se non dettagliare il mio problema reale.

Non voglio creare il mio CreateBookCommandValidator public. Preferirei essereinternal ma se lo internalfaccio, non sarò in grado di registrarlo con il mio contenitore DI. Il motivo per cui vorrei che fosse interno è perché l'unico progetto che dovrebbe avere nozione delle mie implementazioni IValidate <> è nel progetto BookStore.Domain. Qualsiasi altro progetto deve semplicemente consumare IValidator <> e CompositeValidator deve essere risolto e soddisferà tutte le convalide.

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


Solo un nitpick: quello che stai usando non è un modello di comando corretto, quindi chiamarlo comando potrebbe essere una disinformazione. Inoltre, CreateBookCommandHandler sembra spezzare LSP: cosa succederà, se si passa un oggetto, che deriva da CreateBookCommand? E penso che quello che stai facendo qui sia in realtà un modello di modello di dominio anemico. Cose come il salvataggio dovrebbero essere all'interno del dominio e la convalida dovrebbe far parte dell'entità.
Euforico

1
@Euforico: è corretto. Questo non è il modello di comando . È un dato di fatto, l'OP segue un modello diverso: il modello comando / gestore .
Steven,

C'erano così tante buone risposte che avrei potuto segnare di più come risposta. Grazie a tutti per il vostro aiuto.
Evan Larsen,

@Euforico, dopo aver ripensato il layout del progetto penso che CommandHandlers dovrebbe essere nel Dominio. Non sono sicuro del motivo per cui li ho inseriti nel progetto Infrastruttura. Grazie.
Evan Larsen,

Risposte:


11

Rendere il CreateBookCommandValidatorpubblico non viola l'incapsulamento, da allora

L'incapsulamento viene utilizzato per nascondere i valori o lo stato di un oggetto dati strutturato all'interno di una classe, impedendo l'accesso diretto a parti non autorizzate ( Wikipedia )

Il tuo CreateBookCommandValidatornon consente l'accesso ai suoi membri di dati (al momento non sembra averne alcuno) quindi non viola l'incapsulamento.

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

  • Quella classe ha un'unica responsabilità ben definita e pertanto segue il principio della responsabilità singola.
  • L'aggiunta di nuovi validatori al sistema può essere effettuata senza modificare una singola riga di codice e pertanto si segue il principio aperto / chiuso.
  • Quell'interfaccia IValidator <T> implementata da questa classe è stretta (ha un solo membro) e segue il principio di segregazione dell'interfaccia.
  • I tuoi consumatori dipendono solo da quell'interfaccia IValidator <T> e quindi seguono il principio di inversione di dipendenza.

Puoi solo fare il CreateBookCommandValidator interno solo se la classe non viene consumata direttamente dall'esterno della libreria, ma quasi mai è così, dal momento che i test delle unità sono un consumatore importante di questa classe (e quasi ogni classe nel tuo sistema).

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

Il motivo più importante per rendere le classi interne è quello di impedire alle parti esterne (di cui non si ha il controllo) di dipendere da tale classe, perché ciò impedirebbe di apportare modifiche future a quella classe senza interrompere nulla. In altre parole, questo vale solo quando si crea una libreria riutilizzabile (come una libreria di iniezione di dipendenza). È un dato di fatto, Simple Injector contiene elementi interni e il suo progetto di unit test testa questi interni.

Tuttavia, se non si sta 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à: programmare un'astrazione; non un'implementazione (il principio di inversione di dipendenza).

Per farla breve, non rendere interna questa classe se non stai scrivendo una libreria riutilizzabile.

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

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

L'unica cosa di cui accertarsi è che tutti i validatori abbiano un costruttore pubblico, anche se sono interni. Se vuoi davvero che i tuoi tipi abbiano un costruttore interno (non sai davvero perché lo vorresti) puoi ignorare il comportamento di risoluzione del costruttore .

AGGIORNARE

A partire da Simple Injector v2.6 , il comportamento predefinito di RegisterManyForOpenGenericè di registrare sia i tipi pubblici che quelli interni. Quindi la fornitura 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);

8

Non è un grosso problema che la CreateBookCommandValidatorclasse sia pubblica.

Se è necessario crearne istanze al di fuori della libreria che lo definisce, è un approccio abbastanza naturale esporre la classe pubblica e fare affidamento sui client che utilizzano tale classe solo come implementazione di IValidate<CreateBookCommand>. (La semplice esposizione di un tipo non significa che l'incapsulamento sia interrotto, ma rende solo un po 'più facile per i clienti interrompere l'incapsulamento).

Altrimenti, se vuoi davvero forzare i clienti a non conoscere la classe, puoi anche usare un metodo factory statico pubblico invece di esporre la classe, ad esempio:

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

Per quanto riguarda la registrazione nel tuo contenitore DI, tutti i contenitori DI che conosco consentono la costruzione utilizzando un metodo statico di fabbrica.


Sì, grazie .. Inizialmente stavo pensando la stessa cosa prima di creare questo post. Stavo pensando di creare una classe Factory che avrebbe restituito l'implementazione IValidate <> appropriata, ma se una delle implementazioni IValidate <> avesse delle dipendenze, probabilmente sarebbe diventata piuttosto pelosa rapidamente.
Evan Larsen,

@EvanLarsen Perché? Se l' IValidate<>implementazione ha dipendenze, inserire queste dipendenze come parametri nel metodo factory.
jhominal



1

Un'altra opzione è renderlo pubblico ma inserirlo in un altro assembly.

Quindi, essenzialmente, hai un assembly di interfacce di servizio, un assembly di implementazioni di servizi (che fa riferimento a interfacce di servizio), un assembly di consumatori di servizio (che fa riferimento a interfacce di servizio) e un assembly di registro IOC (che fa riferimento sia a interfacce di servizio che a implementazioni di servizi per collegarli insieme ).

Devo sottolineare che questa non è sempre la soluzione più appropriata, ma vale la pena considerarla.


Ciò eliminerebbe il leggero rischio per la sicurezza di rendere visibili gli interni?
Johannes,

1
@Johannes: rischio per la sicurezza? Se ti affidi ai modificatori di accesso per darti sicurezza, devi preoccuparti. È possibile accedere a qualsiasi metodo tramite la riflessione. Ma rimuove l'accesso facile / incoraggiato agli interni inserendo l'implementazione in un altro assembly a cui non si fa riferimento.
pdr
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.