sobota, 19 grudnia 2015

Jeśli trafiłeś gdzieś w środek serii, lepiej będzie jak odpalisz te posty w takiej kolejności:
  • Passive MVC vs MVP Passive View (wkrótce...) 
  • Active MVC vs MVP Passive View (wkrótce...) 
  • Passive MVC vs MVP Supervising Controller (wkrótce...) 
  • MVP Passive View i MVP Supervising Controllet (wkrótce...)
Na początku był plan, żeby każdy z postów był autonomiczny, ale wymagałoby to zbyt wiele powtórzeń tego co już napisałem. Wraz z kolejnymi postami powstają skróty myślowe, do których będe się odnosić w dalszych częściach.

Wstęp

Porównując Acive MVC i MVP Supervising Controller nie będę uzależniać swoich przykładów od konkretnych technologii budowania Widoków. WPF ma swoją technologię wiązania danych z Widokiem, Windows Forms (jeśli ktokolwiek coś w tym jeszcze pisze) ma swoją technologię opartą o DataAdaptery, a ASP.NET to jeszcze całkiem inna bajka. Porównanie wzorca Active MVC i MVP Supervising Controller będę opierać o przykłady napisanie w suchej konsoli.

Porównania będę opierać o następujące kryteria:
  • Który element zarządza stanem Modelu?
  • Który element komunikuje się z Widokiem?
  • W jaki sposób odbywa się uaktualnianie Widoku?
  • W jaki sposób obsługiwane są akcje wykonane przez użytkownika na Widoku?
  • Który element od którego zależy?

Funkcjonalności przykładowej aplikacji

W każdym z moich przykładów będę tworzył aplikację z identycznymi funkcjonalnościami. Program będzie posiadać obiekt, który będzie punktem na osi współrzędnych x i y. Widokiem będzie konsola wyświetlająca ten punkt. Interfejsem użytkownika będą strzałki na klawiaturze, za pomocą których będzie można przesuwać punkt w wybranym kierunku.

Szybkie porównanie

We wszystkich odmianach MVC i MVP, logika systemu znajduje się w Modelu. W żadnym z wzorców Model nie służy do pośredniczenia między bazą danych a Kontrolerem lub Prezenterem.

Na początek dwie tabelki, w których jest krótko o podobieństwach i różnicach między Active MVC a MVP Supervising Controller.

MVP vs MVC - zależności
MVP vs MVC - zależności

MVP vs MVC - cechy charakterystyczne
MVP vs MVC - cechy charakterystyczne

Dwie pomocnicze klasy

Za nim przejdę do porównywania wzorców zdefiniuję dwa obiekty pomocnicze, które w każdym z przykładów będą wyglądać identycznie i nigdy się nie zmienią.

Klasa TeoConsole, będzie służyła mi do renderowania Widoków. Analogicznie jak w WPF czy w Windows Forms, tutaj będę mieć klasę TeoConsole, która będzie moim silnikiem do tworzenia widoków.
public abstract class TeoConsole
{
 private readonly int width;
 private readonly int height;

 private int currentX;
 private int currentY;

 protected TeoConsole(int width, int height)
 {
  this.width = width;
  this.height = height;
 }

 protected void InitializeComponent()
 {
  Console.BackgroundColor = ConsoleColor.Blue;

  for (int i = 0; i < height; ++i)
  {
   for (int j = 0; j < width; ++j)
    Console.Write(" ");

   Console.WriteLine();
  }

  Console.SetCursorPosition(0, 0);
 }

 protected void Render(int x, int y)
 {
  ClearLast(currentX, currentY);
  PrintNew(x, y);
 }

 private void ClearLast(int x, int y)
 {
  Console.SetCursorPosition(x, y);
  Console.Write(" ");
 }

 private void PrintNew(int x, int y)
 {
  if (x >= 0 && y >= 0)
  {
   Console.SetCursorPosition(x, y);
   Console.Write("#");

   currentX = x;
   currentY = y;
  }
 }
}
Drugi obiekt to OverAndOverAgainActionRunner, którego zadaniem jest wywoływanie w nieskończoność aktualnie ustawionej akcji, w stałych odstępach czasu. Można podmieniać tą akcję podczas uruchomienia. Dzięki temu obiektowi będę mógł nadać ruch mojemu punktowi. Kod tej klasy również nigdy nie ulegnie zmianie, niezależnie od wzorca jaki zastosuje.
public interface IActionRunner
{
 void DoIt(Action a);
}

