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();
 }
}


wtorek, 8 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...)
To czy dany kawałek kodu można nazwać wzorcem, nie jest wyznacznikiem czy nasz kod jest dobry czy zły. Możemy w ogóle nie pisać używając wzorców lub używać je nieświadomie. Najważniejsze aby osiągnąć pożądany cel.

... ale ...

... zdefiniowanie wzorców i świadome posługiwanie się nimi, oraz używanie odpowiedniego słownictwa może znacznie podnieść zrozumienie kodu oraz poprawić komunikację. Często też wzorce dają nam gotowe rozwiązanie powtarzalnych zagadnień, co sprawia, że nie musimy wymyślać koła na nowo. Znajomość wzorców i ich świadome stosowanie lub świadome pomijanie znacznie zwiększa prawdopodobieństwo na wytworzenie produktu wysokiej jakości. 

Po tym krótkim, lekko filozoficznym wstępie czuje się usprawiedliwiony aby w dalszej części artykułu rozłożyć na czynniki pierwsze wzorzec MVP, porównam go do Passive i Active MVC.

Przekopałem znaczną część internetu oraz zajrzałem do dwóch książek szukając informacji o MVP. Czasami czytałem, że Prezenter zawiera logikę aplikacji natomiast Model jest jedynie pośrednikiem między tą logiką a bazą danych. Gdzie indziej przeczytałem definicję MVP, która sugeruje, że MVP i MVC to praktycznie to samo, poza różnicą związaną ze sposobem powiadamiania Widoku. Inna często wymienianą różnica: w MVC, to Model odpowiedzialny jest za powiadamianie Widoku, a w MVP, tą odpowiedzialność posiada Prezenter.

Poniżej fragment z książki "C# 6.0 i MVC 5" Krzysztof Żydzik, Tomasz Rak.

"Model View Presenter to wzorzec powstały na bazie wzorca MVC. We wzorcu MVP prezenter jest tym samym, czym kontroler we wzorcu MVC z jedną mała różnicą, w prezenterze zawiera się logika biznesowa. Dane nie są przekazywane bezpośrednio z modelu do widoku jak to ma miejsce w MVC. Prezener wysyła zapytanie do modelu, model zwraca dane do prezentera, prezenter przetwarza otrzymane dane i przekazuje do widoku."

To wszystko nie jest ani prawdą, ani nie prawdą. To jest nie do końca prawda i to też jest nie do końca nieprawda. Nie można porównywać wzorców MVC i MVP bez konkretnego rozdzielenia tych wzorców na warianty. Wzorzec MVC można zaimplementować w odmianie Passive lub Active. Moje zdanie jest takie, że te dwie odmiany wzorca MVC tak się od siebie różnią, że nie można mówić o wzorcu MVC nie doprecyzowując, którą odmianę mamy na myśli. Podobnie jest z wzorcem MVP. Ten też posiada dwie odmiany: Passive View oraz Supervising Controller.

Zauważyłem taką tendencję, że jeśli ktoś pisze lub mówi o MVC to w domyśle ma wersję Passive a gdy ktoś mówi o MVP to w domyśle mówi o Passive View. Taki skrót myślowy powoduje, że porównujemy te dwa wzorce parując ze sobą tylko po jednej z odmian. W moim rozumieniu porównanie MVC i MVP wymaga wykonania sześciu różnych porównań, jak poniżej:

Active MVC, Passive MVC, MVP Passive View, MVP Supervising Controller



sobota, 17 października 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...)
Podobnie jak w poprzednim poście o MVC, powstanie tutaj aplikacja, która umożliwia przesuwanie znaczka po ekranie, co oczywiście nie jest jakimś wyczynem. Celem tej aplikacji jest przećwiczenie teorii o MVC w praktyce, porównując warianty Passive i Active.

Na początku, wymienię różnice między Active i Passive MVC oraz w drugiej kolejności, analogicznie jak w poprzednim poście, zaimplementuję przykładową aplikację.

Passive MVC vs Active MVC

[Round 1]

  • Active MVC: Model posiada kolekcję Widoków, które są informowane przez Model o jego zmianie i konieczności odświeżania Widoku. Powiadamianie Widoków odbywa się za pomocą wzorca Obserwator, gdzie obiektem obserwowanym jest Model a obserwatorem jest Widok.
  • Passive MVC: Model nie posiada kolekcji Widoków i nie powiadamia ich o swojej zmianie. Role obiektu obserwowanego przejmuje na siebie Kontroler. Model jest kompletnie nieświadomy istnienia Widoku i Kontrolera. Model nie posiada żadnych zależności.

[Round 2]

  • Active MVC: Model zależy od Widoku w postaci kolekcji obserwatorów, których informuje o swojej zmianie.
  • Passive MVC: Kontroler oraz Widok zależą od Modelu ale Model nie zależy od niczego.

[Round 3]

  • Active MVC: Model, może (może ale nie musi) sam inicjować zmianę swojego stanu bez konieczności odpalania jego metod przez Kontroler.
  • Passive MVC: Model nie może samoczynnie zmienić swojego stanu. Każda zmiana stanu jest aktywowana przez wywołanie odpowiednich metod przez Kontroler.

[Round 4]

  • Active MVC & Passive MVC: Kontroler interpretuje akcje wykonane przez użytkownika na Widoku i wywołuje odpowiednią metodę na Modelu.

[Round 5]

  • Active MVC: Model sam decyduje o tym kiedy poinformować o swojej zmianie Widok.
  • Passive MVC: Model nie decyduje o konieczności powiadomienia Widoku o zmianie swojego stanu. To Kontroler podejmuje tą decyzję.

[Round 6]

  • Active MVC: Za każdym razem gdy Model zmieni swój stan, z automatu wywołuje metodę Update na Widoku, który zawsze się odświeża.
  • Passive MVC: Kontroler posiada kontrolę nad powiadamianiem Widoku o zmianie Modelu przez co może zdecydować o wydajności i optymalizacji. Nie wszystkie zmiany Modelu muszą odwieżać Widok.

[Round 7]

  • Active MVC: Kontroler jest tak prosty jak to jest możliwe a Widok jest tak głupi jak to tylko możliwe.
  • Passive MVC: Kontroler tak samo jak w Active odpala metody na Modelu ale jest nieco mądrzejszy, ponieważ decyduje o tym kiedy odświeżyć Widok.

