Czy łamię praktykę OOP w tej architekturze?

23

Mam aplikację internetową. Nie wierzę, że technologia jest ważna. Struktura jest aplikacją na poziomie N, pokazaną na obrazku po lewej stronie. Istnieją 3 warstwy.

Interfejs użytkownika (wzorzec MVC), warstwa logiki biznesowej (BLL) i warstwa dostępu do danych (DAL)

Mam problem z tym, że moja BLL jest ogromna, ponieważ ma logikę i ścieżki w wywołaniu zdarzeń aplikacji.

Typowy przepływ przez aplikację może być:

Zdarzenie uruchomione w interfejsie użytkownika, przejdź do metody w BLL, wykonaj logikę (być może w wielu częściach BLL), ostatecznie do DAL, z powrotem do BLL (gdzie prawdopodobnie więcej logiki), a następnie zwróć pewną wartość do interfejsu użytkownika.

BLL w tym przykładzie jest bardzo zajęty i zastanawiam się, jak to rozdzielić. Mam też logikę i obiekty, które mi się nie podobają.

wprowadź opis zdjęcia tutaj

Wersja po prawej to mój wysiłek.

Logika jest nadal jak płynie wniosek między UI i DAL, ale są prawdopodobnie nie ma właściwości ... Tylko metody (większość klas w tej warstwie mogłoby ewentualnie być statyczny, ponieważ nie przechowuje żadnych stanu). Warstwa Poco to miejsce, w którym istnieją klasy, które mają właściwości (takie jak klasa Person, w której będzie imię, wiek, wzrost itp.). Nie miałyby one nic wspólnego z przepływem aplikacji, tylko przechowują stan.

Przepływ może być:

Nawet wyzwolony z interfejsu użytkownika i przekazuje niektóre dane do kontrolera warstwy interfejsu użytkownika (MVC). To tłumaczy nieprzetworzone dane i konwertuje je na model poco. Model poco jest następnie przekazywany do warstwy logicznej (która była BLL) i ostatecznie do warstwy zapytania, potencjalnie manipulowanej po drodze. Warstwa zapytania Command przekształca POCO w obiekt bazy danych (które są prawie takie same, ale jeden jest przeznaczony do trwałości, a drugi do interfejsu użytkownika). Element zostanie zapisany, a obiekt bazy danych zostanie zwrócony do warstwy zapytania. Następnie jest konwertowany na POCO, gdzie wraca do warstwy logicznej, potencjalnie przetwarzany dalej, a następnie z powrotem do interfejsu użytkownika

Wspólna logika i interfejsy to miejsce, w którym możemy mieć trwałe dane, takie jak MaxNumberOf_X i TotalAllowed_X i wszystkie interfejsy.

Zarówno wspólna logika / interfejsy, jak i DAL są „podstawą” architektury. Nic nie wiedzą o świecie zewnętrznym.

Wszystko wie o poco innych niż wspólna logika / interfejsy i DAL.

Przepływ jest nadal bardzo podobny do pierwszego przykładu, ale sprawia, że ​​każda warstwa jest bardziej odpowiedzialna za 1 rzecz (czy to stan, przepływ, czy cokolwiek innego) ... ale czy łamie OOP przy takim podejściu?

Przykładem demonstracji Logiki i Poco może być:

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method1(PocoB pocoB) 
    { 
        return cmdQuery.Save(pocoB); 
    }

    /*This has no state objects, only ways to communicate with other 
    layers such as the cmdQuery. Everything else is just function 
    calls to allow flow via the program */
    public PocoA Method2(PocoB pocoB) 
    {         
        pocoB.UpdateState("world"); 
        return Method1(pocoB);
    }

}

