Czysta architektura cz.3 – Przykładowa aplikacja

Trochę czasu upłynęło od poprzedniego wpisu. Brak czasu.

Tym razem 3, ostatnia, część wpisów o czystej architekturze. Skupię się tutaj na objaśnieniach do przykładowej aplikacji, którą napisałem jako “demonstrator technologii”.
Aplikacja składa się z dwóch solucji:
1. Interactors.Common – Bazowe biblioteki do używania w projektach opartych na Autofac i implementujących ideę czystej architektury.
2. Interactors.Example – Przykładowa “nicnierobiąca” aplikacja implementująca ideę czystej architektury.

Dodatkowo, w poniższym wpisie, pokażę w jaki sposób rozwiązałem problem komunikacji pomiędzy prezenterami i odświeżanie widoków (pracujących na oddzielnych DTO, pochodzących z tych samych danych).

Na początek diagram powiązań pomiędzy bibliotekami:
dllki

Widać na nim, że wszystkie referencje z wyjątkiem jednej (czerwonej) prowadzą do kontraktu. I tak ma być!
Ta jedna referencja to kod budujący kontener.

Całość kodu zgodna jest z założeniami poprzednich wpisów. I tu pojawia się problem. Jaki? Już wyjaśniam.

Wykonanie dowolnej operacji w aplikacji odbywa się wg schematu:
View.Click -> FormControllerAdapter.Action -> InteractorController.Run -> Interactor.Run -> InteractorPresenter.ShowAction -> Presenter.Show -> View.Show

A naszym kodzie może to być np. Dodanie dokumentu:

MainForm (View):


	public partial class MainForm : Form, IMainWindowView
	{
		private readonly MainWindowController controller;

		private MainForm()
		{
			InitializeComponent();
		}

		public MainForm(MainWindowController controller)
			: this()
		{
			this.controller = controller;
		}

		private void newDocumentButton_Click(object sender, EventArgs e)
		{
			controller.CreateNewDocument();
		}
	}

MainWindowController (ControllerAdapter):


	public class MainWindowController
	{
		private readonly ICreateNewDocumentController createNewDocumentController;

		public MainWindowController(
			ICreateNewDocumentController createNewDocumentController)
		{
			this.createNewDocumentController = createNewDocumentController;
		}

		public void CreateNewDocument()
		{
			createNewDocumentController.Run();
		}
	}

CreateNewDocumentController (InteractorController):


	internal class CreateNewDocumentController : ICreateNewDocumentController
	{
		private readonly ICreateNewDocumentInteractor interactor;

		public CreateNewDocumentController(ICreateNewDocumentInteractor interactor)
		{
			this.interactor = interactor;
		}

		public void Run()
		{
			var createNewDocumentRequestData = new CreateNewDocumentRequestData();
			interactor.Run(createNewDocumentRequestData);
		}
	}

CreateNewDocumentInteractor (Interactor):


	internal class CreateNewDocumentInteractor : ICreateNewDocumentInteractor
	{
		private readonly ICreateNewDocumentInteractorDao dao;
		private readonly ICreateNewDocumentPresenter presenter;

		public CreateNewDocumentInteractor(
			ICreateNewDocumentInteractorDao dao,
			ICreateNewDocumentPresenter presenter)
		{
			this.dao = dao;
			this.presenter = presenter;
		}

		public void Run(ICreateNewDocumentRequestData data)
		{
			try
			{
				IDocument document = dao.CreateDocument();
				presenter.ShowDocument(document);
			}
			catch (Exception e)
			{
				presenter.ShowDocumentFailMessage(e.Message);
			}
		}
	}

