Ostatnio odseparowaliśmy od siebie implementację poszczególnych importerów. Dzisiaj spróbujemy uprościć rejestrowanie klas i przenieść logikę wyboru danych i importera, bezpośrednio do procesu importu. Aby w klasie ImportProcess uzyskać dostęp do wszystkich importerów, użyjemy interfejsu IIndex. Następnie wybierzemy odpowiedni importer i użyjemy delegata Func. W ten sposób opóźnimy tworzenie instancji importera i będziemy w stanie przekazać dane do jego konstruktora.
Diagram poglądowy

Diagram jest taki sam jak w poprzednim poście.
IIndex
Każdy importer zarejestrowaliśmy pod indywidualnym kluczem. Następnie używając interfejsu IIndex, możemy spróbować wstrzyknąć wszystkie impertery w konstruktorze i po kluczu wybrać tylko ten, który nas interesuje.
Zaczynamy od usunięcia ręcznego tworzenia instancji klasy ImportProcess. Podczas rejestracji klasy w pliku Program.cs zmieniamy
builder.Register(c => new ImportProcess(c.Resolve<AppConfiguration>(), c.ResolveKeyed<IImport>(c.Resolve<AppConfiguration>().Config.ImportType))).As<IImportProcess>() .InstancePerDependency();
na
builder.RegisterType<ImportProcess>().As<IImportProcess>().InstancePerDependency();
Przechodzimy do wprowadzenia zmian w klasie ImportProcess. Do konstruktora wstrzykujemy wszystkie nasze importery, czyli klasy, które implementują interfejs IImport. Następnie w konstruktorze wybieramy ten importer, który został wskazany w konfiguracji. Warto dodać, że instancja klasy importera tworzona jest dopiero, kiedy pobierzemy go ze słownika imports (linia 9). Nie tworzone są więc obiekty, które i tak nie zostaną wykorzystane.
public class ImportProcess : IImportProcess
{
private readonly AppConfiguration _config;
private readonly IImport _import;
public ImportProcess(AppConfiguration config, IIndex<ImportType, IImport> imports)
{
_config = config;
_import = imports[config.Config.ImportType];
}
public IEnumerable<string> DoImport()
{
if (_config?.Config?.Books is null || !_config.Config.Books.Any())
{
return new List<string> { "No data to import!" };
}
List<string> resultList = _import.Import(_config.Config.Books);
return resultList;
}
}
Testy
Aby uruchomić testy musimy jedynie zmodyfikować rejestrację klasy ImportProcess. Rejestracja powinna wyglądać tak samo jak w pliku Program.cs.
private IContainer BuildContainer(string jsonData)
{
var loadDataMock = new Mock<ILoadData>();
loadDataMock.Setup(c => c.ReadData(string.Empty)).Returns(jsonData);
var builder = new ContainerBuilder();
builder.RegisterInstance(loadDataMock.Object).As<ILoadData>();
builder.RegisterType<ImportInformBookstore>().Keyed<IImport>(ImportType.InformBookstore);
builder.RegisterType<ImportSaveInDb>().Keyed<IImport>(ImportType.SaveInDb);
builder.RegisterType<ImportSendToBackOfficeSystem>().Keyed<IImport>(ImportType.SendToBackOfficeSystem);
builder.RegisterType<ImportProcess>().As<IImportProcess>().InstancePerDependency();
builder.RegisterType<AppConfiguration>()
.WithParameter(new TypedParameter(typeof(string), string.Empty))
.SingleInstance();
return builder.Build();
}
Func
Jako że importery nie mogą istnieć bez listy książek, to spróbujemy przekazać je w konstruktorze. Zmiany będą polegały na usunięciu z metody Import parametru books i przeniesieniu go do konstruktora.
Zaczynamy od zmian w interfejsie IImport
public interface IImport
{
List<string> Import(List<Book> books);
}
// zmieniamy na
public interface IImport
{
List<string> Import();
}
Następnie w każdym importerze dodajemy konstruktor z listą książek oraz z metody Import usuwamy parametr books. Poniżej prezentuję tylko jeden z trzech importerów, resztę znajdziecie na GitHub.
Przed zmianą
public class ImportInformBookstore : IImport
{
public List<string> Import(List<Book> books)
{
var resultList = new List<string> { "The bookstore will be informed of the following books:" };
foreach (var book in books)
{
resultList.Add(book?.Title);
}
return resultList;
}
}
Po zmianie
public class ImportInformBookstore : IImport
{
private readonly List<Book> _books;
public ImportInformBookstore(List<Book> books)
{
_books = books;
}
public List<string> Import()
{
var resultList = new List<string> { "The bookstore will be informed of the following books:" };
foreach (var book in _books)
{
resultList.Add(book?.Title);
}
return resultList;
}
}
Na koniec zostaje już tylko zrobić zmiany w klasie ImportProcess. Dzięki użyciu delegata Func opóźniamy tworzenie instancji importera i uzyskujemy możliwość przekazania listy książek do konstruktora.
Przed zmianą
public class ImportProcess : IImportProcess
{
private readonly AppConfiguration _config;
private readonly IImport _import;
public ImportProcess(AppConfiguration config, IIndex<ImportType, IImport> imports)
{
_config = config;
_import = imports[config.Config.ImportType];
}
public IEnumerable<string> DoImport()
{
if (_config?.Config?.Books is null || !_config.Config.Books.Any())
{
return new List<string> { "No data to import!" };
}
List<string> resultList = _import.Import(_config.Config.Books);
return resultList;
}
}
Po zmianie
public class ImportProcess : IImportProcess
{
private readonly AppConfiguration _config;
private readonly Func<List<Book>, IImport> _imports;
public ImportProcess(AppConfiguration config, IIndex<ImportType, Func<List<Book>, IImport>> imports)
{
_config = config;
_imports = imports[config.Config.ImportType];
}
public IEnumerable<string> DoImport()
{
if (_config?.Config?.Books is null || !_config.Config.Books.Any())
{
return new List<string> { "No data to import!" };
}
List<string> resultList = _imports(_config.Config.Books).Import();
return resultList;
}
}
Testy
W przypadku zmian związanych z wprowadzeniem delegata Func nie musimy robić dodatkowych zmian w testach. Wystarczą te, które zrobiliśmy podczas wprowadzania IIndex.
Podsumowanie
Uprościliśmy sposób rejestracji klasy ImportProcess i przenieśliśmy całą logikę wyboru danych i importera, bezpośrednio do procesu importu. Do wstrzyknięcia importerów użyliśmy interfejsu IIndex. Zmodyfikowaliśmy również importery, dodając konieczność przekazania listy książek bezpośrednio do konstruktora. Użyliśmy delegata Func, aby opóźnić tworzenie instancji importera i ręcznie przekazać dane do konstruktora.
Kod aplikacji dostępny jest na GitHub.
Linki:
Resolving with an Index
Keyed Service Lookup (IIndex
Dynamic Instantiation (Func)
