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)