Iniezione delle dipendenze per una libreria con dipendenze interne

6

Sfondo

Sto lavorando su una libreria di classi a supporto di un sito web. La libreria combina API correlate di diversi fornitori, ognuno dei quali ha le proprie sfumature e oggetti di dominio. Ad esempio, immagina che ci sia un fornitore di carte di credito che espone un oggetto BankAccount , un fornitore di prestiti che espone un Account e un sistema di conti correnti espone un AccountDto . L'obiettivo principale della biblioteca è quello di nascondere queste varie implementazioni e consentire al sito web di lavorare con un singolo concetto di Account , con una serie di servizi che decidono quale fornitore chiamare e come coordinare i dati dalle varie fonti .

Per nascondere la confusione e mantenere l'API pulita, solo i miei oggetti e servizi di dominio sono public . All'interno della libreria, ciascun fornitore ha il proprio repository e gli oggetti del dominio. Le classi del venditore sono tutte contrassegnate con internal per impedire agli sviluppatori web di aggirare il livello dei servizi (intenzionalmente o involontariamente).

Quindi il flusso va in questo modo:

Web Site >> My API >> Service Layer (public) >> Repository Layer (internal)

Questa soluzione utilizza l'iniezione delle dipendenze e il contenitore Unity IoC. I servizi delle mie API sono registrati nella root della composizione e inseriti direttamente nei controller dei siti Web come argomenti del costruttore. Tutto questo funziona bene.

Il problema

Ecco il problema con cui sto lottando. I miei servizi hanno argomenti del costruttore che permettono di iniettare i repository in essi. Ma i repository sono internal quindi il sito web non può registrarli nella composizione root (senza fare qualcosa di veramente bizzarro con Reflection). Inoltre, non voglio che gli sviluppatori di siti Web si preoccupino di iniettarli.

Quindi la domanda è: come faccio ad iniettare i repository interni / privati nel livello di servizio pubblico, mentre aderisco alle convenzioni accettate in un approccio DI?

La mia soluzione proposta

Il modo in cui sto facendo questo adesso è il seguente:

Ho aggiunto un'interfaccia RepositoryLocator astratta e una classe alla libreria. Ha il suo contenitore Unity. Viene lasciato non sigillato in modo che un progetto di test unitario possa derivare la propria versione e sostituire i repository di stub.

public interface IRepositoryLocator
{
    T GetRepository<T>() where T : IRepository;
}

public abstract class BaseRepositoryLocator : IRepositoryLocator
{
    protected readonly UnityContainer _container = new UnityContainer();

    public virtual T GetRepository<T>() where T : IRepository
    {
        return _container.Resolve<T>();
    }
}

Poi ho aggiunto un DefaultRepositoryLocator alla mia libreria che è hardcoded per fornire i repository di runtime di produzione.

public sealed class DefaultRepositoryLocator : BaseRepositoryLocator
{
    public DefaultRepositoryLocator()
    {
        RegisterDefaultTypes();
    }
    private void RegisterDefaultTypes()
    {
        _container.RegisterType<IVendorARepository, VendorARepository>();
        _container.RegisterType<IVendorBRepository, VendorBRepository>();
        _container.RegisterType<IVendorCRepository, VendorCRepository>();
    }
}

Quindi nelle mie classi di servizi, invece di iniettare repository, inserisco il repository locator e estrapolo i repository necessari nel costruttore.

public class AccountService : IAccountService
{
    internal readonly IVendorARepository _vendorA;
    internal readonly IVendorBRepository _vendorB;

    public AccountService(IRepositoryLocator repositoryLocator)
    {
        _vendorA = repositoryLocator.GetRepository<IVendorARepository>();
        _vendorB = repositoryLocator.GetRepository<IVendorBRepository>();
    }
}

L'unica attività del team di sviluppo web

Nella root di composizione del sito Web, è sufficiente registrare il localizzatore di repository predefinito nel contenitore Unity.

container.RegisterType<IRepositoryLocator, DefaultRepositoryLocator>();