public class OverAndOverAgainActionRunner : IActionRunner
{
 private readonly Task overAndOverAgain;
 private Action action;
 private const int INTERVAL = 50;
 private readonly object lockObj;

 public OverAndOverAgainActionRunner()
 {
  lockObj = new object();
  action = () => { };
  overAndOverAgain = Task.Factory.StartNew(OverAndOverAgain);
 }

 public void DoIt(Action a)
 {
  lock (lockObj)
   action = a;
 }

 private void OverAndOverAgain()
 {
  while (true)
  {
   lock (lockObj)
   {
    action();
    overAndOverAgain.Wait(INTERVAL);
   }
  }
 }
Może, można było ten obiekt napisać zgrabniej, wydajniej, bezpieczniej lub użyć już czegoś gotowego. Może zamiast lock lepsze byłoby użycie SpinLock. Pewnie możliwa jest implementacja bez konieczności tworzenia sekcji krytycznej, a może można było do tego użyć programowania reaktywnego. Moim głównym celem jest jednak zaprezentowanie różnic między MVC a MVP. Klasy tej używam tylko jako obiektu pomocniczego.

Active MVC

Poniżej już samo mięso czyli, diagramy klas i kod w wersji Active MVC i MVP Supervising Controller. Na start wkleiłem cały kod, a w dalszych akapitach będę omawiać kluczowe elementy. Przeklejając kolejno kod do VS, otrzymasz kompilującą się i działającą aplikacje. Kod aplikacji również jest do pobrania z GitHub, gdzie dodatkowo znajduje się implementacja w Windows Forms.

MVC - diagram klas
MVC - diagram klas

#region Model

public interface IModel
{
 void IncreaseX();
 void DecreaseX();
 void IncreaseY();
 void DecreaseY();
}

public interface IObservable
{
 void Subscribe(IObserver observer);
}

public class Model : IModel, IObservable
{
 private readonly int minX;
 private readonly int maxX;

 private readonly int minY;
 private readonly int maxY;

 private int x;
 private int y;

 private readonly List observers;
 private readonly IActionRunner actionRunner;

 public Model(int minX, int maxX, int minY, int maxY, IActionRunner actionRunner)
 {
  this.minX = minX;
  this.maxX = maxX;
  this.minY = minY;
  this.maxY = maxY;
  this.actionRunner = actionRunner;

  observers = new List();
 }

 public void Subscribe(IObserver observer)
 {
  observers.Add(observer);
 }

 public void IncreaseX() 
  => DoIt(IncX);

 public void DecreaseX() 
  => DoIt(DecX);

 public void IncreaseY() 
  => DoIt(IncY);

 public void DecreaseY() 
  => DoIt(DecY);

 private void IncX()
 {
  if (x == maxX - 1)
   x = minX;
  else
   x++;
 }

 private void DecX()
 {
  if (x == minX)
   x = maxX - 1;
  else
   x--;
 }

 private void IncY()
 {
  if (y == maxY - 1)
   y = minY;
  else
   y++;
 }

 private void DecY()
 {
  if (y == minY)
   y = maxY - 1;
  else
   y--;
 }

 private void Notify()
 {
  foreach (var observer in observers)
   observer.Update(x, y);
 }

 private void DoIt(Action action)
 {
  actionRunner.DoIt(() => {
   action();
   Notify();
  });
 }
}

#endregion Model

#region Controller

public interface IController
{
 void MoveRight();
 void MoveLeft();
 void MoveUp();
 void MoveDown();
}

public class Controller : IController
{
 private readonly IModel model;

 public Controller(IModel model)
 {
  this.model = model;
 }

 public void MoveRight()
 {
  model.IncreaseX();
 }

 public void MoveLeft()
 {
  model.DecreaseX();
 }

 public void MoveUp()
 {
  model.DecreaseY();
 }

 public void MoveDown()
 {
  model.IncreaseY();
 }
}

#endregion Controller

#region View

public interface IView
{
 void SetController(IController controller);
}

public interface IObserver
{
 void Update(int x, int y);
}

public class View : TeoConsole, IObserver, IView
{
 private IController controller;

 public View(int width, int height)
  : base(width, height)
 {
  InitializeComponent();
 }

 public void SetController(IController cntrl)
  => controller = cntrl;

 public void Update(int x, int y)
  => Render(x, y);

