poniedziałek, 19 grudnia 2011

Pasywny widok cd. i antywzorzec Magiczna Metoda

    W poprzednim poście opisałem wzorzec projektowy Pasywny Widok. Tomek w komentarzu do wpisu słusznie zauważył, że mój kod jest trochę przekombinowany. Ponadto gdy spojrzałem jeszcze raz na ten kod zauważyłem w nim pewien smrodek (nawiasem mówiąc, jak powinno się przetłumaczyć na polski angielskie "code smell"?). Myślę, że warto wrócić do tamtego kodu raz jeszcze.
    Najpierw opiszę, o co chodzi z "przekombinowaniem". Zdefiniowałem interfejs INameModel:


public interface INameModel
{
    string Name { getset; }
    event Action<string> NameChanged;
}


A potem konkretną klasę:


public class NameModel: INameModel
{
    
string name;
  
    
public string Name {
        
get { return name; }
        
set
        {
            name = 
value;
            
OnNameChanged(name);
        }
    }
  
    
public event Action<string> NameChanged;
  
    
protected virtual void OnNameChanged(string obj)
    {
        
if (NameChanged != null) {
            
NameChanged(obj);
        }
    }
}

 
    Po przemyśleniu uznałem, że choć nie jest to błąd, jest to niepotrzebny przerost formy nad treścią. Generalnie, interfejsy mają sens w dwóch przypadkach:
  1. gdy klasa implementująca jest serwisem,
  2. gdy dana implementacja klasy-wartości nie jest jedyną słuszną implementacją (np. interfejs IList jest implementowany przez LinkedList i ArrayList).
Wydaje się, że w przypadku klasy NameModel nie zachodzi żaden z powyższych przypadków. Sklasyfikowałbym :-) ją jako typową klasę-wartość. Odpalenie zdarzenia po wywołaniu settera to chyba nie jest żadna logika. Poza tym, w przypadku stosowania wzorca Pasywny Widok, dodalibyśmy sobie snippet generujący takie kilkanaście linii kodu. A jak coś jest generowane przez snippet, to to nie jest logika, tylko składnia języka :-) Czy ktoś czuje się jeszcze nieprzekonany, że NameModel nie różni się zasadniczo od takich klas, jak String czy Uri? Ja sam siebie przekonałem :-)
    To teraz uderzę się w piersi i opiszę smrodek, który napisałem. Użyłem antywzorca, który nazwałbym Magiczną Metodą (jeśli ktoś zna fachową nazwę, proszę o komentarz). Polega on na tym, że obiekt nie jest gotowy do użycia pomimo tego, że został legalnie utworzony przy użyciu konstruktora. Po prostu, konstruktor nie inicjuje wszystkich pól zostawiając część wynullowanymi. Na konkretnym przykładzie z poprzedniego postu:


public class NameController: INameController
{
    
private readonly INameValidator validator;
    
private INameModel model;
    
private INameView view;
  
    
public NameController(INameValidator validator)
    {
        
this.validator = validator;
    }
  
    
public void StartControl(INameModel model, INameView view)
    {
        
this.view = view;
        
this.model = model;
        view.
RegisterObserver(this);
        
RegisterFeedback();
    }
  
    
private void RegisterFeedback()
    {
        model.NameChanged += newName =>
            view.ErrorMessage = validator.
ErrorMessage(newName);
    }      
  
    
public string Name {
        
set {
            model.Name = 
value;
        }
    }

 
W powyższej klasie, dopóki nie wywołamy magicznej metody StartControl, nie możemy wywoływać innych metod, bo będziemy dostawać NullReferenceException. A tak nie powinno być w żadnym wypadku.
Kontroler powinien wyglądać albo tak:


public class NameController: INameController
{
    
private readonly INameValidator validator;
    
private readonly INameView view;
    
private readonly NameModel model = new NameModel();
  
    
public NameController(
        INameValidator validator, INameView view)
    {
        
this.validator = validator;
        
this.view = view;
    }
  
    
public void StartControl()
    {
        view.
RegisterObserver(this);
        
RegisterFeedback();
    }
    [..]
}



albo, ewentualnie, tak:


public class NameController<TView>: INameController
    where TView: INameView, new()
{
    private readonly INameValidator validator;
    private readonly INameView view = new TView();
    private readonly NameModel model = new NameModel();
  
    public NameController(INameValidator validator)
    {
        this.validator = validator;
    }
  
    public void StartControl()
    {
        view.RegisterObserver(this);
        RegisterFeedback();
    }
    [..]

}


