Komunikaty z oddzieleniem logiki

0

Tworzę aplikację, gdzie bardzo mocno przestrzegam podziału na logikę biznesową i interfejs użytkownika. Właściwie przede wszystkim zależy mi na tym, aby aplikacja była możliwe jak najbardziej przenośna, pomijając kwestie technologiczne jak np. ograniczenia platformy .NET (piszę w C#, ale aplikacja będzie ostatecznie w ASP.NET).

Tak, więc mam kilka dll-ek, które wykonują różne operacje funkcjonalne. Np. Rejestracja użytkownika, czyli po prostu zapisanie usera do bazy danych. Niestety podczas rejestracji mogą wystąpić różne "wyjątkowe" sytuacje. Tj:

  • User może już istnieć na bazie
  • Opcja zakładania usera jest wyłączona
  • Błędnie podane hasło usera, np. nieprawidłowe znaki lub też za mało znaków etc.

Chciałbym, aby na poziomie GUI było jak najmniej kodu. Właściwie powinno się tam znajdować jedynie wywoływanie controlerów, tak aby uzyskiwać wyniki swoich działań. Robimy, więc:

ControlUser.Registry("Login", "Pass");

I użytkownik zarejestrowany, a wszelkie instrukcje warunkowe zostały w środku wykonane. Niestety tutaj mam problem, gdyż o ile funkcjonalność w ten sposób zadziała, to jak przekazać użytkownikowi komunikaty o "wyjątkowych" sytuacjach?
Wymyśliłem kilka możliwych rozwiązań:

1.) Każda z tych sytuacji to osobny wyjątek. Np:

if (IsLoginInDataBase(login))
	throw new IsLoginInDataBaseException("Podany login istnieje już w bazie danych");

Następnie na poziomie GUI wyłapywałbym takie wyjątki:

try
{
   ControlUser.Registry("Login", "Pass");
}
catch(IsLoginInDataBaseException)
{
   // Tutaj tylko jakiś prosty showWarning o zaistniałym problemie.
}
catch(Exception)
{
   // działanie na wypadek faktycznie nie wiadomego wyjątku - czyli formatka z logiem właściwie
}

Niestety rozwiązanie to powoduje, że czuję wewnętrzny niepokój. Po pierwsze wyjątki nie zostały stworzone po to by przekazywać nimi komunikaty, a po drugie sytuacje przedstawione tutaj wcale nie są "wyjątkowe". Są to zwyczajne sytuacje, które można przewidzieć i się przed nimi zabezpieczyć. Rozwiązanie więc słabe.

2.) Każda z tych sytuacji będzie obsługiwana na poziomie GUI korzystając z publicznych metod. np:

try
{
   if (ControlUser.IsLoginInDataBase("Login"))
   	// Tutaj jakiś prosty ShowWarning o tym, że login istnieje na bazie.
   ControlUser.Registry("Login", "Pass");
}
catch(Exception)
{
   // działanie na wypadek faktycznie nie wiadomego wyjątku - czyli formatka z logiem właściwie
}

Niestety to rozwiązanie powoduje, że:

  • Mamy więcej metod publicznych, co powoduje pisanie większej ilości testów oraz zwiększa ryzyko wystąpienia błędu np. poprzez złe wykorzystanie metody głównej (Registry), gdyż programista zewnętrzny nie będzie wiedział jakie funkcje sprawdzające powinny być wywołane, aby nie doprowadzić do niepotrzebnych błęów.

Rozwiązanie, więc wydaje mi się być słabe.

3.) Użycie osobnej klasy obsługującej komunikaty, czyli po prostu przechowującej stringi / stringa i sterowanie tą klasą np. za pomocą wstrzyiwania zależności.

Rozwiązanie na razie mam operacowane w głowie i nieco z abstrakcyjnym myśleniem, ale wyglądałoby to zapewne mniej-więcej tak:

try
{
   ControlUser.Registry("Login", "Pass");
   ICommunicat comm = DiControl.Get<IComunicat, ControlUser>(); // powiedzmy - czysta abstrakcja na razie
   if (comm.IsError())
   	// Wyświetlamy błąd.
   if (comm.IsWarning())
   	// Wyświetlamy ostrzezenie.
   if (comm.IsInformation())
   	// Wyświetlamy informację.
}
catch(Exception)
{
   // działanie na wypadek faktycznie nie wiadomego wyjątku - czyli formatka z logiem właściwie
}

W uproszczeniu oczywiście tak by to mogło wyglądać. Rozwiązanie wydaje mi się najbardziej sensowne, ale nie poczyniłem jeszcze żadnych kroków do jego implementacji.

Czy widzicie jakieś inne możliwości? Jak przekazywać komunikaty użytkownikowi rozdzielając logikę biznesową od interfejsu, nie tworząc przy tym zbyt wiele kodu w GUI?

0

Zwracaj z metody enum o wartościach: Success, BadPassword, UserAlreadyExists, itd. i na jego podstawie zdecyduj co zrobić w kontrolerze.

0

I w GUI robić setki switchy, by podejmować odpowiednie kroki?
Według mnie kiepski pomysł, już dużo lepiej zrobić wstrzykiwanie zależności, a potem poprzez fabrykę abstrakcyjną decydować o rodzaju komunikatu (błąd, warning itd.). Dzięki temu nie będzie switcha - będzie zmieniony w polimorficzną postać.

Poza tym takie rozwiązanie jakie proponujesz mija się z celem, gdy metoda powinna zwracać konkretną wartość np. string, wartość numeryczną lub też obiekt biznesowy, a przecież i takie metody w aplikacji istnieją. W takim przypadku również mogą wystąpić komunikaty, a enuma już zwracać nie mogę.

