MVP - prośba o sprawdzenie implementacji i wyjaśnienie kilku wątpliwości

0

Witam, ostatnio zacząłem uczyć się wzorca MVP, ale nie jestem do końca pewny czy dobrze go rozumiem.
Tak przedstawia się kod, który napisałem.

Interfejsy:

interface ILoginPresenter
{
    bool CanLogIn();
}

interface ILoginView
{
    LoginData GetLoginData();
}

Modele:

class Logger
    {
        public LoginData LoginData { get; set; }

        public Logger(LoginData loginData)
        {
            LoginData = loginData;
        }

        public bool CanLogIn()
        {
            var users = new UsersDataContext();
            var login = LoginData.Login;
            var password = LoginData.Password;

            return users.Users.
                Where(user => user.Login == login && user.Password == password).
                Count() == 1;
        }
    }

public class LoginData
    {
        public string Login { get; set; }
        public string Password { get; set; }
    }

Prezenter:

class LoginPresenter : ILoginPresenter
    {
        private readonly ILoginView view;
        
        public LoginPresenter(ILoginView view)
        {
            this.view = view;
        }

        public bool CanLogIn()
        {
            LoginData loginData = view.GetLoginData();
            var logger = new Logger(loginData);

            return logger.CanLogIn();
        }
    }

Widok:

public partial class LoginView : Form, ILoginView
    {
        private readonly ILoginPresenter presenter;

        public LoginView()
        {
            InitializeComponent();

            presenter = new LoginPresenter(this);
        }

        public LoginData GetLoginData()
        {
            string login = LoginTextBox.Text;
            string password = PasswordTextBox.Text;

            return new LoginData { Login = login, Password = password };
        }

        private void LogInButton_Click(object sender, EventArgs e)
        {
            bool canLogIn = presenter.CanLogIn();

            MessageBox.Show(canLogIn ? "Można." : "Nie można.");
        }
    }

No i kilku rzeczy nie jestem pewny lub nie rozumiem, a mianiowicie:

  1. Między widokiem a prezenterem istnieje relacja jeden do jednego?
  2. Mogę w widoku wykorzystywać klasy z modeli - tak jak tutaj np. LoginData?
  3. W prezenterze powinienem używać już gotowych klas(modeli?), które mają w sobie jakąś logikę?
  4. Prezenter powinien działać na zasadzie prześlij z widoku do modelu i na odwrót, np. update do bazy albo pobieranie danych z bazy?
  5. Logika w prezenterze powinna dotyczyć tylko pośrednictwa między widokiem, a modelem?
  6. Pola view i presenter powinny być readonly?
  7. Czy moja implementacja jest poprawna? Wiem, że przykład banalny, ale chciałem się skupić na prawidłowym zrozumieniu MVP.
  8. W klasie LoginPresenter w funkcji CanLogIn powinienem użyć ILoginData i ILogger czy LoginData i Logger?
1

Co prawda inny język, ale myślę, że ten link rozjaśni różnice między mvc a mvp i ich zasadę działania. http://blog.karolak.it/2010/11/17/wzorzec-projektowy-zastosowany-w-cakephp-mvc-czy-mvp/

0

Czyli weźmy sobie przykład z logowania. Widok wywołuje metodę w prezenterze. Prezenter pobiera dane z widoku, a następnie pobiera z modelu dane o użytkowniku przez login. Teraz prezenter sprawdza czy istnieje taki użytkownik(jest różny od null), czy hasła pasują i wysyła do widoku dane o użytkowniku lub null jeśli nie ma takiego?

A jeśli chciałbym dodać walidatory(jakieś tam klasy związane z walidacją) to gdzie powinny się ona znajdować? W folderze z prezenterami? Czy może lepiej byłoby np. stworzyć statyczną klasę z metodami do walidacji albo metody zamieścić już bezpośrednio w prezenterze?

