Długie ciągi odwołań

2008-08-11 22:12

Chociaż obiekty powinny być jak najbardziej autonomiczne i możliwie niezależne od innych, to bardzo często zdarza się sytuacja, gdy trzeba “zrobić coś” używając czegoś “powszechnie dostępnego”. W praktyce chodzi też o to, by obiekty nie miały kilometrowej długości konstruktorów, do których trzeba by przekazywać wszystko, z czego ewentualnie będą one chciały kiedyś skorzystać.
Dlatego też stosuje się raczej inne rozwiązania, pozwalające w razie potrzeby ‘skoczyć’ w inne miejsce całej sieci powiązań między obiektami, jaka zwykle występuje w programie.

Sieć ta ma zresztą często formę drzewka, wychodzącego od wspólnego korzenia (jakiegoś ‘superobiektu’, reprezentującego całą aplikację), zawierającego w sobie wszystkie obiekty niższych rzędów. Logiczne jest więc zapewnienie odpowiednich metod, pozwalających na dostęp do nich. Wówczas można zawsze rozpocząć od samej góry i, schodząc coraz niżej, dojść w końcu do interesującego nas obiektu.
Z drugiej strony można też rozpoczynać wędrówkę zawsze od obiektu, w którym aktualnie jesteśmy, i w razie potrzeby poruszać się też w górę. Aby było to możliwe, należy jednak dla każdego obiektu jasno określić jego obiekt nadrzędny (parent) i dać możliwość dostępu do niego (np. poprzez przekazanie w konstruktorze odpowiedniego wskaźnika).

Niestety, obie te metody mogą w niektórych sytuacjach zaowocować długimi ciągami wywołań w rodzaju:

  1. ParentObject()->ParentObject()->SubObject3()->Objects(56)->SubObject1()

Według mnie jest to jednak żadna wada. Podobne potworki świadczą tak naprawdę, że struktura programu jest nieprzemyślana i niezgodna z potrzebami. Nic zatem dziwnego, że korzystanie z niej jest trudne i niewygodne. W tym przypadku można wręcz powiedzieć, że język programowania wykazuje się pewną inteligencją, jednoznacznie wskazując nam, że coś robimy źle ;-)

Tags:
Author: Xion, posted under Programming »