Oczywiście parametry typu ref lub out nawet nie biorę pod uwagę, gdyż kod ma być napisany zgodnie ze standardami czystego kodu, a nie łamiąc zasady jakie tylko istnieją :)

3
Biały Lew napisał(a):

I w GUI robić setki switchy, by podejmować odpowiednie kroki?
Według mnie kiepski pomysł, już dużo lepiej zrobić wstrzykiwanie zależności, a potem poprzez fabrykę abstrakcyjną decydować o rodzaju komunikatu (błąd, warning itd.). Dzięki temu nie będzie switcha - będzie zmieniony w polimorficzną postać.

A gdzie pisałem o switchach? o.O Poczytaj lepiej o typie Dictionary<> zamiast myśleć o switchach.
Pytasz jak przekazać z góry znaną możliwy wynik dla konkretnego przypadku. Exceptiony i stringi są złe, enum jest odpowiednim rozwiązaniem.

Poza tym takie rozwiązanie jakie proponujesz mija się z celem, gdy metoda powinna zwracać konkretną wartość np. string, wartość numeryczną lub też obiekt biznesowy, a przecież i takie metody w aplikacji istnieją. W takim przypadku również mogą wystąpić komunikaty, a enuma już zwracać nie mogę.

Teraz pytasz o jakieś generyczne rozwiązanie, więc trzeba je rozwiązać... generycznie.

class OperationResult<T>
{
    public T Data { get; private set; }
	public OperationStatus Status { get; private set; }
	
	OperationResult<T>(T data, OperationStatus status)
	{
		this.Data = data;
		this.Status = status;
	}
}

gdzie OperationStatus to nasz enum.

0

A gdzie pisałem o switchach? o.O Poczytaj lepiej o typie Dictionary<> zamiast myśleć o switchach.
Pytasz jak przekazać z góry znaną możliwy wynik dla konkretnego przypadku. Exceptiony i stringi są złe, enum jest odpowiednim rozwiązaniem.

Tak Cię zrozumiałem, ale oczywiście jedynie w nieświadomości. Jak rozumiem proponujesz bym w słowniku zbindował odpowiednie wartości enuma z np. funkcjami ostrzeżeń, errorów, informacji itd?

Po wykonaniu funkcji w controlerze, pobrać wartość słownikową (funkcję) zgodną ze zwróconą wartością w enumie i gotowe. Czy tak?

Teraz pytasz o jakieś generyczne rozwiązanie, więc trzeba je rozwiązać... generycznie.

Tutaj chyba troszkę nie zrozumiałeś lub zapropnowałeś rozwiązanie nad wyraz uniwersalne. Wcześniej nie miałem dostępu do kodu, ale teraz mogę pokazać o co chodzi konkretniej:

public class ControlUser
{
  public static void RegistryUser(User user)
  {
     IUserSelect selectUser = (IUserSelect)DIControl.Kernel.Get<IUserSelect>(typeof(ControlUser));
     selectUser.InsertData(user);
  }
}

To jest mój controler. Funkcja pobiera klasę z której ma skorzystać (zaleznie od rodzaju bazy danych), a następnie wykonuje zapis na bazę danych.

Oczywiście gdzieś tutaj w środku należy wykonać różnego rodzaju sprawdzenia.
I jeśli dobrze Cię zrozumiałem - powinienem zrobić takie coś:

public class ControlUser
{
  public OperationStatus status { get; private set; }

  public static void RegistryUser(User user)
  {
     IUserSelect selectUser = (IUserSelect)DIControl.Kernel.Get<IUserSelect>(typeof(ControlUser));

     if (selectUser.IsLoginInDataBase(user.Login))
                status = OperationStatus.IsLoginInDataBase;

     selectUser.InsertData(user);
     status = OperationStatus.OK;
  }
}

Oczywiście dla każdego controlera w takim przypadku musi być oddzielny enum, gdyż każdy controler może mieć różne problemy, nie zawsze takie same.

Czy dobrze teraz zrozumiałem? Bo takie rozwiązanie faktycznie wydaje mi się dość dobre, ale może coś pominąłem lub znów użyłem błędnego skrótu myślowego.

Widzę tutaj jedynie jeden problem: jak odróżnić warningi od errorów? Musiałbym użyć atrybutów w enumie lub ewentualnie użyć kilku typów enumeratorów czyż nie?

0

To może zacznijmy od podstaw:

  1. Dlaczego metoda RegistryUser jest statyczna?
  2. Dlaczego w tej metodzie wyciągasz coś z kontenera IoC? Dlaczego zależność nie jest wstrzykiwana automatycznie do obiektu klasy?
  3. Czym według Ciebie jest "Controller"? Bo terminem tym w programowaniu określa się specyficzny rodzaj klas, a Ty słowo to stosujesz chyba do klas realizujących logikę biznesową aplikacji.
  4. Zwracanie wyniku metody przez pole klasy to jakiś koszmar. Wynik z metody zwraca się przez return w jej ciele, nie inaczej.

I teraz - zarzucasz mi, że podałem zbyt uniwersalne rozwiązanie, a z drugiej strony chcesz rozwiązania dla różnych operacji, czyli właśnie uniwersalnego. To czego w końcu potrzebujesz?

0
  1. Dlaczego metoda RegistryUser jest statyczna?

Metoda RegistryUser nie modyfikuje własnego obiektu, więc nie ma powodu by nie była ona statyczna.
Jeśli chcę utworzyć usera to po co mam dodatkowo zajmować pamięć i tworzyć dodatkowy, zbędny obiekt? Lepiej wywołac bezpośrednio metodę, gdyż zmiana stanu obiektu nie występuje.

  1. Dlaczego w tej metodzie wyciągasz coś z kontenera IoC? Dlaczego zależność nie jest wstrzykiwana automatycznie do obiektu klasy?

