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
- 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. - RepositoryLocator è hardcoded per il runtime di produzione. Non credo che harcoding sia un peccato capitale come fanno molti ingegneri, ma lo considero con sospetto.
-
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.
-
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
?