public struct PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     public int DataC {get;set;}

    /*This simply returns something that is part of this class. 
     Everything is self-contained to this class. It doesn't call 
     trying to directly communicate with databases etc*/
     public int GetValue()
     {

         return DataB * DataC; 
     }

     /*This simply sets something that is part of this class. 
     Everything is self-contained to this class. 
     It doesn't call trying to directly communicate with databases etc*/
     public void UpdateState(string input)
     {        
         DataA += input;  
     }
}
MyDaftQuestions
źródło
Nie widzę nic zasadniczo złego w twojej architekturze, tak jak ją obecnie opisałeś.
Robert Harvey
19
W przykładowym kodzie nie ma wystarczającej liczby szczegółów funkcjonalnych, aby zapewnić dalszy wgląd. Przykłady Foobar rzadko dają wystarczającą ilustrację.
Robert Harvey
1
Przesłano do rozpatrzenia: Baruco 2012: Dekonstrukcja szkieletu, autor: Gary Bernhardt
Theraot
4
Czy możemy znaleźć lepszy tytuł dla tego pytania, aby łatwiej było go znaleźć w Internecie?
Soner Gönül
1
Po prostu pedantyczny: poziom i warstwa to nie to samo. „Warstwa” mówi o wdrożeniu, „warstwa” o logice. Twoja warstwa danych zostanie wdrożona zarówno na poziomie kodu po stronie serwera, jak i na poziomie bazy danych. Warstwa interfejsu użytkownika zostanie wdrożona zarówno w warstwie kodu klienta WWW, jak i serwera. Przedstawiona architektura jest trójwarstwowa. Twoje warstwy to „klient sieci Web”, „kod serwera” i „baza danych”.
Laurent LA RIZZA

Odpowiedzi:

54

Tak, najprawdopodobniej łamiesz podstawowe koncepcje OOP. Jednak nie czuj się źle, ludzie robią to cały czas, nie oznacza to, że twoja architektura jest „zła”. Powiedziałbym, że jest to prawdopodobnie mniej konserwowalne niż odpowiedni projekt OO, ale jest to raczej subiektywne i nie jest to twoje pytanie. ( Oto mój artykuł krytykujący ogólnie architekturę n-tier).

Rozumowanie : najbardziej podstawową koncepcją OOP jest to, że dane i logika tworzą jedną jednostkę (obiekt). Chociaż jest to bardzo uproszczone i mechaniczne stwierdzenie, mimo to tak naprawdę nie jest przestrzegane w twoim projekcie (jeśli dobrze cię rozumiem). Dość wyraźnie oddzielasz większość danych od większości logiki. Posiadanie metod bezstanowych (statycznych) na przykład nazywa się „procedurami” i ogólnie są one przeciwne do OOP.

Oczywiście zawsze są wyjątki, ale ten projekt z reguły narusza te rzeczy.

Ponownie chciałbym podkreślić, że „narusza OOP”! = „Źle”, więc niekoniecznie jest to ocena wartości. Wszystko zależy od ograniczeń architektury, przypadków użycia w zakresie konserwacji, wymagań itp.

Robert Bräutigam
źródło
9
Mam głos, jest to dobra odpowiedź, gdybym pisał własny, skopiowałbym i wkleił to, ale także dodałbym, że jeśli zauważysz, że nie piszesz kodu OOP, być może powinieneś rozważyć język inny niż OOP ma wiele dodatkowych kosztów ogólnych, bez których możesz się obejść, jeśli go nie używasz
TheCatWhisperer
2
@TheCatWhisperer: Nowoczesne architektury korporacyjne nie wyrzucają OOP całkowicie, tylko selektywnie (np. Dla DTO).
Robert Harvey
@RobertHarvey Zgadzam się, miałem na myśli, że jeśli nie używasz OOP prawie w dowolnym miejscu w swoim projekcie
TheCatWhisperer
@TheCatWhisperer wiele zalet w OOP, takich jak C #, niekoniecznie jest w części OOP języka, ale w dostępnej pomocy technicznej, takiej jak biblioteki, studio graficzne, zarządzanie pamięcią itp.
@Orangesandlemons Jestem pewien, że istnieje wiele innych dobrze obsługiwanych języków ...
TheCatWhisperer
31

Jedną z podstawowych zasad programowania funkcjonalnego są czyste funkcje.

Jedną z podstawowych zasad programowania obiektowego jest łączenie funkcji z danymi, na których działają.