 public void ReadArrow()
 {
  while (true)
  {
   var key = Console.ReadKey();

   switch (key.Key)
   {
    case ConsoleKey.UpArrow:
     controller.MoveUp();
     break;
    case ConsoleKey.DownArrow:
     controller.MoveDown();
     break;
    case ConsoleKey.RightArrow:
     controller.MoveRight();
     break;
    case ConsoleKey.LeftArrow:
     controller.MoveLeft();
     break;
    case ConsoleKey.Escape:
     return;
   }
  }
 }
}

#endregion View

class Program
{
 static void Main(string[] args)
 {
  int width = 50;
  int height = 50;

  IActionRunner runner = new OverAndOverAgainActionRunner();
  var model = new Model(0, width, 0, height, runner);
  var controller = new Controller(model);
  var view = new View(width, height);

  model.Subscribe(view);
  view.SetController(controller);

  view.ReadArrow();
 }
}

MVP Supervising Controller

MVP Supervising Controller - diagram klas
MVP Supervising Controller - diagram klas

#region Model

public delegate void ModelChangedEventHandler(IModel model);

public interface IModel
{
 event ModelChangedEventHandler ModelChanged;

 int X { get; }
 int Y { get; }

 void IncreaseX();
 void DecreaseX();
 void IncreaseY();
 void DecreaseY();
}

public class Model : IModel
{
 private readonly int minX;
 private readonly int maxX;

 private readonly int minY;
 private readonly int maxY;

 public int X { get; private set; }
 public int Y { get; private set; }

 public Model(int minX, int maxX, int minY, int maxY)
 {
  this.minX = minX;
  this.maxX = maxX;
  this.minY = minY;
  this.maxY = maxY;
 }

 public void IncreaseX()
 {
  if (X == maxX - 1)
   X = minX;
  else
   X++;

  OnModelChanged();
 }

 public void DecreaseX()
 {
  if (X == minX)
   X = maxX - 1;
  else
   X--;

  OnModelChanged();
 }

 public void IncreaseY()
 {
  if (Y == maxY - 1)
   Y = minY;
  else
   Y++;

  OnModelChanged();
 }

 public void DecreaseY()
 {
  if (Y == minY)
   Y = maxY - 1;
  else
   Y--;

  OnModelChanged();
 }

 public event ModelChangedEventHandler ModelChanged;

 private void OnModelChanged()
  => ModelChanged?.Invoke(this);
}

#endregion Model

#region Presenter

public class Presenter
{
 private readonly IModel model;
 private readonly IActionRunner actionRunner;

 public Presenter(IModel model, IView view, IActionRunner actionRunner)
 {
  this.model = model;
  view.OnPressArrow += ViewOnPressArrow;
  this.actionRunner = actionRunner;
 }

 private void ViewOnPressArrow(object sender, ConsoleKey consoleKey)
 {

  switch (consoleKey)
  {
   case ConsoleKey.UpArrow:
    DoIt(model.DecreaseY);
    break;
   case ConsoleKey.DownArrow:
    DoIt(model.IncreaseY);
    break;
   case ConsoleKey.RightArrow:
    DoIt(model.IncreaseX);
    break;
   case ConsoleKey.LeftArrow:
    DoIt(model.DecreaseX);
    break;
   case ConsoleKey.Escape:
    return;
  }
 }

 private void DoIt(Action action)
  => actionRunner.DoIt(action);
}

#endregion Presenter

#region View

public interface IView
{
 event EventHandler OnPressArrow;
}

public class View : TeoConsole, IView
{
 public View(int width, int height, IModel model)
  : base(width, height)
 {
  InitializeComponent();
  Bind(model);
 }

 public event EventHandler OnPressArrow;

 public void ReadArrow()
 {
  while (true)
  {
   var key = Console.ReadKey();
   OnPressArrow?.Invoke(this, key.Key);
  }
 }

