Jakie są wady wzorca ActiveRecord?

30

Ciekawe, jakie są wady używania wzorca ActiveRecord do dostępu do danych / obiektów biznesowych. Jedyne, co mogę wymyślić z góry, to to, że narusza zasadę pojedynczej odpowiedzialności, ale wzorzec AR jest na tyle powszechny, że sam ten powód nie wydaje się „wystarczająco dobry”, aby uzasadnić nieużywanie go (oczywiście moje widok może być wypaczony, ponieważ często żaden kod, z którym pracuję, nie jest zgodny z żadną z zasad SOLID).

Ja osobiście nie fanem ActiveRecord (z wyjątkiem pisania Ruby on Rails aplikacji, gdzie AR czuje się „naturalne”), ponieważ czuje się jak w klasie robi zbyt wiele, a dostęp do danych nie powinien być aż do samej klasie Poradzić sobie. Wolę używać repozytoriów, które zwracają obiekty biznesowe. Większość kodu, z którym pracuję, ma tendencję do używania odmiany ActiveRecord w postaci (nie wiem, dlaczego metoda jest wartością logiczną):

public class Foo
{
    // properties...

    public Foo(int fooID)
    {
        this.fooID = fooID;
    }

    public bool Load()
    {
        // DB stuff here...
        // map DataReader to properties...

        bool returnCode = false;
        if (dr.HasRows)
            returnCode = true;

        return returnCode;
    }
}

lub czasami bardziej „tradycyjny” sposób posiadania public static Foo FindFooByID(int fooID)metody dla wyszukiwarek i coś podobnego public void Save()do zapisywania / aktualizacji.

Rozumiem, że ActiveRecord jest zwykle o wiele prostszy we wdrożeniu i użyciu, ale wydaje się nieco zbyt prosty w przypadku złożonych aplikacji i możesz mieć bardziej solidną architekturę, zamykając logikę dostępu do danych w repozytorium (nie wspominając o łatwiejszej wymianie strategie dostępu do danych, np. może korzystasz z Przechowanych Procs + DataSets i chcesz przejść na LINQ lub coś takiego)

Jakie więc są inne wady tego wzorca, które należy wziąć pod uwagę przy podejmowaniu decyzji, czy ActiveRecord jest najlepszym kandydatem do pracy?

Wayne Molina
źródło

Odpowiedzi:

28

Główną wadą jest to, że „istoty” zdają sobie sprawę z własnej wytrwałości, która prowadzi do wielu innych złych decyzji projektowych.

Inne problemy polegają na tym, że najbardziej aktywne zestawy narzędzi do nagrywania w zasadzie mapują 1 do 1 na pola tabeli z zerowymi warstwami pośredniczącymi. Działa to na małych skalach, ale rozpada się, gdy masz trudniejsze problemy do rozwiązania.


Cóż, posiadanie przez obiekty informacji o ich trwałości oznacza, że ​​musisz robić takie rzeczy jak:

  • łatwo dostępne połączenia z bazą danych wszędzie. Zwykle prowadzi to do nieprzyjemnego twardego kodowania lub jakiegoś statycznego połączenia, które trafia zewsząd.
  • twoje obiekty wyglądają bardziej jak SQL niż obiekty.
  • trudno jest nic zrobić w aplikacji odłączonej, ponieważ baza danych jest tak głęboko zakorzeniona.

Oprócz tego pojawia się cała masa innych złych decyzji.

Wyatt Barnett
źródło
2
potrafisz rozwinąć „inne złe decyzje projektowe”?
kevin cline
2
Dzięki. Nie znalazłem tych problemów w rozwoju Ruby on Rails. Nadal możliwe jest osobne przetestowanie zachowania i trwałości. IMO oddzielająca upór od zachowania ma niewielką wartość praktyczną.
kevin cline
@kevin: te rzeczy są mniejszą wadą dzięki funkcjom ruby, takim jak mixiny i pisanie kaczek. W przypadku języków statycznych - takich jak C #, którego użył OP w swoim pytaniu - nieco trudniej jest je rozdzielić.
Wyatt Barnett
@Wayne: dla mnie klasy to tylko pola do umieszczenia metod - mogę oddzielić logikę biznesową od uporczywości, umieszczając je w osobnych klasach lub po prostu rozdzielić je koncepcyjnie, upewniając się, że metody biznesowe nie robią tego / O. W językach o słabej obsłudze delegowania (np. Java) oszczędza to ogromną ilość kodu. OTOH, właśnie wchodzę w stan hibernacji, więc mogę się całkowicie mylić.
kevin cline
4
Dodałbym dwie rzeczy; 1. Sprzężenie z mechanizmem utrwalającym utrudnia, jeśli nie niemożliwy, prawidłowy test jednostkowy kodu. 2. Active Record przykleja twój kod do relacji w scentralizowanym mechanizmie trwałości, co naprawdę utrudnia podział twojego monolitu, jeśli kiedykolwiek zdecydujesz.
istepaniuk
15

Największą wadą aktywnego zapisu jest to, że twoja domena zazwyczaj jest ściśle związana z określonym mechanizmem trwałości. Gdyby ten mechanizm wymagał globalnej zmiany, być może z trwałości opartej na plikach na trwałość opartą na DB, lub między ramami dostępu do danych, KAŻDA klasa, która implementuje ten wzorzec, może się zmienić. W zależności od języka, frameworka i projektu nawet coś tak prostego, jak zmiana lokalizacji bazy danych lub kto „jest właścicielem”, może wymagać przejścia przez każdy obiekt w celu zaktualizowania metod dostępu do danych (jest to rzadkie w większości języków, które zapewniają łatwy dostęp do konfigurowania plików za pomocą parametrów połączenia).