[Round 8]

  • Active MVC: Wzorzec Obserwator występuje między Modelem a Widokiem gdzie Model jest obserwowany a Widok jest obserwatorem.
  • Passive MVC: Wzorzec Obserwator może występować między Kontrolerem a Widokiem gdzie Kontroler jest obserwowany a Widok jest obserwatorem.

[Round 9]

  • Active MVC: Po raz pierwszy aplikacja oparta o wzorzec MVC powstała w 1979 roku w języku SmallTalk, gdy WEB jeszcze nie istniał. MVC był projektowany wyłącznie dla aplikacji okienkowych. Dzisiaj już się go nie stosuje często w jego czystej postaci.
  • Passive MVC: Odmiana pasywna wzorca MVC jest adaptacją klasycznego wzorca MVC do dzisiejszych realiów, gdzie rządzi niepodzielnie WEB. Najbliżej tej odmianie do Modelu 2 (JSP) w Javie oraz ASP.NET MVC w .NET.
KOD APLIKACJI JEST NA GITHUB... TeoVincent/MVC-by-example

Model

  • Modelem jest obiekt będący punktem, posiadającym współrzędne X i Y. W przeciwieństwie do wersji Active, Model Passive nie posiada kolekcji obserwatorów oraz metody wywołującej powiadomienia tych obserwatorów.
  • Model jest czystym obiektem zawierającym tylko i wyłącznie logikę, bez jakichkolwiek zależności od Kontrolera oraz Widoku.
  • Jedynym ukłonem w stronę Kontrolera jest tutaj wystawienie publicznych getterów zwracających aktualną wartość współrzędnych X i Y.
public interface IModel
{
    int X { get; }
    int Y { get; }

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

public class Model : IModel
{
    public int X { get; private set; }
    public int Y { get; private set; }

    public void IncreaseX()
    {
        X++;
    }

    public void DecreaseX()
    {
        X--;
    }

    public void IncreaseY()
    {
        Y++;
    }

    public void DecreaseY()
    {
        Y--;
    }
}

Kontroler

  • Kontroler w trybie Passive posiada te same odpowiedzialności jakie posiadał w trybie Active oraz dodatkowo odpowiada za to, za co odpowiadał Model, czyli za powiadamianie Widoku o zmianie stanu Modelu.
  • Kontroler zapewnia Widokowi inteligencję.
  • Zawiera instancję Modelu, do którego deleguje wywołania otrzymane od Widoku.
  • Obsługuje wywołania otrzymane od Widoku.
  • Pobiera dane wejściowe użytkownika i określa ich znaczenie dla Modelu. Odpowiada za interpretację danych wejściowych i wykonanie odpowiednich operacji na Modelu.
  • Żąda od Modelu zmiany stanu.
  • Określa, jakie operacje powinny być wykonane na Modelu.
  • Oddziela logikę sterowania od Widoku separując od siebie Widok i Model.
  • Między Widokiem a Kontrolerem jest wzorzec Strategia. Kontroler jest zachowaniem Widoku.
  • Zmieniając Kontroler zmienimy zachowanie Widoku, bez konieczności zmiany Widoku.
  • Widok jest obiektem konfigurowalnym za pomocą odpowiedniej strategii, którą zapewnia Kontroler.
  • Najpierw Widok przekazuje Kontrolerowi informacje o czynnościach użytkownika, następnie Kontroler inicjuje operacje Modelu.
  • Kontroler może wykonywać pewną pracę związaną z określaniem, jakie metody Modelu mają zostać wywołane, ale nigdy nie należy to do funkcji określanych jako, "logika aplikacji". Logika aplikacji to kod, który zarządza i operuje danymi. Jest on w całości zawarty w Modelu.
  • Kontroler zawiera instancję Modelu oraz Widoku dzięki czemu wywołuje odpowiednie metody na Modelu oraz powiadamia Widok o zmianie stanu Modelu.
public interface IController
{
 void MoveRight();
 void MoveLeft();
 void MoveUp();
 void MoveDown();
}

public class Controller : IController
{
 private readonly IModel model;
 private readonly IObserver view;

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

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

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

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

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

 protected override void Notify()
 {
  view.Update(model.X, model.Y);
 }
}

Widok

  • Nie ma różnic między Passive i Active w kontekście Widoku
  • Skoro Widok jest obserwatorem, to implementuje interfejs IObserver, w którym zdefiniowana jest metoda Update.
  • Widok otrzymuje powiadomienia o zmianie Modelu od Kontrolera za pomocą metody Update.
public interface IObserver
{
    void Update(int x, int y);
}
  • Metoda Update Widoku bazując na argumentach przekazanych od Kontrolera, odświeża swój Widok.
  • Poniżej przedstawiam przykładową implementację Widoku, który jest konsolą. W wyniku wywołania metody Update, Widok maluje się na nowo umieszczając znak '#' na miejscu przesuniętym w prawo o wartość 'x' oraz w dół o wartość 'y'. Za każdym razem gdy Model zmieni swój stan, czyli gdy zmienne x i y zmienią się w modelu, Kontroler wywoła metodę Update przekazując te wartości do Widoku, który namaluje się ponownie umieszczając znak '#' w nowym miejscu.
public class ConsoleView : IObserver
{
    private readonly int width;
    private readonly int height;
 
    public ConsoleView(int width, int height)
    {
        this.width = width;
        this.height = height;
    }
 
    public void Update(int x, int y)
    {
        Render(x, y);
    }
 
    private void Render(int x, int y)
    {
        Console.Clear();
 
        for (int i = 0; i < height; ++i)
        {
            for (int j = 0; j < width; ++j)
            {
                if (y == i && x == j)
                    Console.Write("#");
                else
                    Console.Write(" ");
            }
            Console.WriteLine();
        }
    }
}
  • Widać tutaj, że możemy implementować wiele różnych rodzajów Widoków bez konieczności zmiany implementacji Kontrolera oraz Modelu. Wystarczy tylko zaimplementować interface IObserver.
  • Aby użytkownik mógł za pomocą Widoku sterować aplikacją, Widok zawiera w sobie instancję Kontrolera.
  • Widok zawiera publiczną metodę SetController, za pomocą której, przypisuje sobie ten Kontroler.
  • Kontroler będzie służył Widokowi do delegowania akcji, jakie wykonał na Widoku użytkownik.
  • Metoda SetController definiowana jest w interfejsie, który każdy Widok implementuje.
public interface IView
{
    void SetController(IController controller);
}
 
public class ConsoleView : IObserver, IView
{
    private IController controller;
 