Ponieważ zakładam, że zmianie może ulec cel zapisu w trakcie działania aplikacji. Gdybym wczytał dane jeden raz, byłoby to utrudnienie, a w krytycznej sytuacji mogłoby się to wiązać z błędem.

  1. Czym według Ciebie jest "Controller"? Bo terminem tym w programowaniu określa się specyficzny rodzaj klas, a Ty słowo to stosujesz chyba do klas realizujących logikę biznesową aplikacji.

Dla mnie controller to klasa, która jest mostem pomiędzy faktycznymi wewnętrznymi operacjami (np. bazodanowymi lub logicznymi), a interfejsem użytkownika. Controller w takiej postaci posiada funkcje, które zwracają obiekty mogące być zaprezentowane w GUI lub tez wykonują zwyczjane operacje bez zwracania danych (np. własnie registry).

  1. Zwracanie wyniku metody przez pole klasy to jakiś koszmar. Wynik z metody zwraca się przez return w jej ciele, nie inaczej.

Ok, ale metoda Registry nie zwraca nic. Mogłaby na siłę zwracać wartość bool, ale...
... co w takiej sytuacji gdybym chciał napisać metodę sprawdzającą ilu jest użytkowników w okreslonym wieku (przykładowo).

public int GetCountForAgeUser(int age);

Taka metoda także posiada zbiór możliwych komunikatów. Jak w takim przypadku chciałbyś je przekazać do GUI bez pola instancyjnego w klasie? W parametrze metody? To też niezbyt estetyczne rozwiązanie.

0
Biały Lew napisał(a):

Metoda RegistryUser nie modyfikuje własnego obiektu, więc nie ma powodu by nie była ona statyczna.
Jeśli chcę utworzyć usera to po co mam dodatkowo zajmować pamięć i tworzyć dodatkowy, zbędny obiekt? Lepiej wywołac bezpośrednio metodę, gdyż zmiana stanu obiektu nie występuje.

Argument dotyczący marnowania pamięci ma sens, jeżeli programujesz system embedded z 2kB (słownie: dwa kilobajty) RAM. Na standardowym desktopie podejrzewam jest nieco więcej.

Obiekt "ControlUser" posiada indywidualną właściwość - właśnie ten obiekt IUserSelect, który wyciągasz "na chama" z kontenera DI zamiast wstrzyknąć go po ludzku do instancji klasy.

Ponieważ zakładam, że zmianie może ulec cel zapisu w trakcie działania aplikacji. Gdybym wczytał dane jeden raz, byłoby to utrudnienie, a w krytycznej sytuacji mogłoby się to wiązać z błędem.

Nie rozumiem, w czasie działania aplikacji chcesz mieć możliwość przepięcia providera bazy danych? Nawet jeżeli to konieczne, to właśnie takie przepinanie internalsów jest niebezpieczne i może się wiązać z kosmicznymi błędami.

public int GetCountForAgeUser(int age);

Taka metoda także posiada zbiór możliwych komunikatów. Jak w takim przypadku chciałbyś je przekazać do GUI bez pola instancyjnego w klasie? W parametrze metody? To też niezbyt estetyczne rozwiązanie.

Jaki jest normalny przypadek, kiedy funkcja mająca zwrócić ilość userów nie zwróci ilości userów? Nie ma. No to jeżeli z jakiegoś powodu nie udało jej się tego zrobić, powinna rzucić wyjątek.

I popracuj nad angielskim. RegistryUser = RejestrUżytkownik. Cała konwencja nazewnictwa zastosowana tutaj jest dość "rakotwórcza" jeżeli chodzi o prawdziwą gramatykę angielskiego i może być przez to myląca.

0

Jaki jest normalny przypadek, kiedy funkcja mająca zwrócić ilość userów nie zwróci ilości userów? Nie ma. No to jeżeli z jakiegoś powodu nie udało jej się tego zrobić, powinna rzucić wyjątek.

Ech.... Jakie to ma znaczenie? Podaję przykład, których można podać setki, życiowych i nie życiowych.
Dobrze niech będzie przykład realny.

Mamy do napisania funkcję generującą raport na temat użytkowników.
Nagłówek funkcji wygląda tak:

public List<StatsUser> GetStatsUser();

Funkcja zwraca listę danych, które następnie są bindowane w GUI.
Gdzie może wystąpić wyjątek lub po prostu komunikat? Ano chociażby nie każdy user ma prawo do korzystania z tej funkcji. Czyli w controllerze następuje sprawdzenie, czy zalogowany user ma prawo do generowania raportu, jeśli nie to powinien otrzymać komunikat. Teraz masz życiowy przykład.

Argument dotyczący marnowania pamięci ma sens, jeżeli programujesz system embedded z 2kB

Nie, nie tylko wtedy ma sens. Po co marnować czas i pisać dodatkowe zbędne linijki kodu skoro można to zrobić krócej i sensowniej. Tworzenie dodatkowych obiektów ma się nijak do czystego kodu. Tam gdzie to nie jest wymagane, nie należy na siłę tworzyć obiektów.

Nie rozumiem, w czasie działania aplikacji chcesz mieć możliwość przepięcia providera bazy danych? Nawet jeżeli to konieczne, to właśnie takie przepinanie internalsów jest niebezpieczne i może się wiązać z kosmicznymi błędami.