 private void Bind(IModel model)
  => model.ModelChanged += (m) => Render(model.X, model.Y);
}

#endregion View

class Program
{
 static void Main(string[] args)
 {
  int width = 50;
  int height = 50;

  var model = new Model(0, width, 0, height);
  var view = new View(width, height, model);
  var runner = new OverAndOverAgainActionRunner();
  var presenter = new Presenter(model, view, runner);

  view.ReadArrow();
 }
}

Analiza

Kto powiadamia Widok o zmianie stanu Modelu?

Zarówno w Active MVC jak i w MVP Supervising Controller to Model odpowiedzialny jest za powiadomienie Widoku o zmianie stanu Modelu. W Active MVC informowanie Widoku zrealizowane jest po przez Wzorzec Obserwator. W MVP Supervising Controller powiadamianie Widoku zrealizowane jest przez wiązanie (ang. binding).

Między MVP a MVC charakterystyczną różnicą jest pojawienie się pojęcia wiązania (ang. binding). Każda z technologii posiada swoje implementacje silników umożliwiające wiązanie. Na przykład WPF ma wiązania w XAML do DataContextu, a Windows Forms można wykorzystać DataAdaptery. Dzięki wiązaniu, nie pojawia się w Modelu zależność od Widoku lub czegokolwiek innego, nawet od czystej abstrakcji reprezentującej jakiegoś obserwatora.

W MVP Supervising Controller pozbyto się wzorca Obserwator na rzecz tak zwanego wiązania. Idea wiązania polega na kompletnym pozbyciu się zależności od Widoku. W przeciwieństwie do MVC, Model nie posiada zależności od interfejsu, którym jest obserwator. W MVP Supervising Controller Model zaimplementowany jest tak, jakby pojęcie Widoku nie istniało. To Widok musi umieć dowiązać się do Modelu aby móc czerpać z niego informacje o zmianie stanu. Widok wraz z silnikiem do tworzenia interfejsów użytkownika posiada magiczny kod, który wiąże się z Modelem (na przykład po przez refleksję).

Czy na pewno, za sprawą wiązania, Model nie posiada zależności od Widoku?

W MVP Supervising Controller Model rzeczywiście nie posiada referencji do Widoków, nawet w postaci czystych abstrakcji, ale jest uzależniony od technologii tworzenia Widoku. O ile to na Widokach spoczywa zadanie dowiązania się do Modelu to w praktyce to wygląda tak, że Model musi posiadać, kod który spowoduje że to dowiązanie zadziała.

Na przykład w WPF Model musi implementować interface INotifyPropertyChanged, który wymusza implementację zdarzenia PropertyChangedEventHandler PropertyChanged. Jeśli tego nie zrobimy to dowiązania widokiem w WPF nie zadziała. Owszem, nie ma zależności od Widoku, nie ma zależności nawet od czystej abstrakcji Widoku, ale jest uzależnienie w postaci konieczności wywoływania odpowiedniego zdarzenia z ściśle zdefiniowanymi zasadami. Tam w WPF w argumentach zdarzenia trzeba nawet pisać stringi, które są nazwami właściwości do których Widok się dowiązuje.

Moja implementacja wiązania

Ja swoje wiązanie oparłem na zdarzeniach. Model zawiera zdarzenie typu ModelChangedEventHandler, które jest wywoływane za każdym razem gdy ulegnie zmianie właściwość modelu X lub Y. Dzięki temu dowolny Widok, jeśli będzie chciał obserwować zmiany Modelu może dopiąć się do tego zdarzenia. Wygląda to nieco zgrabniej w porównaniu do Active MVC gdzie w Modelu jest konieczność zawierania kolekcji obserwatorów czyli Widoków.
Za każdym razem gdy zmieni się stan Modelu, wywoływana jest metoda OnModelChanged, która odpala zdarzenie ModelChangedEventHandler ModelChanged.
public class Model : IModel
{
 // ...

 public void IncreaseX()
 {
  if (X == maxX - 1)
   X = minX;
  else
   X++;

  OnModelChanged();
 }

 // ...

 public event ModelChangedEventHandler ModelChanged;

 private void OnModelChanged()
  => ModelChanged?.Invoke(this);
}

Wzorzec Obserwator w Active MVC

W Active MVC odseparowanie logiki od Widoku zostało zrealizowane za pomocą Wzorca Obserwator co skutkuje tym, że w Modelu posiadamy kolekcję Obserwatorów, którymi są Widoki. Oczywiście Obserwatorzy to interfejsy, których implementację muszą spełnić Widoki. Jest to bardzo sprytne rozwiązanie, ponieważ zapewnia nam oddzielenie logiki od Widoku, ale pomimo to i tak jesteśmy zmuszeni w naszej logice mieć kawałek kodu związany z zarządzaniem kolekcją Obserwatorów, czyli dodawaniem, usuwaniem i powiadamianiem tych Obserwatorów.

Za każdym razem gdy zmieni się stan Modelu wywoływana jest metoda Notify, która wywołuje na obserwatorach czyli na Widoku metodę Update.
public class Model : IModel, IObservable
{
 // ...