1
Degusto napisał(a):
  1. Między widokiem a prezenterem istnieje relacja jeden do jednego?
  2. Mogę w widoku wykorzystywać klasy z modeli - tak jak tutaj np. LoginData?
  3. W prezenterze powinienem używać już gotowych klas(modeli?), które mają w sobie jakąś logikę?
  4. Prezenter powinien działać na zasadzie prześlij z widoku do modelu i na odwrót, np. update do bazy albo pobieranie danych z bazy?
  5. Logika w prezenterze powinna dotyczyć tylko pośrednictwa między widokiem, a modelem?
  6. Pola view i presenter powinny być readonly?
  7. Czy moja implementacja jest poprawna? Wiem, że przykład banalny, ale chciałem się skupić na prawidłowym zrozumieniu MVP.
  8. W klasie LoginPresenter w funkcji CanLogIn powinienem użyć ILoginData i ILogger czy LoginData i Logger?
  1. Generalnie jeden widok wymaga jednego prezentera, więc owszem, jest to powiązanie 1:1. (Lepiej unikać słowa "relacja".)
  2. Niby możesz, chociaż przy prawdziwej aplikacji te warstwy mogą być od siebie fizycznie odseparowane, więc w Widoku możesz nie mieć dostępu do Modelu. Dlatego dobrą praktyką jest tworzenie viewmodeli - małych lekkich klas zawierających dane potrzebne do wyświetlenia/pobrania z Widoku. Twój LoginData to właśnie taki viewmodel, a prawdziwa klasa 'User' zmapowana z bazą danych może mieć dużo więcej właściwości.
  3. Tak.

4 i 5) Prezenter zajmuje się logiką prezentacji, czyli:
a) pobiera dane z Modelu i przekazuje je odpowiedniemu Widokowi do wyświetlenia;
b) odbiera dane z Widoku i przekazuje je do przetwarzania odpowiedniemu Modelowi;
c) po wykonaniu jakiejś akcji przez Widok/Model, decyduje co zrobić, czy np. wyświetlić odświeżony Widok, czy też przekierować do innego Widoku;
d) w przypadku nastąpienia jakiegoś błędu, czy to po stronie Widoku czy Modelu, wyświetla Widok z komunikatem o błędu;
e) decyduje, które sekcje Widoku powinny zostać wyświetlone użytkownikowi, które są możliwe do edycji, a które dostępne tylko w trybie do odczytu.

  1. Tak, właściwie jak wszystkie pola.
  2. Implementacja MVP właściwie tak, sam kod niekoniecznie:

a) Klasa Logger:

  1. Jej nazwa - "Logger" to zazwyczaj coś, co służy do logowania (w sensie audytu) tego, co robi program np. do pliku tekstowego.
  2. Czemu służy publiczna właściwość LoginData?
  3. Czemu jest inicjalizowana w konstruktorze?
  4. Dlaczego po prostu metoda CanLogIn nie przyjmie danych w argumencie i nie zwróci wyniku? Tak byłoby dużo prościej, krócej i uniknąłbyś stanu w tej klasie. Obecnie można zmienić wartość LoginData, przed wywołaniem CanLogIn, psując w ten sposób działanie aplikacji.
  5. Po co Where, skoro do Count też możesz podać warunek?

b) Klasa LoginData:

Tu też masz stan, a przecież można zrobić prywatne settery i konstruktor ustawiający te wartości. Dzięki czemu obiekt staje się niezmienny, więc nie da się go zepsuć po utworzeniu. A do tego nie trzeba pisać nazw właściwości przy tworzeniu obiektu.

  1. Nie ma sensu robienie interfejsów tylko po to, żeby były. Jeśli nie ma takiej potrzeby, to nie ma po co tracić na to czasu. Moim zdaniem nie ma nawet sensu istnienie IPresentera. Interfejsy mają sens, jeśli poszczególne klasy istnieją w oddzielnych warstwach/modułach aplikacji, których nie chcesz wiązać ze sobą na sztywno. W MVP jedynie Widoki muszą być interfejsami, pozostałe elementy według potrzeb i uznania. A już w ogóle nie ma sensu robienie interfejsu dla DTO takiego jak LoginData.
Degusto napisał(a):

Czyli weźmy sobie przykład z logowania. Widok wywołuje metodę w prezenterze. Prezenter pobiera dane z widoku, a następnie pobiera z modelu dane o użytkowniku przez login. Teraz prezenter sprawdza czy istnieje taki użytkownik(jest różny od null), czy hasła pasują i wysyła do widoku dane o użytkowniku lub null jeśli nie ma takiego?

Absolutnie nie, Prezenter niczego nie sprawdza, nie wykonuje logiki biznesowej, nie operuje na bazie, nie robi takich rzeczy. On co najwyżej decyduje o tym, czy wyświetla Widok dostępny dla zalogowanego użytkownika, czy taki z komunikatem o braku dostępu.