    W powyższych kawałkach kodu widać, że widok również traktujemy jako wartość, a nie serwis, ale w przeciwieństwie do modelu, chcielibyśmy żeby był podmienialny jakimś dublerem testowym (bo prawdziwy widok jest ciężkim i wolnym obiektem; patrz: punkt b) na początku wpisu).


PS
Jeśli ktoś nie spotkał się z pojęciem dublera testowego, odsyłam do strony Martina Fowlera, który chyba jako pierwszy użył tego terminu.
W tym wpisie odwoływałem się do klasyfikacji obiektów na serwisy i wartości. Jeśli taki podział nie jest jasny, proszę dać znać w komentarzu - w razie potrzeby napiszę o tym coś więcej.

4 komentarze:

  1. Magiczna metoda - nie taki diabeł straszny jak go malują ;)

    Wszystko zależy od tego co tak naprawdę potrzebujemy. Jeżeli na etapie projektowania wiadomo, że jeden kontroler kontroluje jeden widok, to w rzeczy samej używanie "magicznej metody" jest błędem.

    Natomiast, gdy jeden kontroler kontroluje kilka widoków, deklaracja "magicznej metody" jest jak najbardziej na miejscu. Istotne jest tu, aby użyć prawidłowej, intuicyjnej nazwy. Z reguły w takich sytuacjach "magiczne metody" powinny występować parami np:
    - BindView()
    - UnbindView()

    PS. Jestem zwolennikiem używania interfejsów jak najczęściej się da (nawet, gdy mamy do czynienia jedynie z obiektami-wartościami)

    PS2. Zawsze należy określić granicę, gdzie kończy się abstrakcja. Za tą granicą kończą się wzorce projektowe i zaczyna się zwykłe, bezwzorcowe pisanie kodu.

    OdpowiedzUsuń
  2. W odróżnieniu od Bartosza, uważam 'magiczne metody' za złą praktykę, jeżeli nie w każdej sytuacji, to w tej z pewnością. Dlaczego możemy chcieć, by jedna instancja kontrolera obsługiwała wiele widoków? Jeśli powodem jest jego stanowość - mamy źródło problemu. Zbyt kosztowna inicjalizacja, by mieć osobną instancję na każdy widok? Zdecydowanie poświęciłbym czas na zbadanie, czy nie jest on 'feature envy'.
    Z moich doświadczeń i poszukiwań właściwego stosowania architektury MVC wyciągnąłem jeden wniosek. Przysparza ona najmniej problemów, gdy graf obiektów składających się na aplikację konstruowany jest przy starcie aplikacji i w czasie jej trwania jego jedynym zadaniem jest przetwarzanie danych czy reagowanie na zdarzenia. Każde miejsce, w który możliwa jest jego zmiana jest źródłem problemów, często w czasie wykonania, czyli tych najbardziej przykrych
    Sprawne testowanie takich klas jest osobnym problemem. W tym miejscu polecam blog Misko Hevery'ego (http://misko.hevery.com/).

    OdpowiedzUsuń
  3. Nareszcie konkretny wpis. Poziom rośnie z postu na post :)

    OdpowiedzUsuń
  4. @Piotr spieszę z wyjaśnieniami :) Do konstruktora obiektu przekazywać należy wyłącznie elementy niezbędne do prawidłowego stworzenia obiektu. Nie oznacza to, że w bliżej nieokreślonej przyszłości takiemu obiektowi nie można podać dodatkowych "informacji" do pracy.

    Przykład z życia codziennego:

    Weźmy np. młynek do kawy. Do jego zbudowania potrzebnych jest nam wiele elementów, ale na pewno nie jest nam potrzebna ani kawa, ani źródło zasilania.

    W moim rozumieniu "magiczną metodą" jest właśnie wsypywanie kawy, lub podłączanie/odłączanie urządzenia do/z prądu ;)

    OdpowiedzUsuń