11 comments for post “Długie ciągi odwołań”.
  1. Gynvael:
    August 11th, 2008 o 23:20

    Z takich ciekawych potworków, ostatnio znajomy (nekrataal//vx) mi pokazywał kod w PHP który wyglądał mniej więcej tak jak wyżej, a dotyczył SQLa (tworzenia zapytań). Przy czym zabawa polegała na tym że tam każda metoda zwracała swój obiekt, więc był to po prostu ciąg wywołań na tym samym obiekcie, tylko że dziwnie zapisany. Np. (przykład z głowy wzięty)
    $data = $object->from(‘comment’, array(‘id’, ‘date’, ‘content’))
    ->where(‘newsid = ?’, $id)
    ->order(‘date ASC’)
    ->limit(3);
    Wygląda to dziwnie, natomiast imho jest czytelne ;>
    Szczerze to wcześniej się z tym nie spotkałem ;>

  2. Dabroz:
    August 11th, 2008 o 23:30

    Dla tego konkretnego zapytania nie widzę ŻADNEGO powodu dla którego ten zapis jest lepszy niż czysty SQL.

  3. Nek:
    August 12th, 2008 o 0:50

    #2
    Do wiekszych plusów mozna z pewnoscia zaliczyc to, ze taki kod zadziala nam z każdą populraniejszą baza danych, w przeciwienstwie do surowego SQL. Bez problemu można zmienic adapter z np. MySQL na Oracle – co w przypadku czystego SQL, nie byłoby wcale takie bezstresowe.

    Druga sprawa, przy bardziej zlozonych operacjach zapytanie zostanie automatycznie zoptymalizowane pod konkretny silnik bazy.

    Trzecia, $id będzie przefiltrowane – praktycznie nie ma możliwosci zrobienia SQL Injection.

  4. Gynvael:
    August 12th, 2008 o 8:56

    @Dabroz
    To konkretne zapytanie było tylko przykładem, co jest zaznaczone w tekście komentarza ;>
    Po za tym jak Nek stwierdził, choćby filtrowanie SQLI jest w nim zawarte, a w analogicznym zapytaniu w czystym SQL filtrowanie musiało by być oddzielnie.

  5. Dabroz:
    August 12th, 2008 o 9:33

    $id powinno zostać zwalidowane na samym początku wywołania strony. Inne dane podobnie, jeszcze na etapie pobierania danych z formularza. Poza tym, zerknijcie, jak parametry przekazuje się na przykład w ADO.NET.

    A taka klasa bazy danych w PHP to niestety silnikologia w czystym wydaniu. Trzeba poświęcić dużo czasu, jeżeli ma być w pełni funkcjonalna (łączenia tabel, agregacje, wykorzystanie subqueries itd) — zwłaszcza, jeżeli ma być “uniwersalna”. Dlatego ja już z doświadczenia wiem, że o wiele szybciej jest stworzyć wrapper nie na całą bazę danych, lecz na poszczególne zapytania. Dodatkowo unikamy wtedy wzorca “Magic button”. :)

  6. Gynvael:
    August 12th, 2008 o 13:53

    @Dabroz

    Chyba trochę z tematu zboczyliśmy ;>
    Wstawiony przeze mnie przykład pochodzi z użycia Zend Framework, więc dyskusje na temat zasadności użytego przez nich rozwiązania można przenieść na ich forum: http://www.zfforums.com/
    Co do czasu, najwyraźniej twórcy ZF go poświęcili ;> Żeby daleko nie szukać, przykład z manuala ZF na łączenie tabelek:
    $select = $db->select()
    ->from(array(‘p’ => ‘products’),
    array(‘product_id’))
    ->join(array(‘l’ => ‘line_items’),
    ‘p.product_id = l.product_id’,
    array(‘line_items_per_product’ => ‘COUNT(*)’))
    ->group(‘p.product_id’)
    ->order(array(‘line_items_per_product DESC’, ‘product_id’));
    Analogicznie ma np doctrine:
    $query->select(‘b.title, COUNT(c.id) count’)
    ->from(‘Blog b’)
    ->leftJoin(‘b.Comments c’)
    ->limit(10)
    ->useResultCache(true);

    Po za tym zupełnie zignorowałeś stwierdzenie Nek’a o przenośności zapytań. Wspomniane wyżej rozwiązania odcinają programistę od konieczności nauki kolejnych baz danych, oraz metod optymalizacji w nich. Opisane rozwiązania zapewniają az równo przenośność, jak i optymalizacje.
    Więc korzystając z takich klas nie obchodzi cię czy pracujesz na MySQL, Oracle, MSSQL czy jeszcze czymś innym.

    Co do momentu walidacji argumentu, to moim zdaniem nie trzeba pakować tego do relatywnego frontendu jeżeli relatywny backend się tym zajmuje. Po za tym należy rozróżnić walidację związaną z SQLem, od walidacji poprawności danych (np czy email podchodzi pod stosowny wzorzec) – przy odbiorze formularza nie ma sensu walidować rzeczy związanych z SQL – tworzy to niepotrzebne powiązanie frontendu z backendem :)

    Co do ADO.NET, rzuć jakimś kawałkiem kodu ;>

  7. Dabroz:
    August 12th, 2008 o 23:03

    W ADO.NET parametry przekazuje się w bardziej cywilizowany sposób:

    NpgsqlCommandcmd = new NpgsqlCommand(“SELECT * FROM foo WHERE bar = :text AND id = :id AND date < :date”, MyFancyConnection);
    cmd.Parameters.Add(new NpgsqlParameter(“text”, NpgsqlTypes.NpgsqlDbType.Varchar));
    cmd.Parameters[“text”].Value = “NiceNiceNice”;
    cmd.Parameters.Add(new NpgsqlParameter(“date”, NpgsqlTypes.NpgsqlDbType.Timestamp));
    cmd.Parameters[“date”].Value = DateTime.Now;

    i tak dalej, i tak dalej… (Npgsql zastępujemy właściwym prefiksem dla danej bazy danych — w tym przypadku PostgreSQL)

    “walidacji poprawności danych (np czy email podchodzi pod stosowny wzorzec) – przy odbiorze formularza nie ma sensu walidować rzeczy związanych z SQL”

    Tak — odrzucamy $id jeżeli nie jest liczbą, i tak dalej.

    “Wspomniane wyżej rozwiązania odcinają programistę od konieczności nauki kolejnych baz danych, oraz metod optymalizacji w nich.”

    Wszystkie tego typu metody są wielokrotnie bardziej skomplikowane niż dowolne narzecze SQL. A jeżeli coś jest uniwersalne, to siłą rzeczy nie da się zastosować optymalizacji dla konkretnego DBMS (nie mówię tu o optymalizacjach w rodzaju ++i, tylko o poziomie bardziej projektowym).

    Pytanie, czy piszemy skrypt który będzie potem używany w setkach czy tysiącach serwisów i ma być maksymalnie przenośny, czy też piszemy rozwiązanie dedykowane. Ale w poważnych zastosowaniach łatwiej postawić drugi serwer bazodanowy, niż zmieniać kod.

    Ale offtopa robimy. :]

  8. Gynvael:
    August 12th, 2008 o 23:40

    Dobry offtop nie jest zły ;>

    Co do cywilizowanych parametrów, to mamy:
    (‘newsid = ?’, $id)
    VS
    bar = :text
    cmd.Parameters.Add(new NpgsqlParameter(“text”, NpgsqlTypes.NpgsqlDbType.Varchar));
    cmd.Parameters[“text”].Value = “NiceNiceNice”;

    Hmm ;> Kwestia gustu. Mnie akurat przekonuje bardziej ta pierwsza wersja (kapkę krótsza ;>).
    Chociaż widzę że w ADO.NET podaje się dodatkowo typ pola (Varchar). Po co btw ? Jakaś dodatkowa walidacja jest przeprowadzana ?

    No co do poziomu projektowego to się zgodzę, natomiast i tak uważam iż oddzielenie warstwy zapytania od warstwy SQL daje wystarczająco miejsca na pewną automagiczną optymalizację. (choćby kapkę mądrzejsze cacheowanie wyników żeby daleko nie szukać).

    Czasem lepiej postawić drugi server bazodanowy, a czasem lepiej przerobić kod. To w warunkach bojow… praktycznych zależy od upartości klienta ;p

  9. Dabroz:
    August 13th, 2008 o 0:21

    Dodawanie parametrów można akurat opakować w *prostą* metodę i może wyglądać tak:

    Query q = CreateQuery(“SELECT foo FROM bar WHERE x = :y”);
    q.AddParam(“y”, 124, Param.Integer);

    Typ pola podaje się, bo przecież inaczej będzie przekazany argument typu DateTime, a inaczej zwykły String. Poza tym zauważ, że wykorzystanie automatycznych procedur to coś bardziej wyszukanego niż zwykłe addslashes. Dzięki parametrom w ADO.NET można na przykład z łatwością przekazać do bazy danych dane binarne. A jakbyś je przekazał w PHP? :)

    “(choćby kapkę mądrzejsze cacheowanie wyników żeby daleko nie szukać).”

    Bzdura. Taki automatyczny cache sprawdzi się przy zapytaniach w rodzaju SELECT * FROM tabela, INSERT INTO tabela VALUES (…). Widziałem w życiu różne sposoby na cache zapytań i niektóre z nich były na tyle poronione (np. przeliczanie grafu zależności między tabelami, cache danych losowych, opóźnianie wstawień do tabeli; tak z tych ciekawszych), że silnik DB zdążyłby zapytanie wykonać 3 razy w międzyczasie. ;) Cache trzeba stosować inteligentnie — czyli do wyników zapytań, a nie ich samych.

    “Czasem lepiej postawić drugi server bazodanowy, a czasem lepiej przerobić kod. To w warunkach bojow… praktycznych zależy od upartości klienta ;p”

    A czasem najlepiej przekonać klienta, że to, co mu podsuwamy pod nos to Najlepszy Program na Świecie! :) (i żeby nie szukał dziury w całym tudzież na odwrót)

  10. Anonymous:
    August 14th, 2008 o 4:30

    Jak już przy .NET jesteśmy to ja osobiście wolę LINQ ;)

    var query = from p in dc.editors
    where p.id == uid && p.username == User.Identity.Name
    select p;

    Całośc jest automagicznie walidowana i przekształcana w postać sprawną dla bazy ;)

  11. Gynvael:
    August 14th, 2008 o 10:44

    @Dabroz
    Widać jest to sprawa światopoglądu, więc proponuje na tym zakończyć dyskusję ;>

    Co do cache, napisałem wyraźnie “cacheowanie wyników”, więc twoja uwaga w której podkreślasz że trzeba cacheować wyniki mnie rozbawiła ;>
    “Cache trzeba stosować inteligentnie — czyli do wyników zapytań, a nie ich samych.”

    @Anonim
    Wygląda ciekawie ;>

Comments are disabled.
 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.