    // ... pozostała cześć klasy pozostaje niezmieniona
         
    public void SetController(IController controller)
    {
            this.controller = controller;
    }
}
  • Między Widokiem a Kontrolerem jest wzorzec Strategia. Kontroler jest strategią Widoku.
  • W obiekcie Widoku znajdują się metody umożliwiające interakcję użytkownika z Widokiem.
  • Widok nie wie jak ma zostać obsłużona dana akcja podjęta przez użytkownika.
  • Widok ma dwa zadania:
    • pierwsze - bycie interfejsem wyświetlającym informacje, o których został poinformowany przez Kontroler za pośrednictwem metody Update
    • drugie - umożliwienie interakcji z użytkownikiem, przekazując wywołania do Kontrolera, który podejmie decyzję co zrobić z danym żądaniem
  • Zadaniem Widoku nie jest obsługa interakcji użytkownika.
  • Widok oddelegowuje żądania użytkownika do Kontrolera.
  • Na potrzeby przykładu zaimplementuję możliwość zmieniania wartości x i y za pomocą strzałek na klawiaturze. Zdarzenia wciskania strzałek Widok oddeleguje do Kontrolera wywołując na tym Kontrolerze odpowiednie metody.
  • Poniżej jeszcze raz, tym razem już kompletna implementacja klasy ConsoleView. W porównaniu do poprzednich implementacji pojawiła się nowa metoda Navigate, która czeka na przyciśnięcia strzałek klawiatury.
public class ConsoleView : IObserver, IView
{
    private IController controller;
 
    private readonly int width;
    private readonly int height;
 
    public ConsoleView(int width, int height)
    {
        this.width = width;
        this.height = height;
    }
 
    public void Update(int x, int y)
    {
        Render(x, y);
    }
 
    private void Render(int x, int y)
    {
        Console.Clear();
 
        for (int i = 0; i < height; ++i)
        {
            for (int j = 0; j < width; ++j)
            {
                if (y == i && x == j)
                    Console.Write("#");
                else
                    Console.Write(" ");
            }
            Console.WriteLine();
        }
    }
 
    public void SetController(IController controller)
    {
        this.controller = controller;
    }
 
    public void Navigate()
    {
        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;
            }
        }
    }
}

Nowy wydajniejszy Widok

Dotychczas zaimplementowany Widok jest mało wydajny. Każde ponowne narysowanie znaku '#' pociąga za sobą drukowanie wszystkich wierszy konsoli na nowo.
Napisałem nową wydajniejszą wersję Widoku. Dzięki wzorcu MVC logika aplikacji od wyświetlania interfejsu jest sprytnie oddzielona, tak, że napisanie wydajniejszej wersji Widoku nie pociąga za sobą żadnych zmian w logice aplikacji. Nie ma innych zmian w całej aplikacji, jest tylko nowa wersja Widoku.
W nowym Widoku nie rysuję wszystkich linii na nowo. Efekt poruszania się znaku '#' rozwiązałem za pomocą metody SetCursorPosition klasy Console. Dodatkowo pokusiłem się o namalowanie tła. Implementacja wygląda tak:
public class ConsoleView : IObserver, IView
{
    private IController controller;

    private readonly int width;
    private readonly int height;

    private int currentX;
    private int currentY;

    public ConsoleView(int width, int height)
    {
        this.width = width;
        this.height = height;
        RenderBackground();
    }

    public void SetController(IController controller)
    {
        this.controller = controller;
    }

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

    public void Navigate()
    {
        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;
            }
        }
    }

    private 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;
        }
    }

    private void RenderBackground()
    {
        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);
    }
}

WinForms Widok

W celach ćwiczebno-zaczepnych zaimplementowałem, tak dla sportu, nową całkiem inną wersję Widoku, opartą o inną technologię budowania interfejsu, nie zmieniając przy tym ani jednej linijki kodu w logice aplikacji.
public partial class WinFormView : Form, IObserver, IView
{
    private IController controller;

    private readonly int width;
    private readonly int height;
    private readonly int scale;

    private readonly Graphics graphics;

    public WinFormView(int width, int height, int scale)
    {
        this.width = width*scale;
        this.height = height*scale;
        this.scale = scale;

        InitializeComponent();
        ClientSize = new Size(this.width, this.height);
        graphics = CreateGraphics();
    }

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

    public void SetController(IController controller)
    {
        this.controller = controller;
    }

    private void Render(int x, int y)
    {
        ClearLast();
        PrintNew(x, y);
    }

    private void ClearLast()
    {
        graphics.Clear(Color.Azure);
    }

    private void PrintNew(int x, int y)
    {
        var point = new Point(x*scale, y*scale);
        var size = new Size(scale, scale);
        var rectangle = new Rectangle(point, size);
        graphics.FillEllipse(Brushes.Black, rectangle);
    }

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        if (keyData == Keys.Up)
        {
            controller.MoveUp();
            return true;
        }

        if (keyData == Keys.Down)
        {
            controller.MoveDown();
            return true;
        }

        if (keyData == Keys.Left)
        {
            controller.MoveLeft();
            return true;
        }

        if (keyData == Keys.Right)
        {
            controller.MoveRight();
            return true;
        }

        return base.ProcessCmdKey(ref msg, keyData);
    }
}

Kontroler posiada kolekcję obserwatorów

Na sam koniec zaimplementuję wzorzec obserwator w Kontrolerze aby móc równocześnie wyświetlić więcej niż jeden Widok. Cała logika pozostaje bez zmian. Sterowanie aplikacji będzie możliwe za pomocą dwóch różnych Widoków bez konieczności najmniejszych zmian w logice aplikacji czyli w Modelu.
Kontroler jest obserwowany czyli musi posiadać swoich obserwatorów, więc obsługę subskrybowania obserwatorów można napisać raz w klasie abstrakcyjnej Observable.

public abstract class Observable
{
 protected readonly List observers;

 protected Observable()
 {
  observers = new List();
 }

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