 public void IncreaseX() 
  => DoIt(IncX);

 // ...

 private void IncX()
 {
  if (x == maxX - 1)
   x = minX;
  else
   x++;
 }
 
 // ...

 private void Notify()
 {
  foreach (var observer in observers)
   observer.Update(x, y);
 }

 private void DoIt(Action action)
 {
  actionRunner.DoIt(() => {
   action();
   Notify();
  });
 }
}

Logika

Moja logika w Modelu polega na ograniczeniach nałożonych na wartości X i Y. Wartości te nie mogą być, ani większe, ani mniejsze, od zadanych w konstruktorze. Gdy któraś z nich osiągnie wartość maksymalną, to w kolejnym kroku zwiększającym, wróci ona do wartości minimalnej. Gdy osiągnie wartość minimalną to w kolejnym kroku zmniejszającym zmieni się ona na wartość maksymalną.

Zarządzanie stanem Modelu

Model zawiera logikę systemu, a w moim przypadku tą logiką są ograniczenia nałożone na wartości X i Y. Wywołanie dowolnej metody na Modelu skutkuje zmianą wartości X lub Y, a co za tym idzie powoduje, że Model zmienia swój stan.

Jeśli Model z wnętrza swojej implementacji potrafi wywoływać swoje metody tak, że zmienia się jego stan to oznacza, że Model zarządza swoim stanem. Natomiast gdy Model zmienia swój stan tylko w wyniku wywołania jego metod przez inny moduł, wówczas mówię, że stanem Modelu zarządza ten inny element systemu.

W przypadku Active MVC w modelu mamy referencję do obiektu typu IActionRunner. Po wywołaniu przez Kontroler dowolnej metody interfejsu Modelu, do pracy startuje IActionRunner który, z wnętrza Modelu zmiana stan tego Modelu. Mówię wtedy, że Model zarządza swoim stanem.

W Active MVC Kontroler nic nie wie o tym, że stan Modelu się zmienił. W Active MVC Kontroler odpowiedzialny jest za przetłumaczenie i oddelegowanie interakcji użytkownika z systemem do Modelu. Kontroler jest kompletnie nieświadomy tego, że Model zmienił swój stan.

W MVP Supervising Controller mamy tutaj małą różnicę w porównaniu do Active MVC. O ile, przypomnę tutaj że, zarówno w MVP Supervising Controller i Active MVC to Model jest odpowiedzialny za powiadamianie o zmianie swojego stanu - to za zarządzanie stanem Modelu w MVP Supervising Controller odpowiedzialny jest nie Model a Prezenter.

W Active MVC zasada jest prosta: skoro Model sam decyduje o swojej zmianie stanu to niech on sam powiadamiania Widok o niej. W MVP zarówno w wersji Supervising Controller jak i w wersji Passive View za zmianę stanu Modelu odpowiedzialny jest Prezenter.

Nie chcę tutaj za bardzo mieszać więc nie powiem, że w MVP Passive View w przeciwieństwie do Supervising Controller, to Prezenter a nie Model jest odpowiedzialny za powiadamianie o zmianie stanu Modelu. O MVP Passive View, będzie w kolejnym poście.

W MVP Supervising Controller Prezenter w metodzie ViewOnPressArrow reaguje na zdarzenie przyciśnięcia strzałki, w taki sposób, że za pomocą obiektu typu IActionRunner z wnętrza Prezentera cyklicznie wywołuje metody na Modelu. Każde wywołanie dowolnej metody na Modelu pochodzi z rozkazu Prezentera.
public class Presenter
{
    // ...
 public Presenter(IModel model, IView view, IActionRunner actionRunner)
 {
  // ...
  view.OnPressArrow += ViewOnPressArrow;
  // ...
 }

 private void ViewOnPressArrow(object sender, ConsoleKey consoleKey)
 {
  switch (consoleKey)
  {
   case ConsoleKey.UpArrow:
    DoIt(model.DecreaseY);
    break;
   // ...
  }
 }