CreateNewDocumentPresenter (InteractorPresenter):


	internal class CreateNewDocumentPresenter : ICreateNewDocumentPresenter
	{
		private readonly Func<IErrorMessagePresenter> errorMessagePresenterProvider;
		private readonly Func<ICreateDocumentPresenter> documentPresenterProvider;

		public CreateNewDocumentPresenter(
			Func<IErrorMessagePresenter> errorMessagePresenterProvider,
			Func<ICreateDocumentPresenter> documentPresenterProvider
			)
		{
			this.errorMessagePresenterProvider = errorMessagePresenterProvider;
			this.documentPresenterProvider = documentPresenterProvider;
		}

		public void ShowDocument(IDocument document)
		{
			var documentViewModel = new DocumentViewModel
				{
					DocumentNumber = document.DocumentNumber
				};
			var documentPresenter = documentPresenterProvider();
			documentPresenter.SetDocument(documentViewModel);
			documentPresenter.Show();
		}

		public void ShowDocumentFailMessage(string message)
		{
			var errorMessagePresenter = errorMessagePresenterProvider();
			errorMessagePresenter.SetMessage(message);
			errorMessagePresenter.Show();
		}
	}

CreateDocumentPresenter (Presenter.Show):


	internal class CreateDocumentPresenter : ICreateDocumentPresenter
	{
		private readonly ICreateDocumentView view;

		public CreateDocumentPresenter(ICreateDocumentView view)
		{
			this.view = view;
		}

		public void Show()
		{
			view.Show();
		}

		public void SetDocument(IDocumentViewModel document)
		{
			view.Document = document;
		}
	}

DocumentForm (View):

	public partial class DocumentForm : Form, ICreateDocumentView, IEditDocumentView
	{
		private IDocumentViewModel document;

		private DocumentForm()
		{
			InitializeComponent();
		}

		public DocumentForm(IDocumentWindowController controller)
			: this()
		{
			this.controller = controller;
		}

		private readonly IDocumentWindowController controller;

		public IDocumentViewModel Document
		{
			get
			{
				return document;
			}
			set
			{
				document = value;
				documentNumberTextBox.DataBindings.Add(
					new Binding("Text", document, "DocumentNumber")
				);
			}
		}

		private void closeButton_Click(object sender, System.EventArgs e)
		{
			Close();
		}

		private void saveButton_Click(object sender, System.EventArgs e)
		{
			controller.SaveDocument(document);
			Close();
		}
	}

Powyższy kod powoduje wyświetlenie formy w której możemy podać dane nowego dokumentu.
Następnie należy taki dokument zapisać.

W tym celu ponownie wywołujemy Controller, Interactor itd.

DocumentForm (View):


	public partial class DocumentForm : Form, ICreateDocumentView
	{
		private IDocumentViewModel document;

		private DocumentForm()
		{
			InitializeComponent();
		}

		public DocumentForm(IDocumentWindowController controller)
			: this()
		{
			this.controller = controller;
		}

		private readonly IDocumentWindowController controller;

		public IDocumentViewModel Document
		{
			get
			{
				return document;
			}
			set
			{
				document = value;
				documentNumberTextBox.DataBindings.Add(
					new Binding("Text", document, "DocumentNumber")
				);
			}
		}

		private void closeButton_Click(object sender, System.EventArgs e)
		{
			Close();
		}

		private void saveButton_Click(object sender, System.EventArgs e)
		{
			controller.SaveDocument(document);
			Close();
		}
	}

DocumentWindowController (ControllerAdapter):


	public class CreateDocumentWindowController : IDocumentWindowController
	{
		private readonly ISaveNewDocumentController<IDocumentViewModel> saveNewDocumentController;

		public CreateDocumentWindowController(ISaveNewDocumentController<IDocumentViewModel> saveNewDocumentController)
		{
			this.saveNewDocumentController = saveNewDocumentController;
		}

		public void SaveDocument(IDocumentViewModel document)
		{
			saveNewDocumentController.Run(document);
		}
	}

SaveNewDocumentController (InteractorController):


	internal class SaveNewDocumentController : ISaveNewDocumentController<IDocumentViewModel>
	{
		private readonly ISaveNewDocumentInteractor interactor;

		public SaveNewDocumentController(ISaveNewDocumentInteractor interactor)
		{
			this.interactor = interactor;
		}

		public void Run(IDocumentViewModel document)
		{
			var saveNewDocumentRequestData = new SaveNewDocumentRequestData
				{
					DocumentNumber = new DocumentNumber(document.DocumentNumber)
				};
			interactor.Run(saveNewDocumentRequestData);
		}
	}