 protected abstract void Notify();
}
Metoda Notify implementowana jest bezpośrednio w Kontrolerze, w którym ma ona dostęp do aktualnego stanu Modelu.
public class Controller : Observable, IController
{
 private readonly IModel model;

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

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

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

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

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

 protected override void Notify()
 {
  foreach(var view in  observers)
   view.Update(model.X, model.Y);
 }
}

Zmieniam logikę

bez zmiany Widoku i Kontrolera.
Nowa logika, wnosi do modelu cztery ograniczenia:
  • Wartości x i y nie mogą być mniejsze niż zero.
  • Wartości x i y nie mogą być większe niż zdefiniowane ograniczenie.
  • Gdy x lub y posiada wartość zero i zostanie wywołane żądanie zmniejszenia to nowa wartość ma wynosić największą możliwą.
  • Gdy x lub y osiągnie największą możliwą wartość i zostanie wywołane żądanie zwiększenia wówczas wartość ta ma się wyzerować.
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 = 0;
  else
   X++;
 }

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

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

 public void DecreaseY()
 {
  if (Y == minY)
   Y = maxY - 1;
  else
   Y--;
 }
}
Wszystko działa jak się można było spodziewać. Wciskając klawisze nawigacyjne klawiatury, przesuwamy kursorem w wybranym kierunku, a jeśli osiągniemy granicę przestrzeni, wówczas przeskakujemy na przeciwległą stronę powierzchni, i ani jedna linijka Widoku i Kontrolera nie została przy tym zmieniona.

Zbudowanie zależności

Na sam już koniec kod budujący zależności i startujący aplikację.
class Program
{
 static void Main(string[] args)
 {
  int width = 50;
  int height = 50;
  int scale = 10;

  var model = new Model(0, width, 0, height);
  var controller = new Controller(model);
  var consoleView = new ConsoleView(width, height);
  var winFormsView = new WinFormView(width, height, scale);

  controller.Subscribe(consoleView);
  controller.Subscribe(winFormsView);

  consoleView.SetController(controller);
  winFormsView.SetController(controller);

  Task.Factory.StartNew(() => consoleView.Navigate());
  Application.Run(winFormsView);
 }
}
Powstała aplikacja, która umożliwia przesuwanie znaczka po ekranie, co nie jest jakimś wyczynem. Celem tej aplikacji jest przećwiczenie teorii o MVC, na kodzie nie w ASP.NET MVC.

KOD APLIKACJI JEST NA GITHUB... TeoVincent/MVC-by-example

Update #1

Aby podkreślić istotę różnicy między Active i Passive MVC postanowiłem zmienić implementację Modelu w wersji Active oraz Kontrolera w wersji Passive. Po opublikowaniu tego artykułu, między czasie pogooglowałęm jeszcze więcej na temat Active i Passive MVC. Zauważyłem, że nie często ale powtarza się jedno błędne zrozumienie różnicy między tymi dwoma odmianami. To czy MVC jest Active nie jest determinowane tym, że występuje wzorzec Obserwator pomiędzy Modelem a Widokiem. Wyznacznikiem odmiany Passive nie jest występowanie pojedynczej referencji do Widoku w Kontrolerze. Te dwa warunki mogą zajść ale nie są konieczne do tego aby odróżnić te dwie odmiany.
Najważniejszą równicą jest fakt, że w Active MVC model może sam zmieniać swój stan i przez co on sam informuje Widok o swojej zmianie. Informowanie to może być zaimplementowane przez wzorzec Obserwator ale równie dobrze może to odbywać się po przez pojedynczą instancję obiektu Obserwatora będącego Widokiem. W odmianie Active MVC, Model sam zmienia swój stan i sam wykonuje powiadomienia Widoku.
W przypadku Passive MVC, zmianą modelu zarządza Kontroler. Model posiada logikę biznesową ale sam nie może zmienić swojego stanu. W wyniku wywołań metod Modelu po przez Kontroler, Model zmienia swój stan. Skoro to Kontroler decyduje o tym kiedy Model zmieni swój stan, to również kontroler powiadamia Widok o zmianie Modelu. Tutaj jest podobna historia jak w przypadku Active MVC. Kontroler może posiadać kolekcję Obserwatorów będących Widokami lub może posiadać pojedynczą instancję Obserwatora będącego Widokiem.
To czy dany wzorzec jest Active czy Passive zależy od tego kto jest powodem zmian stanu Modelu a nie to czy mamy zaimplementowany wzorzec Obserwator czy nie.
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<IObserver> observers;
    private readonly Task overAndOverAgain;
    private Action action;
    private const int INTERVAL = 50;
    private readonly object lockObj;

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

        observers = new List<IObserver>();

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

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

    public void IncreaseX()
    {
        lock (lockObj)
            action = IncX;
    }

    public void DecreaseX()
    {
        lock(lockObj)
            action = DecX;
    }

    public void IncreaseY()
    {
        lock (lockObj)
            action = IncY;
    }

    public void DecreaseY()
    {
        lock (lockObj)
            action = DecY;
    }

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

        Notify();
    }

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

        Notify();
    }

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

        Notify();
    }

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

        Notify();
    }

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

    private void OverAndOverAgain()
    {
        while (true)
        {
            lock (lockObj)
            {
                action();
                overAndOverAgain.Wait(INTERVAL);
            }
        }
    }
}
Poniżej, nowa wersja Kontrolera, w którym analogicznie jak w przypadku Modelu zaimplementowany jest task zmieniający stan modelu w stałym interwale czasowym.
public class Controller : IController, IObservable
{
    private readonly IModel model;
    private readonly Task overAndOverAgain;
    private Action action;
    private readonly List<IObserver> observers;
    private const int INTERVAL = 50;

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

        observers = new List<IObserver>();

        action = () => { };
        overAndOverAgain = Task.Factory.StartNew(OverAndOverAgain);
    }

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

    private void Notify()
    {
        foreach (var view in observers)
            view.Update(model.X, model.Y);
    }

    public void MoveRight()
    {
        lock (action)
            action = model.IncreaseX;
    }

    public void MoveLeft()
    {
        lock (action)
            action = model.DecreaseX;
    }

    public void MoveUp()
    {
        lock (action)
            action = model.DecreaseY;
    }

    public void MoveDown()
    {
        lock (action)
            action = model.IncreaseY;
    }

    private void OverAndOverAgain()
    {
        while (true)
        {
            lock (action)
            {
                action();
                Notify();
                overAndOverAgain.Wait(INTERVAL);
            }
        }
    }