Jaki problem niby w takiej sytuacji może wyniknąć? Przepięcie providera bazy danych w sytuacji dowolnej ma w pewnych sytuacjach sens w tej aplikacji. Nie mogę, więc robić tego podczas tworzenia obiektu, gdyż obiekt żyje sobie X czasu.
Proste.

0
Biały Lew napisał(a):

Mamy do napisania funkcję generującą raport na temat użytkowników.
Nagłówek funkcji wygląda tak:

public List<StatsUser> GetStatsUser();

Funkcja zwraca listę danych, które następnie są bindowane w GUI.
Gdzie może wystąpić wyjątek lub po prostu komunikat? Ano chociażby nie każdy user ma prawo do korzystania z tej funkcji. Czyli w controllerze następuje sprawdzenie, czy zalogowany user ma prawo do generowania raportu, jeśli nie to powinien otrzymać komunikat. Teraz masz życiowy przykład.

Czyli ta metoda wyciąga statystyki z bazy danych, sprawdza czy użytkownik jest zalogowany i czy ma prawo do generowania raportu?

Jak to się ma do single responsibility principle? To jest jedna metoda, która realizuje funkcjonalności jednocześnie modelu i kontrolera.

Nie, nie tylko wtedy ma sens. Po co marnować czas i pisać dodatkowe zbędne linijki kodu skoro można to zrobić krócej i sensowniej. Tworzenie dodatkowych obiektów ma się nijak do czystego kodu. Tam gdzie to nie jest wymagane, nie należy na siłę tworzyć obiektów.

Gdzie to przeczytałeś? Ile zaoszczędzisz nie tworząc tego obiektu? 4 bajty? 16 bajtów?

To nie jest takie proste, "czystość kodu" nie jest po prostu odwrotnie proporcjonalna do ilości obiektów.

Przecież instancja istnieje, tylko jest ukryta i globalna - statycznie odwołujesz się do kontenera DI w celu wyciągnięcia czegoś, co powinno być jawną właściwością klasy. Czyli nadal masz własność klasy, tylko niewprost. Takie projektowanie, cytuję, "ma się nijak do czystego kodu". Zastosowałeś pogmatwaną wariację wzorca Singleton tylko po to, żeby uniknąć zrobienia jednej instancji jednej klasy.

Jaki problem niby w takiej sytuacji może wyniknąć? Przepięcie providera bazy danych w sytuacji dowolnej ma w pewnych sytuacjach sens w tej aplikacji. Nie mogę, więc robić tego podczas tworzenia obiektu, gdyż obiekt żyje sobie X czasu.
Proste.

Co jeżeli w czasie bardziej zaawansowanej operacji zrobisz race condition?

0

Co jeżeli w czasie bardziej zaawansowanej operacji zrobisz race condition?

Akurat taka sytuacja nie będzie miała miejsca. Wiem, że to jest jedyna możliwość, kiedy może wystąpić błąd, ale w tym rpzypadku ona nie będzie miała miejsca, gdyż zmiana providera może mieć miejsce jedynie z poziomu odpowiedniego okna "opcji", a nie podczas operacji. Stąd też jestem o to spokojny. Co prawda mógłbym zrobić zrobić zmienną instancyjną w tej sytuacji i być może jest to do przemyślenia. Pewnie tak zrobię i poprawię ten element.

Chciałbym jednak skupić się nad głównym problemem tego topica.

Czyli ta metoda wyciąga statystyki z bazy danych, sprawdza czy użytkownik jest zalogowany i czy ma prawo do generowania raportu?

Jak to się ma do single responsibility principle? To jest jedna metoda, która realizuje funkcjonalności jednocześnie modelu i kontrolera.

I tutaj masz rację. Jednak dochodzimy w takim układzie do rozwiązania, które podałem w pierwszym poście pod numerem: 2
Tam mam wiele metod wykonujących pojedyncze czynności, i są one kolejno wywoływane z poziomu GUI. Teoretycznie jest więc zachowane prawo pojedynczej odpowiedzialności, ale wadą jest to, że dla każdego nowego GUI muszę pisać sporo kodu.

0
Biały Lew napisał(a):

I tutaj masz rację. Jednak dochodzimy w takim układzie do rozwiązania, które podałem w pierwszym poście pod numerem: 2
Tam mam wiele metod wykonujących pojedyncze czynności, i są one kolejno wywoływane z poziomu GUI. Teoretycznie jest więc zachowane prawo pojedynczej odpowiedzialności, ale wadą jest to, że dla każdego nowego GUI muszę pisać sporo kodu.

Jak, po co? Dobrze zaprojektowane helpery i nie powinieneś mieć zbyt dużo kodu, tylko tutaj znowu wracamy do tego co napisał somekind...

0

Co konkretnie masz na myśli?
Mógłbyś napisać kod jak sobie to wyobrażasz?

0
Biały Lew napisał(a):
  1. Dlaczego metoda RegistryUser jest statyczna?

Metoda RegistryUser nie modyfikuje własnego obiektu, więc nie ma powodu by nie była ona statyczna.

A, że metody generalnie nie powinny modyfikować obiektów, więc wszystkie powinny być statyczne. ;]

Metody statyczne pisze się do zadań pomocniczych, nie związanych z głównym zadaniem klasy, które nie potrzebują do działania pól klasy.

Jeśli chcę utworzyć usera to po co mam dodatkowo zajmować pamięć i tworzyć dodatkowy, zbędny obiekt?

Bo jeśli pisze się w C#, to wypadałoby robić to obiektowo i obiektów używać. Jeśli nie chcemy obiektów, to polecam wybrać jeden z wielu języków, który ich nie wspiera.