 private void DoIt(Action action)
  => actionRunner.DoIt(action);
}
W MVP Supervising Controller Prezenter wywołuje metody, które zmieniają stan Modelu, Model nigdy nie może wywołać sam sobie metody, która zmieniłaby jego stan, ale Model nie zwraca swojego stanu do Prezentera. Prezenter nie wie w jakim stanie znajduje się Model. Stan Modelu przekazywany jest bezpośrednio do widoku za pomocą tak zwanego wiązania czyli po przez odpalenie zdarzenia ModelChanged.

Wzorzec Strategia w Active MVC

W Active MVC Kontroler jedynie deleguje żądania z Widoku do modelu. Między Widokiem a Modelem jest Wzorzec Strategia. Kontroler ma za zadanie przetłumaczyć akcje wykonane na Widoku, na wywołanie konkretnych metod na Modelu. Instancja obiektu Kontrolera znajduje się w Widoku. W każdej chwili można podmienić konkretną implementację Kontrolera uzyskując inne zachowanie, bez konieczności zmiany implementacji Widoku. Kontroler ma za zadanie oddelegować wywołania do Modelu. Kontroler nie zarządza stanem Modelu i nie obserwuje zmian stanu Modelu.

Prezenter to Code Behind Widoku

W MVP Supervising Controller zamiast Kontrolera jest Prezenter. Prezenter podobnie jak Kontroler ma za zadanie przetłumaczyć akcje wykonane na Widoku. W przypadku WPF Prezenter jest powiązany z Widokiem za pomocą tak zwanych komend. W przypadku Windows Forms Prezenter zapina się na zdarzenia kontrolek Widoku. Prezenter jest tak zwanym code behind Widoku.
Prezenter zawiera referencje do Modelu. W konstruktorze Prezenter podpina się pod zdarzenia Widoku oraz implementuje cały code behind Widoku. Prezenter wywołuje odpowiednie metody na Modelu. Prezenter nie odczytuje stanu Modelu. Model przekazuje swój stan do Widoku za pomocą wiązania.

Widok ma być tak głupi jak to tylko możliwe

Zarówno w Active MVC i MVP Supervising Controller widok jest tak głupi jak to tylko możliwe. W przypadku MVP Supervising Controller Widok jest głupszy od Widoku w Active MVC i jest tak głupi, że głupszy już być nie może.

W tym wypadku im głupszy widok tym lepiej. W MVP Supervising Controller widok nie posiada praktycznie żadnego kodu. Code behind widoku jest w Prezenterze, który podpina się na zdarzenia kontrolek lub implementuje obsługę komend.

Gdy użytkownik wciśnie strzałkę odpalane jest zdarzenie OnPressArrow. W metodzie Bind Widok dowiązuje się do zdarzeń zmiany stanu Modelu.
public class View : TeoConsole, IView
{
 public View(int width, int height, IModel model)
  : base(width, height)
 {
  InitializeComponent();
  Bind(model);
 }

 public event EventHandler OnPressArrow;

 public void ReadArrow()
 {
  while (true)
  {
   var key = Console.ReadKey();
   OnPressArrow?.Invoke(this, key.Key);
  }
 }

 private void Bind(IModel model)
  => model.ModelChanged += (m) => Render(model.X, model.Y);
}
W Active MVC Widok posiada referencje do Kontrolera, na którym wywołuje metody w wyniku akcji podjętych poprzez użytkownika. Widok nie wie co z daną akcją zrobić. To dzięki odpowiedniej implementacji kontrolera zachowania użytkownika tłumaczone są na odpowiednie metody w logice aplikacji czyli w Modelu. Kontrolery można dowolnie podmieniać bez konieczności zmiany kodu Widoku. Widok dowiaduje się o zmianie stanu Modelu za pomocą metody Update, która wywoływana jest przez Model.
public class View : TeoConsole, IObserver, IView
{
 private IController controller;

 public View(int width, int height)
  : base(width, height)
 {
  InitializeComponent();
 }

 public void SetController(IController cntrl)
  => controller = cntrl;

 public void Update(int x, int y)
  => Render(x, y);