sobota, 3 października 2015

Czasami (czasami często) słyszę, że:

...ktoś nie napisał testu jednostkowego bo nie miał na to czasu.

Gdy to słyszę to aż bolą mnie zęby.

Jak można nie mieć czasu na sprawdzenie czy nasz kod działa poprawnie?

Wtedy zawsze staram się wyjaśnić, że:

... ja nie mam czasu nie pisać testów jednostkowych.

Nie mam czasu marnować czas na uruchomienie całego środowiska i wykonania wszystkich kliknięć w aplikacji aby sprawdzić czy przed chwilą napisany kawałek kodu działa zgodnie z moimi oczekiwaniami.

Załóżmy że: w przeszłości nie miałeś wystarczająco dużo czasu na napisanie testu jednostkowego aby sprawdzić czy aplikacja działa poprawnie. Przeklikałeś aplikację i z większym lub mniejszym strachem powiedziałeś, że działa.

Załużmy teraz że: w przyszłości, zmieniłeś kod w taki sposób, że obawiasz się czy kilka poprzednich funkcjonalności nadal działa poprawnie. Musisz teraz przeklikać wszystkie te przypadki użycia w ramach tych newralgicznych funkcjonalności.

A gdybyś: poświęcił czas na napisanie testu jednostkowego zamiast poświęcić go na klikanie to w przyszłości nie poświęciłbyś ani sekundy na ponowne klikanie - testy jednostkowe sprawdziłby ten kod za ciebie. Nie ma strachu o błędy regresyjne.

Tak zaoszczędzony czas pozwoli ci na napisanie więcej nowych funkcjonalności z testami jednostkowymi, na które nigdy nie miałeś czasu bo musiałeś klikać po aplikacji.

Nie wszędzie, ale może tak być, że zarząd się ani nie ucieszy, ani ucieszy, gdy będziesz się chwalić, że przetestowałeś swój kod testami jednostkowymi. Zarząd może kompletnie nie kminić o co ci chodzi.

Ale jeśli powiesz, że teraz możesz pisać więcej funkcjonalności w tym samym czasie z mniejszą liczbą błędów to nie wszędzie, ale może tak być, że Cię przynajmniej pochwalą.

Każde takie manualne przetestowanie aplikacji zaraz po najbliższej zmianie kodu jest nic nie warte. Po każdej zmianie kodu czas poświęcony na manualne przetestowanie wyrzucany jest do kosza. Czas, który włożyliśmy w manualne testowanie nigdy już w przyszłości nam się nie zwróci. Czas ten jest czasem bezpowrotnie zmarnowanym.

Więc jeśli mamy mało czasu (a zawsze mamy go mało) to oszczędzajmy go pisząc test jednostkowy, zamiast trwonić go robiąc testy manualne.

Napisanie testu jednostkowego jest o wiele szybsze od wielokrotnego przeklikiwania aplikacji na nowo po każdej zmianie. Czas włożony w stworzenie testu jednostkowego, będzie nam się zawsze zwracał, przez cały czas trwania projektu.

Zmiast szastać czasem na klikanie po aplikacji w celu jej przetestowania, ten sam czas wykorzystaj na napisanie testu jednostkowego. Jeśli poświęcisz tyle samo czasu na pisanie testu jednostkowego ile potrzebowałbyś przeznaczyć na uruchomienie aplikacji i wykonanie scenariusza testowego manualnie, już zaoszczędziłeś czas. Pisząc test jednostkowy będziesz mógł wykonywać ten sam test wiele razy w przyszłości, nie poświęcając na niego ani sekundy.

No i oczywiście jest WIELE innych zalet pisania testów jednostkowych takich jak na przykład wymuszenie na programiście pisania kodu obiektowego. Ale nie o tym tutaj w tym poście... W tym poście opisuje sytuację gdy nie ma czasu na nic, więc na obiektowość też nie ma czasu :).
 
Gdy nie masz na nic czasu to radzę zacząć pisać testy jednostkowe aby ten czas sobie zaoszczędzić.
 
 




niedziela, 20 września 2015

Trafiłeś do pierwszego artykułu serii, kolejne artykuły najlepiej będzie jak odpalisz 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...)

Model

  • Model zawiera kolekcję obserwatorów.
  • Gdy model zmienia swój stan, powiadamia o tym swoich obserwatorów.
  • Obserwatorzy posiadają publiczną metodę Update, którą model wywołuje, gdy się zmieni, w celu przekazania obserwatorom swojego nowego stanu.
  • Innymi słowy, model jest obiektem obserwowanym.
  • Model posiada publiczną metodę Subscribe w celu dodawania do swojej kolekcji obserwatorów, którzy są zainteresowani otrzymywaniem od modelu powiadomień o jego zmianie.
  • Model posiada, również metodę Notify, która iteruje kolekcję obserwatorów aby na każdym z tych obserwatorów wywołać metodę Update, dzięki której obserwatorzy są informowani o zmianie modelu.
  • Metoda Notify jest wywoływana z wnętrza modelu, gdy stan modelu się zmienił. Decyzja - kiedy ta metoda zostanie wywołana - spoczywa na szczególnej implementacja logiki modelu, więc metoda Notify jest nie publiczna.
  • Tymi obserwatorami modelu są obiekty Widoku.
  • Między modelem a widokami jest wzorzec Obserwator, gdzie widok jest obserwatorem a model jest obiektem obserwowanym.
  • Każdy model jest obserwowany czyli musi posiadać swoich obserwatorów, więc obsługę subskrybowania obserwatorów można napisać raz w klasie abstrakcyjnej Observable.
KOD APLIKACJI JEST NA GITHUB... TeoVincent/MVC-by-example

public abstract class Observable
{
    protected readonly List<IObserver> observers;

    protected Observable()
    {
        observers = new List<IObserver>();
    }

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

    protected abstract void Notify();
}
  • Metoda Notify implementowana jest bezpośrednio w modelu, w którym ma ona dostęp do aktualnego stanu tego modelu.