W aplikacji zapewne będziesz miał wiele różnych obiektów służących do operacji pewnego rodzaju, które będą realizowały jakiś wspólny zestaw operacji, który należałoby wydzielić do klasy bazowej. Jak masz zamiar zrealizować dziedziczenie na klasach statycznych?

Ponieważ zakładam, że zmianie może ulec cel zapisu w trakcie działania aplikacji. Gdybym wczytał dane jeden raz, byłoby to utrudnienie, a w krytycznej sytuacji mogłoby się to wiązać z błędem.

Ja nie pytam o wczytywanie danych i ich keszowanie tylko o wstrzykiwanie zależności... Pytam, czemu ręcznie wyciągasz obiekt z kontenera w metodzie, zamiast przekazać go w konstruktorze klasy.
Jeśli będziesz miał 10 metod w klasie, to w każdej z nich będziesz odwoływał się do kontenera?
Po co Ci w ogóle kontener, skoro chcesz go ręcznie wołać?

W normalnej aplikacji, gdy np. kontroler korzysta z serwisu, to serwis jest wstrzykiwany do kontrolera w konstruktorze, kontroler ma pole typu tego serwisu, a metody kontrolera operują na tym polu.

Dla mnie controller to klasa, która jest mostem pomiędzy faktycznymi wewnętrznymi operacjami (np. bazodanowymi lub logicznymi), a interfejsem użytkownika. Controller w takiej postaci posiada funkcje, które zwracają obiekty mogące być zaprezentowane w GUI lub tez wykonują zwyczjane operacje bez zwracania danych (np. własnie registry).

Zazwyczaj jednak kontrolery zajmują się logiką prezentacji, czyli łączą GUI z logiką biznesową. Natomiast klasy zajmujące się realizowaniem logiki biznesowej to serwisy. Oczywiście możesz nazywać swoje klasy odwrotnie niż reszta świata, ale może do prowadzić do dziwnych nieporozumień.

Ok, ale metoda Registry nie zwraca nic. Mogłaby na siłę zwracać wartość bool, ale...

To Twoja sprawka, że niczego nie zwraca, bo powinna zwracać wynik operacji, czyli enum z wartościami sukces i możliwymi błędami.

... co w takiej sytuacji gdybym chciał napisać metodę sprawdzającą ilu jest użytkowników w okreslonym wieku (przykładowo).

public int GetCountForAgeUser(int age);

Taka metoda także posiada zbiór możliwych komunikatów. Jak w takim przypadku chciałbyś je przekazać do GUI bez pola instancyjnego w klasie? W parametrze metody? To też niezbyt estetyczne rozwiązanie.

Estetyczne i obiektowe rozwiązanie podałem kilka postów wcześniej, jest nim klasa OperationResult, która pozwala zwrócić zarówno dane jak i status wykonania metody.

0

Estetyczne i obiektowe rozwiązanie podałem kilka postów wcześniej, jest nim klasa OperationResult, która pozwala zwrócić zarówno dane jak i status wykonania metody.

Do wcześniejszych wiadomości się nie ustosunkuję, gdyż muszę je przemyśleć i nie jestem do nich przekonany. Nie jest to jednak temat tego topica i nie chciałbym lawirować pomiędzy zbyt wieloma aspektami.

Jednak do cytatu powyżej - teraz zrozumiałem co miałeś na myśli. Ok, rozwiązanie ciekawe.
Jednak nigdy nie widziałem, aby ten problem rozwiązywano w taki sposób. Nie twierdzę, że pomysł jest zły, bo według mnie jest dość ciekawy, jednak widzę tutaj pewien mankament o którym pisałem wcześniej, a który pominąłeś w swoim poście.

Jeśli zrobisz klasę generyczną przyjmującą wynik funkcji to po 1:

  • Jak wyobrażasz sobie enuma z kodami błędów? Jeden wielki obejmujący wszystkie metody w aplikacji? Raczej nie.
  • Trudno zarządzalny kod, bo każda metoda będzie zwracać obiekt tej samej klasy. W rezultacie obsługa tego stanie się bardzo niewygodna i trudna.
  • Brak rozdzielenia na warningi i errory.

Te problemy są bardzo poważne i eliminują tego typu rozwiązanie. Chciałbym byś to przemyślał, gdyż chyba nie rozumiesz problematyki i zaproponowałeś rozwiązanie potencjalnie ciekawe i dobre, jednak na dłuższą metę, to by zabiło aplikację.

1

No to można zrobić jeszcze bardziej generyczne rozwiązanie:

class OperationResult<D, S>
{
    public D Data { get; private set; }
    public S Status { get; private set; }
 
    OperationResult<D, S>(D data, S status)
    {
        this.Data = data;
        this.Status = status;
    }
}

Tylko czy naprawdę taki jest cel? Z tego co rozumiem próbujesz robić jedną master metodę, która pod spodem sprawdzi jakieś tam uprawnienia, wywoła model i zwróci dane bezpośrednio gotowe do podpięcia do GUI. To znowu łamanie SRP.

Rozważmy taki przypadek:

Model
Jest sobie jakiś model, repozytorium użytkowników oferujące następujące metody (pseudokod):

bool isUserPermitted(String userName, PermissionType perm) throws UserNotFoundException
List<Something> getUserSomething(String userName) throws UserNotFoundException

Pierwsza metoda po prostu sprawdza, czy użytkownik posiada dane uprawnienie i zwraca true lub false. Sytuacja wyjątkowa: taki użytkownik nie istnieje, wtedy rzuca UserNotFoundException.

Druga metoda pobiera jakieś dane (jakąś właśnie List<Something>) dla usera o nazwie userName. Analogicznie sytuacja wyjątkowa: taki użytkownik nie istnieje, rzucamy UserNotFoundException.