SaveNewDocumentInteractor (Interactor):


	internal class SaveNewDocumentInteractor : ISaveNewDocumentInteractor
	{
		private readonly ISaveNewDocumentInteractorDao dao;
		private readonly ISaveNewDocumentPresenter presenter;

		public SaveNewDocumentInteractor(
			ISaveNewDocumentInteractorDao dao,
			ISaveNewDocumentPresenter presenter)
		{
			this.dao = dao;
			this.presenter = presenter;
		}

		public void Run(ISaveNewDocumentRequestData data)
		{
			try
			{
				var document = dao.CreateDocument();
				document.DocumentNumber = data.DocumentNumber;
				dao.Save(document);

				presenter.ShowDocumentSucceedMessage("Document created", document);
			}
			catch (Exception e)
			{
				presenter.ShowDocumentFailMessage(e.Message);
			}
		}
	}

SaveNewDocumentPresenter (InteractorPresenter):


	internal class SaveNewDocumentPresenter : ISaveNewDocumentPresenter
	{
		private readonly Func<IErrorMessagePresenter> errorMessagePresenter;
		private readonly Func<ISucceedMessagePresenter> succeedMessagePresenter;

		public SaveNewDocumentPresenter(
			Func<IErrorMessagePresenter> errorMessagePresenter,
			Func<ISucceedMessagePresenter> succeedMessagePresenter
			)
		{
			this.errorMessagePresenter = errorMessagePresenter;
			this.succeedMessagePresenter = succeedMessagePresenter;
		}

		public void ShowDocumentSucceedMessage(string message, IDocument document)
		{
			ISucceedMessagePresenter messagePresenter = succeedMessagePresenter();
			messagePresenter.SetMessage(message);
			messagePresenter.Show();
			var documentViewModel = new DocumentViewModel
			{
				DocumentId = document.DocumentId,
				DocumentNumber = document.DocumentNumber
			};
		}

		public void ShowDocumentFailMessage(string message)
		{
			IErrorMessagePresenter messagePresenter = errorMessagePresenter();
			messagePresenter.SetMessage(message);
			messagePresenter.Show();
		}
	}

SucceedMessagePresenter (Presenter.Show):


	internal class SucceedMessagePresenter : ISucceedMessagePresenter
	{
		private readonly ISucceedMessageView succeedMessageView;

		public SucceedMessagePresenter(ISucceedMessageView succeedMessageView)
		{
			this.succeedMessageView = succeedMessageView;
		}

		public void Show()
		{
			succeedMessageView.Show();
		}

		public void SetMessage(string message)
		{
			succeedMessageView.SetMessage(message);
		}

		public void SetCaption(string caption)
		{
			succeedMessageView.SetCaption(caption);
		}
	}

SucceedMessageForm (View):


	internal class SucceedMessageForm : ISucceedMessageView
	{
		private string message;
		private string caption = "Sukces";

		public void SetMessage(string value)
		{
			message = value;
		}

		public void SetCaption(string value)
		{
			caption = value;
		}

		public void Show()
		{
			MessageBox.Show(message, caption, MessageBoxButtons.OK, MessageBoxIcon.Information);
		}
	}

Widać, że po utworzeniu dokumentu, w żaden sposób do formy głównej nie wraca informacja, że utworzenie dokumentu powiodło się lub nie. Nie wiadomo czy należy ją odświeżyć, czy nie. W tym celu w solucji Interactors.Common, powstał interfejs IMessageManager.

Wygląda on tak:


	public interface IMessageManager<in T> where T : IMessage
	{
		void Raise(T message);
	}

I jest zaimplementowany przez:


	public sealed class MessageManager<T> : IMessageManager<T> where T : IMessage
	{
		private readonly Func<IEnumerable<IMessageReceiver<T>>> handlers;

		public MessageManager(Func<IEnumerable<IMessageReceiver<T>>> handlers)
		{
			this.handlers = handlers;
		}

		public void Raise(T message)
		{
			foreach (IMessageReceiver<T> handler in handlers())
				handler.Handle(message);
		}
	}