public class Model : Observable
{
    private int x;
    private int y;

    protected override void Notify()
    {
        foreach (var observer in observers)
            observer.Update(x, y);
    }
}
  • Powyższa implementacja klasy modelu jest niepełna, brakuje w niej przynajmniej jednej publicznej metody, która zmodyfikuje stan modelu.
  • Na potrzeby przykładu zdefiniuję cztery metody zwiększające i zmniejszające prywatne własności x i y, a każda zmiana tych własności zostanie rozgłoszona wśród obserwatorów za pomocą metody Notify.
  • Model powinien implementować interfejs Modelu, w którym zdefiniowane są publiczne metody wywołujące akcje na modelu, prowadzące do jego zmiany stanu.
public interface IModel
{
    void IncreaseX();
    void DecreaseX();
    void IncreaseY();
    void DecreaseY();
}

public class Model : Observable, IModel
{
    private int x;
    private int y;

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

    public void IncreaseX()
    {
        x++;
        Notify();
    }

    public void DecreaseX()
    {
        x--;
        Notify();
    }

    public void IncreaseY()
    {
        y++;
        Notify();
    }

    public void DecreaseY()
    {
        y--;
        Notify();
    }
}

Widok

  • Skoro widok jest obserwatorem modelu, to implementuje interfejs IObserver, w którym zdefiniowana jest metoda Update.
  • Widok otrzymuje powiadomienia o zmianie modelu za pomocą metody Update, którą wywołuje model przekazując do argumentów tej metody informacje o jego nowym stanie.
public interface IObserver
{
    void Update(int x, int y);
}
  • Metoda Update widoku bazując na argumentach przekazanych od modelu, odświeża swój widok.
  • Poniżej przedstawiam przykładową implementację widoku, który jest konsolą. W wyniku wywołania metody Update, widok maluje się na nowo umieszczając znak '#' na miejscu przesuniętym w prawo o wartość 'x' oraz w dół o wartość 'y'. Za każdym razem gdy model zmieni swój stan, czyli gdy zmienne x i y zmienią się w modelu, wywoła on metodę Update przekazując te wartości do widoku, który namaluje się ponownie umieszczając znak '#' w nowym miejscu.
public class ConsoleView : IObserver
{
    private readonly int width;
    private readonly int height;

    public ConsoleView(int width, int height)
    {
        this.width = width;
        this.height = height;
    }

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

    private void Render(int x, int y)
    {
        Console.Clear();

        for (int i = 0; i < height; ++i)
        {
            for (int j = 0; j < width; ++j)
            {
                if (y == i && x == j)
                    Console.Write("#");
                else
                    Console.Write(" ");
            }
            Console.WriteLine();
        }
    }
}
  • Widać tutaj, że możemy implementować wiele różnych rodzajów widoków bez konieczności zmiany implementacji modelu. Wystarczy tylko zaimplementować interface IObserver.
  • Inne przykłady widoku, jakie można by zaimplementować to np. WinForms, lub bardziej odjechane, nic nie mające wspólnego z monitorem a na przykład z głośnikami, jak adapter TTS "Text To Speech", który po każdym uaktualnieniu powie nowe współrzędne x i y.
  • Aby użytkownik mógł za pomocą widoku sterować aplikacją, widok zawiera w sobie instancję Kontrolera.
  • Widok zawiera publiczną metodę SetController, za pomocą której, przypisuje sobie ten kontroler.
  • Kontroler będzie służył widokowi do delegowania akcji, jakie wykonał na widoku użytkownik.
  • Metoda SetController definiowana jest w interfejsie, który każdy widok implementuje.
public interface IView
{
    void SetController(IController controller);
}

public class ConsoleView : IObserver, IView
{
    private IController controller;

    // ... pozostała cześć klasy pozostaje niezmieniona
        
    public void SetController(IController controller)
    {
            this.controller = controller;
    }
}
  • Między widokiem a kontrolerem jest wzorzec Strategia. Kontroler jest strategią widoku.
  • W obiekcie widoku znajdują się metody umożliwiające interakcję użytkownika z widokiem.
  • Widok nie wie jak ma zostać obsłużona dana akcja podjęta przez użytkownika.
  • Widok ma dwa zadania:
    • pierwsze - bycie interfejsem wyświetlającym informacje, o których został poinformowany przez model za pośrednictwem metody Update
    • drugie - umożliwienie interakcji z użytkownikiem, przekazując wywołania do kontrolera, który podejmie decyzję co zrobić z danym żądaniem
  • Zadaniem widoku nie jest obsługa interakcji użytkownika.
  • Widok oddelegowuje żądania użytkownika do kontrolera.
  • Na potrzeby przykładu zaimplementuję możliwość zmieniania wartości x i y za pomocą strzałek na klawiaturze. Zdarzenia wciskania strzałek widok oddeleguje do kontrolera wywołując na tym kontrolerze odpowiednie metody.
  • Poniżej jeszcze raz, tym razem już kompletna implementacja klasy ConsoleView. W porównaniu do poprzednich implementacji pojawiła się nowa metoda Navigate, która czeka na przyśnięcia strzałek klawiatury.
public class ConsoleView : IObserver, IView
{
    private IController controller;

    private readonly int width;
    private readonly int height;

    public ConsoleView(int width, int height)
    {
        this.width = width;
        this.height = height;
    }

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

    private void Render(int x, int y)
    {
        Console.Clear();

        for (int i = 0; i < height; ++i)
        {
            for (int j = 0; j < width; ++j)
            {
                if (y == i && x == j)
                    Console.Write("#");
                else
                    Console.Write(" ");
            }
            Console.WriteLine();
        }
    }

    public void SetController(IController controller)
    {
        this.controller = controller;
    }

    public void Navigate()
    {
        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;
            }
        }
    }
}