 public void ReadArrow()
 {
  while (true)
  {
   var key = Console.ReadKey();

   switch (key.Key)
   {
    case ConsoleKey.UpArrow:
     controller.MoveUp();
     break;
    case ConsoleKey.DownArrow:
     controller.MoveDown();
     break;
    case ConsoleKey.RightArrow:
     controller.MoveRight();
     break;
    case ConsoleKey.LeftArrow:
     controller.MoveLeft();
     break;
    case ConsoleKey.Escape:
     return;
   }
  }
 }
}

Active MVP Supervising Controller

Na sam koniec pozwolę sobie na zaprezentowanie swojej wariacji na temat wzorca MVP Supervising Controller. Doszedłem do wniosku, że skoro w Active MVC Model sam zarządza swoim stanem i sam powiadamia o zmianie swojego stanu, to dlaczego nie zastosować tego samego podejścia w przypadku MVP Supervising Controller. Wszystkie zasady tego wzorca pozostawiłem bez zmian po za jedną: Model sam aktywnie będzie zmieniał swój stan. Obiekt IActionRunner wylądował w Modelu.
Active MVP Supervising Controller - zależności
Active MVP Supervising Controller - zależności
Active MVP Supervising Controller - cechy charakterystyczne
Active MVP Supervising Controller - cechy charakterystyczne

Poniżej wkleiłem kod tej mojej szatańskiej wersji.
#region Model

public delegate void ModelChangedEventHandler(IModel model);

public interface IModel
{
 event ModelChangedEventHandler ModelChanged;

 int X { get; }
 int Y { get; }

 void IncreaseX();
 void DecreaseX();
 void IncreaseY();
 void DecreaseY();
}

public class Model : IModel
{
 private readonly int minX;
 private readonly int maxX;

 private readonly int minY;
 private readonly int maxY;

 public int X { get; private set; }
 public int Y { get; private set; }

 private readonly IActionRunner actionRunner;

 public Model(int minX, int maxX, int minY, int maxY, IActionRunner actionRunner)
 {
  this.minX = minX;
  this.maxX = maxX;
  this.minY = minY;
  this.maxY = maxY;

  this.actionRunner = actionRunner;
 }

 public void IncreaseX()
  => DoIt(IncX);

 public void DecreaseX()
  => DoIt(DecX);

 public void IncreaseY()
  => DoIt(IncY);

 public void DecreaseY()
  => DoIt(DecY);

 private void IncX()
 {
  if (X == maxX - 1)
   X = minX;
  else
   X++;
 }

 private void DecX()
 {
  if (X == minX)
   X = maxX - 1;
  else
   X--;
 }

 private void IncY()
 {
  if (Y == maxY - 1)
   Y = minY;
  else
   Y++;
 }

 private void DecY()
 {
  if (Y == minY)
   Y = maxY - 1;
  else
   Y--;
 }

 public event ModelChangedEventHandler ModelChanged;

 private void OnModelChanged()
  => ModelChanged?.Invoke(this);

 private void DoIt(Action action)
 {
  actionRunner.DoIt(() => {
   action();
   OnModelChanged();
  });
 }
}

#endregion Model

#region Presenter

public class Presenter
{
 private readonly IModel model;

 public Presenter(IModel model, IView view)
 {
  this.model = model;
  view.OnPressArrow += ViewOnPressArrow;
 }

 private void ViewOnPressArrow(object sender, ConsoleKey consoleKey)
 {

  switch (consoleKey)
  {
   case ConsoleKey.UpArrow:
    model.DecreaseY();
    break;
   case ConsoleKey.DownArrow:
    model.IncreaseY();
    break;
   case ConsoleKey.RightArrow:
    model.IncreaseX();
    break;
   case ConsoleKey.LeftArrow:
    model.DecreaseX();
    break;
   case ConsoleKey.Escape:
    return;
  }
 }
}

#endregion Presenter

#region View

public interface IView
{
 event EventHandler OnPressArrow;
}

public class View : TeoConsole, IView
{
 public View(int width, int height, IModel model)
  : base(width, height)
 {
  InitializeComponent();
  Bind(model);
 }

 public event EventHandler OnPressArrow;

 public void ReadArrow()
 {
  while (true)
  {
   var key = Console.ReadKey();
   OnPressArrow?.Invoke(this, key.Key);
  }
 }

 private void Bind(IModel model)
  => model.ModelChanged += (m) => Render(model.X, model.Y);
}

#endregion View

class Program
{
 static void Main(string[] args)
 {
  int width = 50;
  int height = 50;

  var runner = new OverAndOverAgainActionRunner();
  var model = new Model(0, width, 0, height, runner);
  var view = new View(width, height, model);
  var presenter = new Presenter(model, view);

  view.ReadArrow();
 }
}


0 komentarze :

Prześlij komentarz