Kontroler
W jakiejś akcji stats kontroler sprawdza uprawnienia użytkownika korzystając z modelu i albo wyświetla błąd, albo wywołuje getUserSomething i wyświetla GUI z danymi.

Rozwiązanie somekinda jest oczywiście poprawne i otrzymałeś je w odpowiedzi na swoje konkretne pytanie, ale nie wiem czy jest sens stosować coś takiego, to idzie w overengineering. Zbyt przegięty kod pełen błyskotliwych generycznych rozwiązań jest tak samo problematyczny jak kod, który abstrakcji w ogóle nie ma.

1

No chyba że jeszcze bardziej chcesz to enkapsulować w jakiś serwis. Kontroler mógłby używać serwisu z takim, funkcyjnym API:

service.getUserSomething(...)
   .onSuccess((Something sth) => {
      // wyświetlamy okno GUI z danymi
   })
   .onError((... error) => {
      // wyświetlamy okno GUI z błędem
   });

Czyli kontroler dostaje serwis, którego używa w sposób podany wyżej i zajmuje się wyłącznie logiką prezentacji, serwis korzystając z modelu sprawdza uprawnienia i wyciąga faktyczne dane, a model to model.

Normalnie musiałbyś najpierw zwracać konkretne statusy, a potem gdzieś ifować, w takim przypadku podpinałbyś po prostu callbacki, które mają się wykonać w danym przypadku.

0

Ok, dziękuję bardzo za pomoc. Myślę, że już wiem jak rozwiążę problem.

0
Biały Lew napisał(a):

Jednak do cytatu powyżej - teraz zrozumiałem co miałeś na myśli. Ok, rozwiązanie ciekawe.
Jednak nigdy nie widziałem, aby ten problem rozwiązywano w taki sposób.

To musiałeś naprawdę bardzo mało widzieć. Zazwyczaj stosuje się takie lub podobne rozwiązanie.

  • Jak wyobrażasz sobie enuma z kodami błędów? Jeden wielki obejmujący wszystkie metody w aplikacji? Raczej nie.

Jeśli pojedynczy enum to w Twoim przypadku zbyt mało elastyczne rozwiązanie, to równie dobrze możesz zamiast enuma użyć klasy w rodzaju ValidationResult przechowującej informację o tym, co poszło nie tak po stronie biznesu.

  • Trudno zarządzalny kod, bo każda metoda będzie zwracać obiekt tej samej klasy. W rezultacie obsługa tego stanie się bardzo niewygodna i trudna.

Yyy? W tym rozwiązaniu jest jedna malutka, generyczna klasa...

  • Brak rozdzielenia na warningi i errory.

Interpretacja zależy do logiki prezentacji. Ale jeśli potrzebujesz takiego podziału po stronie logiki biznesowej, to co za problem, bo nie rozumiem?

Te problemy są bardzo poważne i eliminują tego typu rozwiązanie. Chciałbym byś to przemyślał, gdyż chyba nie rozumiesz problematyki i zaproponowałeś rozwiązanie potencjalnie ciekawe i dobre, jednak na dłuższą metę, to by zabiło aplikację.

Widzisz - dla Ciebie to poważne problemy, dla mnie detale, więc może to jednak Ty to przemyślisz?

Demonical Monk napisał(a):

Normalnie musiałbyś najpierw zwracać konkretne statusy, a potem gdzieś ifować, w takim przypadku podpinałbyś po prostu callbacki, które mają się wykonać w danym przypadku.

Pod warunkiem, że lubisz debugować callbacki, i że możesz ich w ogóle użyć. Gdy logika biznesowa jest na innym serwerze, to chyba nie będzie łatwo.

0

To musiałeś naprawdę bardzo mało widzieć. Zazwyczaj stosuje się takie lub podobne rozwiązanie.

Pracowałem w wielu firmach i nigdzie nie widziałem podobnego cuda. pamiętaj, że Twoje praktyki nie oznaczają praktyk powszechnych, co więcej nie zawsze to co uważasz za słuszne jest najlepsze.
Twoje rozwiązanie jest według mnie błędne, mimo tego, że rozwiązuje problem. Podane przez Ciebie roziwązanie wyjaśnia jeden problem, a generuje kilka nowych. Nie przekonałes mnie w swoich postach do swoich racji, ani Demonical Monk. Mam po prostu po mału nieco inną wizję tego wszystkiego. Jednak chętnie poznam nieco głębiej Wasz tok rozumowania (chociaż wydaje mi się, że oboje mówicie o zupełnie innym podejściu), ale mówienie (tak jak pnoiżej), ze pewne problemy sa dla Ciebie detalami jest oznaką małej ignorancji, gdyż na detalach właśnie najłatwiej położyć projekt. W detalach tkwi diabeł.

Jeśli pojedynczy enum to w Twoim przypadku zbyt mało elastyczne rozwiązanie, to równie dobrze możesz zamiast enuma użyć klasy w rodzaju ValidationResult przechowującej informację o tym, co poszło nie tak po stronie biznesu.

Czyli znów wracamy do mojego pomysłu (ewentualnie zmodyfikowanej wersji) z pierwszego posta. Numer pomysłu 3 w 1 poście.

Yyy? W tym rozwiązaniu jest jedna malutka, generyczna klasa...