Kontroler

  • Kontroler zapewnia widokowi inteligencję.
  • Zawiera instancję modelu, do którego deleguje wywołania otrzymane od widoku.
  • Obsługuje wywołania otrzymane od widoku.
  • Pobiera dane wejściowe użytkownika i określa ich znaczenie dla modelu. Odpowiada za interpretację danych wejściowych i wykonanie odpowiednich operacji na modelu.
  • Żąda od modelu zmiany stanu.
  • Określa, jakie operacje powinny być wykonane na modelu.
  • Oddziela logikę sterowania od widoku separując od siebie widok i model.
  • Między widokiem a kontrolerem jest wzorzec Strategia. Kontroler jest zachowaniem widoku.
  • Zmieniając kontroler zmienimy zachowanie widoku, bez konieczności zmiany widoku.
  • Widok jest obiektem konfigurowalnym za pomocą odpowiedniej strategii, którą zapewnia kontroler.
  • Najpierw widok przekazuje kontrolerowi informacje o czynnościach użytkownika, następnie kontroler inicjuje operacje modelu.
  • Kontroler może wykonywać pewną pracę związaną z określaniem, jakie metody modelu mają zostać wywołane, ale nigdy nie należy to do funkcji określanych jako, "logika aplikacji". Logika aplikacji to kod, który zarządza i operuje danymi. Jest on w całości zawarty w modelu.
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();
    }
}

Nowy wydajniejszy widok

Dotychczas zaimplementowany widok jest mało wydajny. Każde ponowne narysowanie znaku '#' pociąga za sobą drukowanie wszystkich wierszy konsoli na nowo.
Napisałem nową wydajniejszą wersję widoku. Dzięki wzorcu MVC logika aplikacji od wyświetlania interfejsu jest sprytnie odzielona, tak, że napisanie wydajnieszej wersji widoku nie pociąga za sobą żadnych zmian w logice aplikacji. Nie ma innych zmian w całej aplikacji, jest tylko nowa wersja widoku.
W nowym widoku nie rysuję wszystkich linii na nowo. Efekt poruszania się znaku '#' rozwiązałem za pomocą metody SetCursorPosition klasy Console. Dodatkowo pokusiłem się o namalowanie tła. Implementacja wygląda tak:
public class ConsoleView : IObserver, IView
{
    private IController controller;

    private readonly int width;
    private readonly int height;

    private int currentX;
    private int currentY;

    public ConsoleView(int width, int height)
    {
        this.width = width;
        this.height = height;
        RenderBackground();
    }

    public void SetController(IController controller)
    {
        this.controller = controller;
    }

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

    public void Navigate()
    {
        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;
            }
        }
    }

    private 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;
        }
    }

    private void RenderBackground()
    {
        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);
    }
}

WinForms widok

W celach ćwiczebno-zaczepnych zaimplementowałem, tak dla sportu, nową całkiem inną wersję widoku, opartą o inną technologię budowania interfejsu, nie zmieniając przy tym ani jednej linijki kodu w logice aplikacji.
public partial class WinFormView : Form, IObserver, IView
{
    private IController controller;

    private readonly int width;
    private readonly int height;
    private readonly int scale;

    private readonly Graphics graphics;

    public WinFormView(int width, int height, int scale)
    {
        this.width = width*scale;
        this.height = height*scale;
        this.scale = scale;

        InitializeComponent();
        ClientSize = new Size(this.width, this.height);
        graphics = CreateGraphics();
    }

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

    public void SetController(IController controller)
    {
        this.controller = controller;
    }

    private void Render(int x, int y)
    {
        ClearLast();
        PrintNew(x, y);
    }

    private void ClearLast()
    {
        graphics.Clear(Color.Azure);
    }

    private void PrintNew(int x, int y)
    {
        var point = new Point(x*scale, y*scale);
        var size = new Size(scale, scale);
        var rectangle = new Rectangle(point, size);
        graphics.FillEllipse(Brushes.Black, rectangle);
    }

    protected override bool ProcessCmdKey(ref Message msg, Keys keyData)
    {
        if (keyData == Keys.Up)
        {
            controller.MoveUp();
            return true;
        }

        if (keyData == Keys.Down)
        {
            controller.MoveDown();
            return true;
        }

        if (keyData == Keys.Left)
        {
            controller.MoveLeft();
            return true;
        }

        if (keyData == Keys.Right)
        {
            controller.MoveRight();
            return true;
        }

        return base.ProcessCmdKey(ref msg, keyData);
    }
}
Na starcie aplikacji, należy stworzyć nowy obiekt widoku, zarejestrować go jako słuchacza w modelu, oraz wpisać do tego widoku kontroler.
internal class Program
{
    private static void Main(string[] args)
    {
        int width = 50;
        int height = 50;
        int scale = 10;

        var model = new Model();
        var controller = new Controller(model);
        var consoleView = new ConsoleView(width, height);
        var winFormsView = new WinFormView(width, height, scale);

        model.Subscribe(consoleView);
        model.Subscribe(winFormsView);

        consoleView.SetController(controller);
        winFormsView.SetController(controller);

        Task.Factory.StartNew(() => consoleView.Navigate());
        Application.Run(winFormsView);
    }
}

Wzorcowi MVC mówię SPRAWDZAM

zmieniając logikę w modelu i nie zmieniając ani jednej linijki w obu widokach i ich kontrolerze.
Nowa logika, wnosi do modelu cztery ograniczenia:
  • Wartości x i y nie mogą być mniejsze niż zero.
  • Wartości x i y nie mogą być większe niż zdefiniowane ograniczenie.
  • Gdy x lub y posiada wartość zero i zostanie wywołane żądanie zmniejszenia to nowa wartość ma wynosić największą możliwą.
  • Gdy x lub y osiągnie największą możliwą wartość i zostanie wywołane żądanie zwiększenia wówczas wartość ta ma się wyzerować.
public class Model : Observable, IModel
{
    private readonly int minX;
    private readonly int maxX;

    private readonly int minY;
    private readonly int maxY;

    private int x;
    private int y;

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

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

    public void IncreaseX()
    {
        if (x == maxX - 1)
            x = 0;
        else
            x++;

        Notify();
    }

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

        Notify();
    }

    public void IncreaseY()
    {
        if (y == maxY - 1)
            y = 0;
        else
            y++;

        Notify();
    }

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

        Notify();
    }
}
Wszystko działa jak się można było spodziewać. Wciskając klawisze nawigacyjne klawiatury, przesuwamy kursorem w wybranym kierunku, a jeśli osiągniemy granicę przestrzeni, wówczas przeskakujemy na przeciwległą stronę powierzchni, i ani jedna linijka widoku i kontrolera nie została przy tym zmieniona.

Na koniec