A jeśli chciałbym dodać walidatory(jakieś tam klasy związane z walidacją) to gdzie powinny się ona znajdować? W folderze z prezenterami? Czy może lepiej byłoby np. stworzyć statyczną klasę z metodami do walidacji albo metody zamieścić już bezpośrednio w prezenterze?

Walidacja logiki biznesowej to część Modelu.
Walidacja danych wprowadzonych przez użytkownika, to część Widoku - czyli to on powinien zadbać, czy dane przesłane do Prezentera mają odpowiedni typ i format. Prezenter to tylko pośrednik, sam niczego nie powinien walidować.
Statyczna klasa to generalnie zły pomysł, ale to zależy co i kiedy chcesz walidować.

0

Dzięki, wiele rzeczy mi rozjaśniłeś :)

Poprawiłem trochę kod.

Interfejs:

interface ILoginView
    {
        LoginData GetLoginData();
    }

Widok:

 public partial class LoginView : Form, ILoginView
    {
        private readonly LoginPresenter presenter;

        public LoginView()
        {
            InitializeComponent();

            presenter = new LoginPresenter(this);
        }

        public LoginData GetLoginData()
        {
            string login = LoginTextBox.Text;
            string password = PasswordTextBox.Text;

            return new LoginData(login, password);
        }

        private void LogInButton_Click(object sender, EventArgs e)
        {
            bool canLogIn = presenter.CanLogIn();

            MessageBox.Show(canLogIn ? "Można." : "Nie można.");
        }
    }

Prezenter:

class LoginPresenter
    {
        private readonly ILoginView view;
        
        public LoginPresenter(ILoginView view)
        {
            this.view = view;
        }

        public bool CanLogIn()
        {
            LoginData loginData = view.GetLoginData();
            var logger = new Logger();

            return logger.CanLogIn(loginData.Login, loginData.Password);
        }
    }

Model:

class Logger
    {
        public bool CanLogIn(string login, string password)
        {
            var users = new UsersDataContext();

            return users.Users.
                Count(user => user.Login == login && user.Password == password) == 1;
        }
    }

ViewModel:

public class LoginData
    {
        public string Login { get; private set; }
        public string Password { get; private set; }

        public LoginData(string login, string password)
        {
            this.Login = login;
            this.Password = password;
        }
    }

Mam jeszcze kilka pytań:

  1. Gdzie powinienem przechowywać ViewModele, w prezenterze czy widoku?
  2. Powinienem w prezenterze przetworzyć wyniki z widoku do modelu tak jak zrobiłem to w prezenterze w metodzie CanLogIn?
  3. Jakbyś nazwał lepiej nazwał klasę Logger?
  4. Kod wygląda teraz lepiej?
  5. Mógłbym mieć w prezenterze zdarzenia np. LoginFailed i LoginSuccess, a widok podpinał by pod nie swoje metody?
1
  1. Pytasz o obiekty ViewModeli (tych się w ogóle nie przechowuje tylko tworzy na czas użycia) czy definicje klas? Jeśli to drugie, to musi to być miejsce dostępne zarówno dla Prezenterów jak i konkretnych Widoków, w praktyce np. gdzieś obok definicji interfejsów Widoków.
  2. Tak.
  3. Np. AuthenticationService.
  4. Znacznie. Tylko korzystanie z UserDataContext powinno być w zawarte w using i zamiast .Count(warunek) == 1 można zrobić Any(warunek).
  5. No niby mógłbyś... Ale jaki to miałoby cel? Bo zdarzenia w ogólności mają cel taki, że wiele obiektów może zostać poinformowanych o ich wystąpieniu, natomiast w przypadku powiązania 1 Prezenter : 1 Widok, takie coś z definicji nie nastąpi.
    Ja generalnie jestem przeciwnikiem opierania przebiegu programu o zdarzenia. Kod zdarzeniowy jest nieczytelny i trudniejszy w debugowaniu.

Spróbuj pisać aplikację, która coś odczytuje i zapisuje do jakiejś bazy/pliku, bo ten przykład jest zbyt prosty, żeby pokazać o co chodzi w MVP.

0

Dzięki wielkie, wiele mi wyjaśniłeś. Jutro spróbuję napisać jakąś prostą aplikację i wrzucę kod.

EDIT:
Napisałem coś takiego, mam nadzieję, że nie jest zbyt prosty:
https://github.com/Degusto/MVP-Sample

1 użytkowników online, w tym zalogowanych: 0, gości: 1