Nie mówię o tej klasie. Pomyśl... masz 150 metod w 10 klasach. Każda metoda według Ciebie powinna zwracać jedyny słuszny obiekt klasy OperationResult. To oznacza, że patrząć na kod nie amsz zielonego pojęcia jaki typ danych faktycznie zwracają określone metody, nie wiesz jaki typ danych przechowywany jest na odpowiednich etapach programu, przez co ewentualne problemy rozwiązuje się bardzo długo a proces analizy trwa niewspółmiernie dłużej niż przeciętnie. Według mnie to nie jest definicja dobrego jakościowo kodu. Co z tego, że napisałeś klasę generyczną skoro ona nie ułątwia Ci zarzadzania kodem? Metody powinny zwracać określone dane, nie zas jeden rodzaj obiektu danej klasy. Nigdy nie widziałem tak napisanej aplikacji i uważam, że byłaby to prawdziwa katastrofa. Jeśli uważasz inaczej - przekonaj mnie. Jak niby chcesz zarządzać dobrze kodem, albo nawet jedną klasą gdzie masz 10 różnych metod i każda zwraca ten sam generyczny typ danych? Analizować ciało tej metody? Bez jaj.

Interpretacja zależy do logiki prezentacji. Ale jeśli potrzebujesz takiego podziału po stronie logiki biznesowej, to co za problem, bo nie rozumiem?

Tutaj problem jest najmniejszy i jest kilka rozwiązań. Chodzi jednak o to, że Twoja propozycja wcale niczego nie ułątwia - wręcz przeciwnie, utrudnia pisanie całego programu oraz uniemożliwia jego zarządzanie w dłuższym okresie rozwoju.
jednak jeśli to problem możesz pominąć ten punkt, gdyż rozwiązanie jest stosunkowo proste.

Widzisz - dla Ciebie to poważne problemy, dla mnie detale, więc może to jednak Ty to przemyślisz?

Myślę nad tym wystarczająco długo i sądzę, że jesteś zbyt pewny siebie i zbyt długo pracujesz nad tego typu kodem by być obiektywnym. Wygląda na to, że każdy projekt nad którym pracujesz w taki sposób piszesz. Według mnie - kompletnie niezarządzalne rozwiązanie i bardzo kosztowne w utrzymaniu.

Pod warunkiem, że lubisz debugować callbacki, i że możesz ich w ogóle użyć. Gdy logika biznesowa jest na innym serwerze, to chyba nie będzie łatwo.

A czemu zakładasz, że logika będzie na serwerze, czy gdziekolwiek indziej? Logika może być schowana gdziekolwiek, a mimo to kod ma być łatwy w deubggowaniu. Na tym polega ułatwianie sobie pracy. Aplikację testuje się także jednostkowo przy pomocy narzędzi które umożliwiają debuggowanie w locie, więc w czym problem?
Twoje rozwiązanie natomiast utrudnia cały proces i jest łamaniem podstawowych zasad programowania obiektowego - tj. każda metoda zwraca obiekt tej samej klasy generycznej. Nie rozumiem gdzie widzisz tutaj zaletę takiego rozwiązania, gdyż zarządzanie tym to będzie udręka.

1
Biały Lew napisał(a):

Pracowałem w wielu firmach i nigdzie nie widziałem podobnego cuda. pamiętaj, że Twoje praktyki nie oznaczają praktyk powszechnych

To nie są moje praktyki, lecz powszechnie stosowane. Tak to wygląda w wielu projektach, w wielu firmach. Nie chcesz wierzyć - nie musisz.

Czyli znów wracamy do mojego pomysłu (ewentualnie zmodyfikowanej wersji) z pierwszego posta. Numer pomysłu 3 w 1 poście.

No nie wiem. Ja mówię o zwracaniu różnych rezultatów działania różnych metody. W zdaniu: Użycie osobnej klasy obsługującej komunikaty, czyli po prostu przechowującej stringi / stringa i sterowanie tą klasą np. za pomocą wstrzyiwania zależności. nawet trudno zrozumieć o co właściwie chodzi.

Każda metoda według Ciebie powinna zwracać jedyny słuszny obiekt klasy OperationResult. To oznacza, że patrząć na kod nie amsz zielonego pojęcia jaki typ danych faktycznie zwracają określone metody, nie wiesz jaki typ danych przechowywany jest na odpowiednich etapach programu, przez co ewentualne problemy rozwiązuje się bardzo długo a proces analizy trwa niewspółmiernie dłużej niż przeciętnie. Według mnie to nie jest definicja dobrego jakościowo kodu. Co z tego, że napisałeś klasę generyczną skoro ona nie ułątwia Ci zarzadzania kodem? Metody powinny zwracać określone dane, nie zas jeden rodzaj obiektu danej klasy. Nigdy nie widziałem tak napisanej aplikacji i uważam, że byłaby to prawdziwa katastrofa. Jeśli uważasz inaczej - przekonaj mnie. Jak niby chcesz zarządzać dobrze kodem, albo nawet jedną klasą gdzie masz 10 różnych metod i każda zwraca ten sam generyczny typ danych? Analizować ciało tej metody? Bez jaj.

Jedna metoda zwraca OperationResult<int>, inna metoda zwraca OperationResult<Customer>, a jeszcze inna OperationResult<IEnumerable<Invoice>>. To nie są takie same typy, od razu widać, co zwraca każda metoda, i każda zwraca coś innego.

A czemu zakładasz, że logika będzie na serwerze, czy gdziekolwiek indziej?

Bo logika gdzieś być musi, na tej samej fizycznej maszynie, albo na innej. Jeśli na innej, to zdarzenia działają "nieco" inaczej.

Logika może być schowana gdziekolwiek, a mimo to kod ma być łatwy w deubggowaniu. Na tym polega ułatwianie sobie pracy. Aplikację testuje się także jednostkowo przy pomocy narzędzi które umożliwiają debuggowanie w locie, więc w czym problem?