Powstała aplikacja, która umożliwia przesuwanie znaczka po ekranie, co nie jest jakimś wyczynem. Celem tej aplikacji jest przećwiczenie teori o MVC, na kodzie nie w ASP.NET MVC.
C.D.N... Będzie jeszcze Passive MVC ...
KOD APLIKACJI JEST NA GITHUB... TeoVincent/MVC-by-example

Update #1

Aby podkreślić cechę, która charakteryzuję odmianę Active MVC zmodyfikowałem implementację Modelu.
W poprzedniej wersji (powyżej) Model zmieniał swoje wartości gdy Kontroler wywołał na Modelu odpowiednie metody. Kontroler wywoływał te metody w wyniku przyciśnięcia na widoku strzałek klawiatury. Efekt był taki, że w wyniku przyciskania strzałek kropka na Widoku przesuwała się o jedno miejsce w danym kierunku.
W nowej wersji zaimplementowałem metodę OverAndOverAgain, która w dodatkowym tasku, non-stop zwiększa lub zmniejsza zmienną x lub y w stałych odstępach czasu. Przyciśnięcie strzałek powoduje wybór czy będzie to zwiększanie czy zmniejszanie, oraz czy zmieniać się będzie wartość x czy y. Efekt jest taki, że kropka niezależnie, cały czas przesuwa się po ekranie, a przyciśnięcie strzałek na klawiaturze zmienia kierunek tego poruszania.
Na tym przykładzie widać, wyraźniej, jak na dłoni, że Model zmienia swój stan niezależnie od Kontrolera oraz Model niezależnie od Kontrolera odpowiedzialny jest za powiadamianie Widoków o zmianie jego stanu.
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<IObserver> observers;
    private readonly Task overAndOverAgain;
    private Action action;
    private const int INTERVAL = 50;
    private readonly object lockObj;

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

        observers = new List<IObserver>();

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

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

    public void IncreaseX()
    {
        lock (lockObj)
            action = IncX;
    }

    public void DecreaseX()
    {
        lock(lockObj)
            action = DecX;
    }

    public void IncreaseY()
    {
        lock (lockObj)
            action = IncY;
    }

    public void DecreaseY()
    {
        lock (lockObj)
            action = DecY;
    }

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

        Notify();
    }

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

        Notify();
    }

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

        Notify();
    }

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

        Notify();
    }

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

    private void OverAndOverAgain()
    {
        while (true)
        {
            lock (lockObj)
            {
                action();
                overAndOverAgain.Wait(INTERVAL);
            }
        }
    }
}

niedziela, 6 września 2015


Kalkulator napisany z pomocą BDD (Behavior-driven development)Postawiłem sobie za cel napisanie kalkulatora przy użyciu metody BDD a wynikami mojej pracy chcę się z wami podzielić. Kalkulator ten będzie zachowywać się jak ten pokazany na obrazku. Ja mam jeszcze taki w domu - ma chyba już 20 lat. Ten, który tutaj zaimplementuje będzie posiadał, tak jak ten oryginalny, kilka dziwnych zachowań takich jak na przykład: przy każdym ponownym wciśnięciu "=" liczba na wyświetlaczu będzie się zwiększać o jeden jeśli wcześniej wykonaliśmy działanie "1+1".

Zapraszam do foto-reportażo-tutoriału, w którym pokażę krok, po kroku, bardzo szczegółowo jakie narzędzia zainstalowałem oraz jak zmieniał się kod podczas kolejnych, czerwono-zielonych faz.


Pod każdym zdjęciem znajdują się krótkie opisy tego co aktualnie się wydarzyło. Aby te komentarze były widoczne należy włączyć informacje o zdjęciu, klikając w prawym, górnym rogu zdjęcia na ikonkę z literą "i".

Testy będą miały postać zwykłego teksu. Tekst ten będzie źródłem do wygenerowania (w tle) testów jednostkowych, których spełnienie będzie oznaczało spełnienie danego wymagania biznesowego.


Widok tego kalkulatora przypomniał mi piękne czasy lat 90-tych. Kto nie miał kiedyś takiego kalkulatora? Kto kiedyś nie bawił się w nieskończoność wciskając różne kombinacje klawiszy aby wygenerować w lewym dolnym rogu ekranu literę 'E'. Kto nie bił rekordu ilości wciśnięć klawisza "=" w określonym przedziale czasowym.

 

 

poniedziałek, 25 maja 2015


Poniższy cytat jest brutalną prawdą, esencją strasznej zarazy panującej w firmach, która z zimną krwią wyniszcza wszystko co jej stanie na drodze.

Miałem okazję przyjrzeć się tej sytuacji z bardzo bliska. Cieszę się, że jestem już dożywotnio zaszczepiony na wypadek tego wirusa.

Po tym doświadczeniu, zdefiniowałem na nowo co to znaczy dobry programista. Warunkiem niewystarczającym ale koniecznym, jaki musi spełnić dobry programista to: nie pozwolić nigdy na realizację scenariusza "Szybciej, wolniej, wolniej." Dobry programista, to taki, który się nie ugnie pod rzeszą managerów, szefów i innych takich. Dobry programista nie weźmie udziału w scenariuszu "Szybciej, wolniej wolniej" mając na względzie troskę o dobro projektu, firmy i swoich szefów, chcąc uchronić ich przed straszliwą, długą i wyniszczającą wszystkich, powolną klęską.

"Systematyczne lekceważenie planowania i projektowania prowadzi do rozwoju w cyklu "szybciej, wolniej, wolniej". Wygląda to mniej więcej tak:

(1) Błyskawicznie dostarczasz wersję 1.0, pisząc cały kod na kolanie.

(2) Budujesz wersję 2.0, na bieżąco rozwiązując problemy stwarzane przez uciążliwy bałagan w kodzie.

(3) Wraz z kolejnymi wersjami rozwiązywanie problemów ze starym kodem "na bieżąco" sprawia, że bałaganu przybywa, a praca staje się coraz wolniejsza. Wyszyscy stopniowo tracą wiarę w system, programistów i całą sytuację, w której się znaleźli.

(4) Gdzieś w okolicach wersji 4.0 zgajesz sobie sprawę, że nie wygrasz. Zaczynasz rozważać opcję przepisania systemu od podstaw."

"Refaktoryzacja do wzorców projektowych" Joshua Kerievsky