Una volta fatto ciò, il contenitore Unity inietta il localizzatore del repository in tutti i servizi che vengono iniettati nei controller e i servizi possono recuperare i repository di cui hanno bisogno.

Test dell'unità

In un progetto di test unitario, l'assemblea test unitaria avrà accesso ai membri interni usando attributo InternalsVisibleTo , e il progetto di test sostituirà il proprio StubRepositoryLocator conforme alle interfacce interne ma ora pubbliche.

Cose che sono fastidiose per questo design

Quindi, tutti voi esperti IoC, che tipo di problemi ho creato? Ecco di cosa mi preoccupo

  1. Ci sono due contenitori Unity, uno nell'applicazione web e uno nel RepositoryLocator della libreria. Ho sentito che dovrebbe essere solo uno, ma ho anche letto che è un anti-pattern per passarlo.
  2. RepositoryLocator è hardcoded per il runtime di produzione. Non credo che harcoding sia un peccato capitale come fanno molti ingegneri, ma lo considero con sospetto.
  3. Il progetto di test unitario avrà bisogno di InternalsVisibleToAttribute aggiunto alla libreria. Questo è sicuramente un odore di codice e mi chiedo come ci comporteremmo se il progetto di test unitario cambi il suo nome o se ce ne fossero molti.

  4. I costruttori dei miei servizi eseguono una sola iniezione per RepositoryLocator , invece di iniezioni separate per i singoli repository. Questo nasconde le singole dipendenze al momento della compilazione.

Esiste un approccio che risolve questi problemi ed è coerente con le best practice IoC, nascondendo allo stesso tempo il livello del repository dietro lo spazio di internal ?

    
posta John Wu 23.05.2017 - 02:46
fonte

2 risposte

2

In the web site's composition root, they just need to register the default repository locator

IMO il sito web non ha nemmeno bisogno di conoscere i tuoi repository. Come hai detto, le classi di repository e vendor sono interne, quindi il tuo sito non dovrebbe preoccuparsi di accedervi o iniettarli da qualche parte. Di nuovo, come hai detto, il ruolo della tua biblioteca è quello di astrarre più fornitori ed esporre un'API più semplice all'utente (che è il sito web nel tuo esempio).

Detto questo, penso che dovresti:

  • Nella tua API, fornisci un metodo di ingresso iniziale, che deve essere eseguito una sola volta dal client affinché la tua libreria funzioni in modo appropriato;
  • All'interno di questo metodo (incluso nella libreria), si configura il contenitore con le classi dei repository dei fornitori (interne alla libreria);
  • Mantieni i fornitori privati e i tuoi servizi pubblici
  • Ecco fatto: il tuo cliente (il sito web) deve preoccuparsi solo di iniettare i tuoi servizi, nient'altro.
  • Facoltativo: è possibile utilizzare il file di configurazione per collegare interfacce e implementazioni

Informazioni sulla tua domanda / preoccupazioni:

  1. Tu (di solito) non hai alcun controllo su ciò che farà il tuo cliente; l'app client potrebbe non iniettare nemmeno le tue classi, potrebbe semplicemente usarle subito; quindi non preoccuparti di ciò, come detto nei commenti;
  2. Il file di configurazione potrebbe essere un approccio migliore per questo, facilmente modificabile per ambienti di produzione o di test;
  3. Per quanto riguarda i test unitari, non vedo alcun problema con questo attributo;
  4. Il mio suggerimento risolve questo
risposta data 22.09.2017 - 13:10
fonte
1

Puoi utilizzare estensione del contenitore . Ciò consente all'assembly che contiene classi interne di configurare le proprie dipendenze piuttosto che richiedere al consumatore di conoscerle e configurarle.

La documentazione lo mostra in questo modo:

IUnityContainer container = new UnityContainer();
container.AddNewExtension<MyCustomExtension>();

Il consumatore aggiunge semplicemente l'estensione e quella classe di estensione gestisce i dettagli.

    
risposta data 23.05.2017 - 03:41
fonte

Leggi altre domande sui tag