W tym, że zdarzenia nieco zaburzają ogólny przepływ w aplikacji, i trudniej debuguje się kod oparty o nie.

Twoje rozwiązanie natomiast utrudnia cały proces i jest łamaniem podstawowych zasad programowania obiektowego - tj. każda metoda zwraca obiekt tej samej klasy generycznej.

Serio?
Piszesz strukturalny kod, którego podstawą są statyczne metody, zwracasz wyniki przez pola klasy, nie rozumiesz typów generycznych, odrzucasz powszechne wzorce, a mi zarzucasz ignorancję i łamanie zasad OOP?

2

Spotkałem się z rozwiązaniami sugerowanymi przez somekinda czy dm, jest to dość powszechne podejście - być może pracowałeś w złych firmach. Sam we własnych projektach również używam takiej klasy.

Twoje rozwiązanie natomiast utrudnia cały proces i jest łamaniem podstawowych zasad programowania obiektowego

Niestety ale z twoich postów wynika, ze to właśnie Ty nie rozumiesz podstaw OOP.

0

Myślałem kilka godzin nad tym co miałeś na myśli i czy ja czegoś nie rozumiem. Faktycznie - w pewnym momencie zrozumiałem, że to co napisałeś nieco inaczej zrozumiałem (nie do końca źle, ale kontekst gdzieś mi uciekł).
W tym wszystkim oczywiście musiałem też odnaleźć moją architekturę.

Co się okazało?
Musiałem przebudować architekturę mojego programu.

Teraz nie ma statycznych metod, tylko obiektowe klasy biznesowe:

public class User : LogInfo
    {
        private IUserSelect userDB;
        public Group Group { get; set; }
        public string Login { get; set; }
        public string Pass { get; set; }
.......
etc.
}

Jak widzisz mam providera bazy danych, wczytywanego podczas tworzenia obiektu tej klasy.
W klasie User umieszczone zostały metody do rejestracji użytkownika, logowania itp operacji.
Mamy więc w klasie User metodę przykładowo:

public OperationStatus RegistryUser()
        {
            if (userDB.IsExistUserInDataBase(this.login))
                return new OperationStatus<bool, string>(false, "Taki user już istnieje");

            userDB.SaveUser(this);
            return new OperationStatus<bool, string>(true, string.Empty);
        }

Metody Usera zwracają obiekt klasy o której pisałeś - czyli obiekt generyczny. Na razie jedynie nie zastosowałem enuma jako komunikatów, a zwykłego stringa. Jednak w planach jest zmienienie stringa na osobną klasę zarządzającą komunikatami i ogólnie operacjami tego typu.
Jeśli to miałeś na myśli próbując mi przekazać pewne informacje to teraz rozumiem. Taka funkcja wydaje mi się ok, ale może jednak uważasz inaczej i coś innego miałeś na myśli?

W providerze natomiast mam wystawione metody publiczne, które nie zwracają typów generycznych. Np walidacje zwracają bool, zaś generujące raport obiekt jakiejś klasy. Tak jak przedstawiłem wyżej metody "IsExistUserInDataBase" oraz "SaveUser".

Inaczej sobie już tego nie wyobrażam, a takie wykonanie faktycznie jest dość fajne. Być może wymaga jeszcze małych korekt, ale chyba w tę stronę warto iść.
Wcześniej jednak zupełnie inaczej sobie wyobrażałem to o czym piszecie. Jakoś nie potrafiłem umieścić sobie tego co Somekind pisze w moim kodzie. Dopiero po przebudowie wydaje mi się, że mnie olśniło.

Ale może nadal źle coś mam? Coś źle zrozumiałem?

2
public OperationStatus RegistryUser()

Nie wiem jak to jest w C#, ale to bodaj znaczy OperationStatus<cokolwiek, cokolwiek>? Czemu masz tak ogólną deklarację metody, skoro zwracasz wyłącznie obiekty typu OperationStatus<bool, string>? Potem będzie tak jak pisałeś, czyli IDE będzie widziało:

public OperationStatus Operation1();
public OperationStatus Operation2();
public OperationStatus Operation3();

A to tylko na Twoje własne życzenie, bo przecież chyba nic nie szkodzi dać OperationStatus<bool, string> w wartości zwracanej?

I tak jak pisałem, popracuj nad angielskim - RegistryUser oznacza RejstrUżytkownik (rejestr, a rejestracja to jak minister i ministrant). IsExistUserInDataBase jest kompletnie niegramatyczne. Inni ludzie mogą mieć później problem ze zrozumieniem tego kodu przez myląco brzmiące identyfikatory (LogInfo to już w ogóle bym pomyślał, że to coś od loggera wiadomości).

Ogólnie to zaczęło to wreszcie iść w jakimś dobrym kierunku, załapałeś żeby nie hackować obiektów staticami i że kontener DI nie służy do ręcznego wyciągania zależności...

0

Nie wiem jak to jest w C#, ale to bodaj znaczy OperationStatus<cokolwiek, cokolwiek>? Czemu masz tak ogólną deklarację metody, skoro zwracasz wyłącznie obiekty typu OperationStatus<bool, string>? Potem będzie tak jak pisałeś, czyli IDE będzie widziało:

Jak zauważyłeś w komentarzach pisałem to z ręki bez kompilacji (musiałem się upewnić, że na pewno zrozumiałem w czym rzecz) i przeoczyłem tą jedną składniową pomyłkę. Akurat tutaj to zwykłe przeoczenie i oczywiście metoda będzie określona w normalny sposób i będzie jasno zaznaczone co ona zwraca. Dzięki za pomoc.

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