Czysta architektura cz.2 – Kod biznesowy

Jako że post teoretyczny był oznaczony jako część pierwsza, wypadało by napisać cześć drugą. Tym razem nie będzie już teoretyzowania. Będzie za to kod i odrobinę objaśnień. Zapraszam!

Fragmenty kodu zostały wyciągnięte z mojej małej aplikacji narzędziowej https://bitbucket.org/bartoszgolek/referencechecker. Napisałem ją kiedyś w celu testowania i wyłapywania niepożądanych referencji pomiędzy bibliotekami.

Ponieważ w poprzednim poście, napisałem że najważniejszym elementem systemu są interaktory (logika biznesowa), zacznę od jednego z mojej aplikacji, a następnie “dobuduję” do niego peryferia.

Na początek trochę kodu interaktora:
Interfejs:

public interface IAnalyzeLibrariesInteractor
{
	void Run(IAnalyzeLibrariesRequestData requestData);
}

Jak widać dane potrzebne do wykonania polecenia przekazujemy przez interfejs:

public interface IAnalyzeLibrariesRequestData
{
	string AssembliesPath { get; set; }
}

Po podejrzeniu interfejsu IAnalyzeLibrariesRequestData, możemy od razu stwierdzić, że nasz interaktor analizuje biblioteki znajdujące się w wybranym katalogu.
Zwrócić należy też uwagę, że jedyna metoda naszego IAnalyzeLibrariesInteractor jest typu void. Jest tak, ponieważ w systemie posługujemy się prezenterami do prezentacji wyniku. Interfejs interaktora w żaden sposób nie mówi nam w jaki sposób zostanie zaprezentowany wynik obliczeń. I BARDZO DOBRZE!!!. O tym jak zaprezentujemy wynik decyduje implementacja!

Przyjrzyjmy się jej:

internal class AnalyzeLibrariesInteractor: IAnalyzeLibrariesInteractor
{
	public AnalyzeLibrariesInteractor(
		IAnalyzeLibrariesInteractorDao dao,
		IAnalyzeLibrariesInteractorPresenter presenter)
	{
		this.dao = dao;
		this.presenter = presenter;
	}

	public void Run(IAnalyzeLibrariesRequestData requestData)
	{
		if (requestData.AssembliesPath == null)
			presenter.ShowFailedMessage("Nie wybrano ścieżki z bibliotekami.");

		try
		{
			IGraph graphData = AnalyzeAssemblies(requestData.AssembliesPath);
			if (!graphData.Vertices.Any())
				presenter.ShowFailedMessage("Brak danych do wyświetlenia!");
			else
				presenter.ShowGraph(graphData);
		}
		catch (Exception e)
		{
			presenter.ShowFailedMessage(e.Message);
		}
	}

	private IGraph AnalyzeAssemblies(string assembliesPath)
	{
		return new AssembliesAnalyzer(
			dao.GetAssemblies(assembliesPath),
			dao.GetAllowedRules(),
			dao.GetOmmitRules(),
			dao.GetWarningRules()
		).Analyze();
	}

	/*...*/

	private readonly IAnalyzeLibrariesInteractorDao dao;
	private readonly IAnalyzeLibrariesInteractorPresenter presenter;
}

Z powyższej implementacji widać, że Interaktor realizuje zadanie analizy. Przy wykorzystaniu zewnętrznych reguł (dostarczanych przez dao), budowany jest graf zależności IGraph. Nasz interaktor, oczekuje również prezentera potrafiącego powstały graf wyświetlić.

Przyjrzyjmy się zatem naszemu Dao.
Definicja danych potrzebnych implementacji interaktora:

public interface IAnalyzeLibrariesInteractorDao
{
	IEnumerable GetWarningRules();
	IEnumerable GetAllowedRules();
	IEnumerable GetOmmitRules();
	IEnumerable GetAssemblies(string assembliesPath);
}

Oraz realizacja:

public class AnalyzeLibrariesInteractorDao : IAnalyzeLibrariesInteractorDao
	{
		public AnalyzeLibrariesInteractorDao(IConfigDao configDao, IAssembliesDao assembliesDao)
		{
			this.configDao = configDao;
			this.assembliesDao = assembliesDao;
		}

		public IEnumerable GetWarningRules()
		{
			return configDao.GetWarningRules();
		}

		public IEnumerable GetAllowedRules()
		{
			return configDao.GetAllowedRules();
		}

		public IEnumerable GetOmmitRules()
		{
			return configDao.GetOmmitRules();
		}

		public IEnumerable GetAssemblies(string path)
		{
			return assembliesDao.GetAssemblies(path);
		}

		private readonly IConfigDao configDao;
		private readonly IAssembliesDao assembliesDao;
	}

Od razu widać, że AnalyzeLibrariesInteractorDao jest adapterem, który pośredniczy w komunikacji pomiędzy logiką biznesową (i przez nią jest definiowany), a różnymi źródłami danych (pliki bibliotek zapisane w katalogu, konfiguracja).

To samo możemy zaobserwować po stronie prezentacji:

public interface IAnalyzeLibrariesInteractorPresenter
{
	void ShowGraph(IGraph graphData);
	void ShowFailedMessage(string message);
}

Interfejs informuje, co interaktor będzie chciał prezentować (graf lub komunikat błędu). Natomiast implementacja decyduje jak to zrobić:

internal class AnalyzeLibrariesInteractorPresenter: IAnalyzeLibrariesInteractorPresenter
{
	private readonly Func mainWindowPresenterProvider;
	private readonly Func errorMessagePresenterProvider;

	public AnalyzeLibrariesInteractorPresenter(
		Func mainWindowPresenterProvider,
		Func errorMessagePresenterProvider)
	{
		this.mainWindowPresenterProvider = mainWindowPresenterProvider;
		this.errorMessagePresenterProvider = errorMessagePresenterProvider;
	}

	public void ShowGraph(IGraph graphData)
	{
		IMainWindowPresenter mainWindowPresenter = GetMainWindowPresenter();
		mainWindowPresenter.GraphData = graphData;
		mainWindowPresenter.Run();
	}

	public void ShowFailedMessage(string message)
	{
		IErrorMessagePresenter errorMessagePresenter = GetErrorMessagePresenter();
		errorMessagePresenter.Message = message;
		errorMessagePresenter.Run();
	}

	private IMainWindowPresenter GetMainWindowPresenter()
	{
		return mainWindowPresenterProvider.Invoke();
	}

	private IErrorMessagePresenter GetErrorMessagePresenter()
	{
		return errorMessagePresenterProvider.Invoke();
	}
}

I znowu, implementacja jest tylko adapterem, który “opóźnia” instancjonowanie prezenterów oraz decyduje o tym, przy pomocy jakich widoków/okien zostaną one zaprezentowane.
W naszym przypadku (aplikacja okienkowa) do wyświetlenia komunikatu zostanie użyty MessageBox, a do wyświetlenia grafu okno główne.

Wprowadzenie takiego, a nie innego podziału na interfejsy i implementacje nie jest przypadkowe. Dzięki temu możemy aplikację podzielić na wygodne warstwy nie posiadające cykli pomiędzy bibliotekami, a także w łatwy sposób wymieniać komponenty aplikacji:
Architektura klas

Na teraz to tyle. Zapraszam na cześć 3: Kod prezentacji. Pokażę, w jaki sposób przy pomocy Autofac poskładać wszystko razem, oraz jak bezboleśnie rozwiązać problem odświeżania widoków.

Comments:0

Leave a Reply

Your email address will not be published. Required fields are marked *