Obie te podstawowe zasady znikają, gdy aplikacja musi komunikować się ze światem zewnętrznym. Rzeczywiście, możesz być wierny tym ideałom tylko w specjalnie przygotowanej przestrzeni w swoim systemie. Nie każda linia kodu musi spełniać te ideały. Ale jeśli żaden wiersz kodu nie spełnia tych ideałów, nie można tak naprawdę twierdzić, że używasz OOP lub FP.

Tak więc możesz mieć tylko „obiekty” danych, które rzucasz, ponieważ potrzebujesz, aby przekroczyły granicę, której po prostu nie można zmienić, aby przenieść zainteresowany kod. Po prostu wiedz, że to nie jest OOP. To jest rzeczywistość. OOP polega na tym, że gdy znajdziesz się w tej granicy, zgromadzisz całą logikę, która działa na te dane w jednym miejscu.

Nie musisz też tego robić. OOP to nie wszystko dla wszystkich ludzi. Jest jak jest. Po prostu nie twierdz, że coś dzieje się po OOP, gdy tak się nie stanie lub masz zamiar dezorientować ludzi próbujących utrzymać Twój kod.

Wygląda na to, że twoje POCO mają logikę biznesową, więc nie martwiłbym się zbytnio anemią. Martwi mnie to, że wszystkie wydają się bardzo zmienne. Pamiętaj, że metody pobierające i ustawiające nie zapewniają prawdziwej enkapsulacji. Jeśli twój POCO zmierza do tej granicy, to dobrze. Po prostu zrozum, że nie daje to pełnych korzyści z prawdziwego enkapsulowanego obiektu OOP. Niektórzy nazywają to Obiektem Transferu Danych lub DTO.

Sztuką, którą z powodzeniem zastosowałem, jest tworzenie obiektów OOP, które jedzą DTO. Używam DTO jako obiektu parametru . Mój konstruktor odczytuje z niego stan (odczytuje jako kopię obronną ) i odrzuca go na bok. Teraz mam w pełni zamkniętą i niezmienną wersję DTO. Wszystkie metody związane z tymi danymi można przenieść tutaj, pod warunkiem, że znajdują się po tej stronie granicy.

Nie dostarczam getterów ani seterów. Idę powiedzieć, nie pytaj . Wywołujecie moje metody i robią to, co trzeba. Prawdopodobnie nawet nie mówią ci, co zrobili. Po prostu to robią.

Teraz w końcu coś, gdzieś natrafi na inną granicę i to wszystko znów się rozpada. W porządku. Obróć kolejne DTO i przerzuć je przez ścianę.

Na tym właśnie polega architektura portów i adapterów. Czytałem o tym z funkcjonalnego punktu widzenia . Może cię też to zainteresuje.

candied_orange
źródło
5
pobierający i ustawiający nie zapewniają prawdziwej enkapsulacji ” - tak!
Boris the Spider
3
@ BoristheSpider - metody pobierające i ustawiające absolutnie zapewniają enkapsulację, po prostu nie pasują do wąskiej definicji enkapsulacji.
Davor Ždralo
4
@ DavorŽdralo: Czasami są użyteczne jako obejście, ale ze względu na swoją naturę gettery i setery przerywają enkapsulację. Zapewnienie sposobu na uzyskanie i ustawienie wewnętrznej zmiennej jest przeciwieństwem bycia odpowiedzialnym za swój własny stan i działania na nim.
cHao
5
@cHao - nie rozumiesz, czym jest getter. Nie oznacza to metody zwracającej wartość właściwości obiektu. Jest to powszechna implementacja, ale może zwrócić wartość z bazy danych, zażądać jej przez http, obliczyć ją w locie, cokolwiek. Tak jak powiedziałem, gettery i settery przerywają enkapsulację tylko wtedy, gdy ludzie używają własnych wąskich (i niepoprawnych) definicji.
Davor Ždralo
4
@cHao - enkapsulacja oznacza, że ​​ukrywasz implementację. Właśnie to zostaje zamknięte. Jeśli masz getter „getSurfaceArea ()” w klasie Square, nie wiesz, czy pole powierzchni jest polem, czy jest obliczane w locie (wysokość powrotu * szerokość), czy w trzeciej metodzie, więc możesz zmienić implementację wewnętrzną kiedy tylko chcesz, bo jest zamknięty.
Davor Ždralo
1