W jej implementacji pojawia się: IMessageReceiver


	public interface IMessageReceiver<T> where T : IMessage
	{
		void Handle(T message);

		event EventHandler<MessageEventArgs<T>> MessageReived;
	}

	public class MessageEventArgs<T> : EventArgs
	{
		private readonly T message;

		public MessageEventArgs(T message)
		{
			this.message = message;
		}

		public T Message
		{
			get
			{
				return message;
			}
		}
	}

W kontenerze rejestrowane są generycznie:


	builder
		.RegisterGeneric(typeof(MessageManager<>))
		.As(typeof(IMessageManager<>))
		.SingleInstance();

	builder
		.RegisterGeneric(typeof(MessageReceiver<>))
		.As(typeof(IMessageReceiver<>))
		.SingleInstance();

Oznacza to że dla każdego typu T, powstanie dokładnie jedna instancja MessageManager<T> oraz dokładnie jedna instancja MessageReceiver<T>.
Szczegóły w dokumentacji: Open Generic Components

Dzięki temu, prosty zabieg w prezenterach pozwoli nam na wymianę pomiędzy nimi informacji o zdarzeniach.
W przypadku naszego tworzenia dokumentów będzie to wyglądać tak.

Dodajemy do SaveNewDocumentPresenter obsługę powiadomienia o utworzeniu dokumentu:


	internal class SaveNewDocumentPresenter : ISaveNewDocumentPresenter
	{
		private readonly IMessageManager<NewDocumentCreatedMessage> messageManager;
		[...]

		public SaveNewDocumentPresenter(
			[...],
			IMessageManager<NewDocumentCreatedMessage> messageManager
			)
		{
			[...]
			this.messageManager = messageManager;
		}

		public void ShowDocumentSucceedMessage(string message, IDocument document)
		{
			[...]
			messageManager.Raise(new NewDocumentCreatedMessage { CreatedDocument = documentViewModel });
		}

		[...]
	}

	public class NewDocumentCreatedMessage : IMessage
	{
		public IDocumentViewModel CreatedDocument { get; set; }
	}

Dodaliśmy nowy typ zdarzenia: NewDocumentCreatedMessage, i zażądaliśmy od MessageManagera, aby powiadomił wszystkich zainteresowanych tym zdarzeniem że nastąpiło.

Pozostaje nam zmodyfikować MainForm w taki sposób aby naszą wiadomość otrzymała. W tym celu należy dodać jej zależność od MessageReceiver i zapisać się na zdarzenie MessageReived:


	internal class MainWindowPresenter : IMainWindowPresenter
	{
		public MainWindowPresenter(
			[...]
			IMessageReceiver<NewDocumentCreatedMessage> documentCreatedMessageReceiver)
		{
			[...]

			documentCreatedMessageReceiver.MessageReived += NewDocumentCreatedMessageReceived;
		}

		private void NewDocumentCreatedMessageReceived(object sender, MessageEventArgs<NewDocumentCreatedMessage> args)
		{
			view.Documents.Add(args.Message.CreatedDocument);
		}
	}

That's it!
Nic więcej nie potrzeba. Mamy już wszystko co pozwoli nam na napisanie aplikacji wielookienkowej, która będzie odświeżać widoki gdy któraś akcja spowoduje zmianę danych.

Comments:1

  1. Bardzo udane wpisy w tym cyklu. A jeśli jeszcze czegoś z tego nie widziałeś – to są linki, jakie ostatnio poleciłem ludziom z mojej firmy:

    Architecture, the Lost Years – http://www.confreaks.com/videos/759-rubymidwest2011-keynote-architecture-the-lost-years

    Screaming Architecture – http://blog.8thlight.com/uncle-bob/2011/09/30/Screaming-Architecture.html

    Clean Architecture – http://blog.8thlight.com/uncle-bob/2011/11/22/Clean-Architecture.html

    The Clean Architecture – http://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html

    Prezentacja na NDC 2014 – http://vimeo.com/43612849

    Clean Micro-service architecture – http://blog.cleancoder.com/uncle-bob/2014/10/01/CleanMicroserviceArchitecture.html

Leave a Reply

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