Naciśnij “Enter” aby skoczyć do treści

Dependency Injection w aplikacji konsolowej cz.3

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

LoadingMultipleConfig_diagram_cz2
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)