Jeśli poprawnie odczytam twoje wyjaśnienie, twoje obiekty wyglądają trochę tak: (trudne bez kontekstu)

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method(PocoB pocoB) { ... }
}

public class PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     ... etc
}

W tym, że twoje klasy Poco zawierają tylko dane, a twoje klasy logiki zawierają metody, które działają na tych danych; tak, złamałeś zasady „Classic OOP”

Ponownie trudno jest stwierdzić na podstawie twojego ogólnego opisu, ale zaryzykowałbym to, że to, co napisałeś, może być sklasyfikowane jako Anemiczny Model Domeny.

Nie sądzę, że jest to szczególnie złe podejście, ani, jeśli weźmiesz pod uwagę Poco jako struktury, nie jest to w żaden sposób nie do złamania OOP w bardziej konkretnym sensie. Dzięki temu Twoje obiekty są teraz LogicClasses. Rzeczywiście, jeśli sprawisz, że Twoje Pocos będą niezmienne, projekt można uznać za całkiem funkcjonalny.

Kiedy jednak odwołujesz się do Shared Logic, Pocos, które są prawie, ale nie takie same i statyki, zaczynam się martwić o szczegóły twojego projektu.

Ewan
źródło
Dodałem do mojego posta, zasadniczo kopiując twój przykład. Przepraszam, że na początku nie było jasne
MyDaftQuestions
1
mam na myśli to, że jeśli powiesz nam, co robi aplikacja, łatwiej byłoby napisać przykłady. Zamiast LogicClass możesz mieć PaymentProvider lub cokolwiek innego
Ewan
1

Jeden potencjalny problem, który widziałem w twoim projekcie (i jest bardzo powszechny) - niektóre z absolutnie najgorszych kodów „OO”, jakie kiedykolwiek spotkałem, były spowodowane przez architekturę, która oddzielała obiekty „Data” od obiektów „Code”. To koszmarne rzeczy! Problem polega na tym, że wszędzie w kodzie biznesowym, gdy chcesz uzyskać dostęp do swoich obiektów danych, masz tendencję do po prostu kodowania bezpośrednio tam (nie musisz, możesz zbudować klasę narzędziową lub inną funkcję do obsługi tego, ale to właśnie to Z biegiem czasu widziałem to wielokrotnie).

Kod dostępu / aktualizacji zazwyczaj nie jest gromadzony, więc wszędzie znajdziesz duplikaty.

Z drugiej strony te obiekty danych są przydatne, na przykład jako trwałość bazy danych. Wypróbowałem trzy rozwiązania:

Kopiowanie i wysyłanie wartości do „prawdziwych” obiektów i wyrzucanie obiektu danych jest żmudne (ale może być poprawnym rozwiązaniem, jeśli chcesz pójść tą drogą).

Dodanie metod przenoszenia danych do obiektów danych może działać, ale może sprawić, że duży, nieporządny obiekt danych robi więcej niż jedną rzecz. Może to również utrudnić enkapsulację, ponieważ wiele mechanizmów trwałości chce publicznych akcesorów ... Nie podobało mi się to, kiedy to zrobiłem, ale jest to prawidłowe rozwiązanie

Najlepszym rozwiązaniem dla mnie jest koncepcja klasy „Wrapper”, która zawiera klasę „Data” i zawiera wszystkie funkcje przekierowywania danych - wtedy nie ujawniam w ogóle klasy danych (nawet seterów i getterów chyba że są absolutnie potrzebne). Eliminuje to pokusę bezpośredniego manipulowania obiektem i zmusza do dodania współdzielonej funkcjonalności do opakowania.

Inną zaletą jest to, że możesz mieć pewność, że twoja klasa danych jest zawsze w poprawnym stanie. Oto szybki przykład kodu psuedocode:

// Data Class
Class User {
    String name;
    Date birthday;
}

Class UserHolder {
    final private User myUser // Cannot be null or invalid

    // Quickly wrap an object after getting it from the DB
    public UserHolder(User me)
    {
        if(me == null ||me.name == null || me.age < 0)
            throw Exception
        myUser=me
    }

    // Create a new instance in code
    public UserHolder(String name, Date birthday) {
        User me=new User()
        me.name=name
        me.birthday=birthday        
        this(me)
    }
    // Methods access attributes, they try not to return them directly.
    public boolean canDrink(State state) {
        return myUser.birthday.year < Date.yearsAgo(state.drinkingAge) 
    }
}

Pamiętaj, że nie masz kontroli wieku w całym kodzie w różnych obszarach, a także nie masz ochoty jej używać, ponieważ nie możesz nawet dowiedzieć się, jakie są urodziny (chyba że potrzebujesz jej na coś innego, w w którym przypadku możesz go dodać).

Zwykle nie rozszerzam obiektu danych, ponieważ tracisz tę enkapsulację i gwarancję bezpieczeństwa - w tym momencie możesz po prostu dodać metody do klasy danych.

W ten sposób w Twojej logice biznesowej nie ma wielu śmieci / iteratorów dostępu do danych, staje się o wiele bardziej czytelny i mniej zbędny. Polecam również nawyk zawsze owijania kolekcji z tego samego powodu - utrzymywanie zapętlania / wyszukiwania konstrukcji poza logiką biznesową i upewnianie się, że zawsze są w dobrym stanie.

Bill K.
źródło
1

Nigdy nie zmieniaj kodu, ponieważ myślisz lub ktoś mówi ci, że to nie to lub nie to. Zmień kod, jeśli stwarza to problemy i wymyśliłeś sposób na uniknięcie tych problemów bez tworzenia innych.

Więc poza tym, że nie lubisz rzeczy, chcesz zainwestować dużo czasu, aby dokonać zmian. Zapisz problemy, które masz teraz. Napisz, jak Twój nowy projekt rozwiązałby problemy. Oblicz wartość ulepszenia i koszt wprowadzenia zmian. Następnie - i to jest najważniejsze - upewnij się, że masz czas na dokończenie tych zmian, w przeciwnym razie skończysz w połowie w tym stanie, w połowie w tym stanie, a to najgorsza możliwa sytuacja. (Kiedyś pracowałem nad projektem z 13 różnymi typami ciągów i trzema możliwymi do zidentyfikowania połowicznymi wysiłkami, aby ustandaryzować jeden typ)

gnasher729
źródło
0

Kategoria „OOP” jest znacznie większa i bardziej abstrakcyjna niż to, co opisujesz. Nie przejmuje się tym wszystkim. Dba o wyraźną odpowiedzialność, spójność, połączenie. Na poziomie, o który pytasz, nie ma sensu pytać o „praktykę OOPS”.

To powiedziawszy na przykład:

Wydaje mi się, że istnieje nieporozumienie na temat tego, co oznacza MVC. Nazywasz swój interfejs użytkownika „MVC”, niezależnie od logiki biznesowej i kontroli „zaplecza”. Ale dla mnie MVC obejmuje całą aplikację internetową:

  • Model - zawiera dane biznesowe + logikę
    • Warstwa danych jako szczegół implementacji modelu
  • Widok - kod interfejsu użytkownika, szablony HTML, CSS itp.
    • Obejmuje aspekty po stronie klienta, takie jak JavaScript lub biblioteki aplikacji internetowych „jednostronicowych” itp.
  • Kontrola - klej po stronie serwera między wszystkimi innymi częściami
  • (Istnieją rozszerzenia, takie jak ViewModel, Batch itp., Do których nie będę wchodzić tutaj)