Ogólnie wymaga to również powtórzenia się. Większość mechanizmów trwałości ma wiele wspólnego kodu, aby połączyć się z bazą danych i rozpocząć transakcję. DRY (Don't Repeat Yourself) powiedziałby ci jako programistę do scentralizowania takiej logiki.

Utrudnia to także operacje atomowe. Jeśli grupa obiektów musi zostać zapisana w trybie „wszystko albo nic” (na przykład faktura i jej faktury i / lub wpisy klienta i / lub GL), jeden z obiektów musi wiedzieć o wszystkich innych obiektach i kontrolować ich trwałość ( co rozciąga się na zakres obiektu kontrolującego; duże połączone ze sobą rekordy mogą łatwo stać się „obiektami boskimi”, które wiedzą wszystko o ich zależnościach), lub kontrola nad całą transakcją musi być obsługiwana spoza domeny (i w takim przypadku dlaczego używasz AR?)

Jest to również „złe” z perspektywy obiektowej. W prawdziwym świecie faktura nie wie, jak się złożyć, więc dlaczego obiekt kodu faktury miałby wiedzieć, jak zapisać się w bazie danych? Oczywiście zbyt religijne przestrzeganie „obiektów powinno modelować tylko to, co potrafią ich odpowiedniki w świecie rzeczywistym”, prowadziłoby do anemicznych modeli domenowych (faktura również nie wie, jak obliczyć własną sumę, ale dzielenie obliczenia tej sumy na inny obiekt jest ogólnie uważany za zły pomysł).

KeithS
źródło
W Ruby, gdzie trwałość może być zdefiniowana lub wyodrębniona za pomocą bardzo prostego słowa kluczowego lub polecenia, prawdopodobnie jest to mniejszy problem. W .NET, który wymaga więcej LoC do skonfigurowania różnych mechanizmów trwałości, zwykle nadmiarowe jest połączenie SQL dla każdego obiektu, szczególnie jeśli wiele obiektów musi być zapisanych w jednej transakcji atomowej. Połączenie z DB wygląda tak samo bez względu na to, czy zapisujesz fakturę, czy klienta. Byś albo abstrakcyjny wspólne rzeczy do wspólnej klasy bazowej dla wszystkich ActiveRecord obiektów w kodzie, lub wyodrębnić go do własnej klasy (repozytorium)
KeithS
czy możesz podać przykłady użycia Ruby ActiveRecord, które ilustrują twoje punkty? Nie napotkałem tych problemów, ale moja aplikacja była dość mała. Mógłbym pisać migracje w Rubim i wdrażać je w różnych implementacjach DB. Odkryłem, że ActiveRecord Ruby był bardzo przydatny w eliminowaniu powtórzeń, więc nie rozumiem, dlaczego twierdziłbyś, że użycie ActiveRecord miałoby odwrotny skutek. Nie miałem problemów z zapisywaniem: właśnie zmodyfikowałem model obiektu i pozwoliłem AR zaktualizować bazę danych, aby odzwierciedlała model obiektu.
kevin cline,
2

Podstawową wadą jest to, że sprawia, że model domeny jest złożony, ponieważ zawiera nie tylko logikę biznesową, ale także informacje o trwałości.

Tak więc rozwiązaniem jest wykorzystanie implementacji ORM Data Mappera . To oddziela warstwę trwałości i jesteśmy teraz bardziej skoncentrowani na logice biznesowej encji. Doctrine to ORM Mapera danych .

Ale to podejście ma również pewną złożoność. W przypadku zapytań w zbyt dużym stopniu zależy od Mapera danych, sprawia, że ​​środowisko zorientowane na zapytania. Aby to uprościć, wprowadzono kolejną warstwę między Modelem Domeny, a Maperem Danych nazywa się Repozytorium .

Repozytorium wyodrębnia warstwę trwałości. Wyczuwa programowanie obiektowe w tym sensie, że jest kolekcją wszystkich obiektów tego samego typu (jak wszystkie encje przechowywane w tabeli bazy danych) i możesz wykonywać na nich operacje jako operacje kolekcjonowania, dodawania , usuwania . zawiera itp.

Na przykład dla encji użytkownika będzie UserRepository, które reprezentują kolekcję tego samego typu obiektów użytkownika (przechowywanych w tabeli użytkowników), nad którymi można wykonać operację. W celu zapytania do tabeli użytkowników wykorzystuje Mapowanie danych użytkownika, ale wyodrębnił ją do modelu domeny Użytkownik .

Wzorzec repozytorium jest rodzajem warstwy dostępu do danych , inną są różnice tylko w obiekcie dostępu do danych Repozytorium ma funkcję Agreguj root

palash140
źródło
Wzorce repozytorium i DAO różnią się, DAO zapewnia ogólny dostęp do danych, Repozytorium służy do przechowywania kolekcji wszystkich obiektów tego samego typu. Tj. Wszystkie repozytoria powinny mieć ten sam interfejs (jak tablica lub lista). Inny król metod nie należy do repozytorium. DAO ma niższy poziom, Repozytorium może używać DAO. Często programiści (zwłaszcza PHP) używają Repozytorium jako DAO, ale to nie jest poprawne.
xmedeko,