Istnieją tutaj niezwykle ważne podstawowe założenia:

  • Klasa / obiekty modelowe nigdy nie mają żadnej wiedzy na temat innych części (widok, sterowanie, ...). Nigdy ich nie wywołuje, nie zakłada, że ​​są przez nich wywoływane, nie otrzymuje atrybutów / parametrów sesji ani niczego innego w tym zakresie. Jest całkowicie sam. W językach, które to obsługują (np. Ruby), możesz uruchomić ręczny wiersz poleceń, utworzyć instancję klas Modelu, pracować z nimi do woli i robić wszystko, co robią bez żadnego wystąpienia Kontroli, Widoku lub innej kategorii. Co najważniejsze, nie ma wiedzy na temat sesji, użytkowników itp.
  • Nic nie ma wpływu na warstwę danych poza modelem.
  • Widok ma tylko lekki akcent na modelu (wyświetlanie rzeczy itp.) I nic więcej. (Zauważ, że dobrym rozszerzeniem jest „ViewModel”, które są specjalnymi klasami, które wykonują bardziej znaczące przetwarzanie w celu renderowania danych w skomplikowany sposób, co nie pasowałoby dobrze ani do Modelu, ani do Widoku - jest to dobry kandydat do usuwania / unikania wzdęć w czysty model).
  • Kontrola jest tak lekka, jak to możliwe, ale odpowiada za zebranie wszystkich innych graczy razem i przenoszenie rzeczy między nimi (tj. Wyodrębnianie wpisów użytkownika z formularza i przesyłanie go do modelu, przekazywanie wyjątków z logiki biznesowej do użytecznego komunikaty o błędach dla użytkownika itp.). W przypadku interfejsów API sieci Web / HTTP / REST itp. Wszystkie autoryzacje, zabezpieczenia, zarządzanie sesjami, zarządzanie użytkownikami itp. Mają miejsce tutaj (i tylko tutaj).

Co ważne: interfejs użytkownika jest częścią MVC. Nie na odwrót (jak na schemacie). Jeśli to zaakceptujesz, wtedy tłuste modele są naprawdę całkiem dobre - pod warunkiem, że rzeczywiście nie zawierają rzeczy, których nie powinny.

Zauważ, że „tłuste modele” oznaczają, że cała logika biznesowa należy do kategorii Model (pakiet, moduł, niezależnie od nazwy w wybranym języku). Poszczególne klasy powinny oczywiście mieć strukturę OOP w dobry sposób, zgodnie z wszelkimi wytycznymi dotyczącymi kodowania, które sam sobie podajesz (tj. Niektóre maksymalne wiersze kodu dla klasy lub metody itp.).

Należy również pamiętać, że sposób implementacji warstwy danych ma bardzo ważne konsekwencje; zwłaszcza, czy warstwa modelu jest w stanie funkcjonować bez warstwy danych (np. do testowania jednostek lub do tanich DB w pamięci na laptopie programisty zamiast drogich DB Oracle lub cokolwiek masz). Ale tak naprawdę jest to szczegół implementacji na poziomie architektury, na którą teraz patrzymy. Oczywiście tutaj nadal chcesz mieć separację, tzn. Nie chciałbym widzieć kodu, który ma czystą logikę domeny bezpośrednio przeplataną z dostępem do danych, intensywnie łącząc to ze sobą. Temat na inne pytanie.

Wracając do twojego pytania: Wydaje mi się, że między twoją nową architekturą a schematem MVC, który opisałem, istnieje duże pokrywanie się, więc nie jesteś w zupełnie złym kierunku, ale wydaje się, że albo wymyślasz coś na nowo, lub używając go, ponieważ sugeruje to obecne środowisko programistyczne / biblioteki. Trudno mi powiedzieć. Nie mogę więc udzielić ci dokładnej odpowiedzi, czy to, co zamierzasz, jest szczególnie dobre, czy złe. Możesz dowiedzieć się, sprawdzając, czy każda „rzecz” ma za to dokładnie jedną klasę; czy wszystko jest bardzo spójne i mało sprzężone. To daje dobrą wskazówkę i moim zdaniem wystarcza na dobry projekt OOP (lub dobry test porównawczy tego samego, jeśli chcesz).

AnoE
źródło