Zasada DRY w dobrych praktykach?

11

Staram się przestrzegać zasady DRY w moim programowaniu tak mocno, jak potrafię. Ostatnio uczyłem się wzorców projektowych w OOP i skończyło się na tym, że powtarzałem sobie całkiem sporo.

Utworzyłem wzorzec repozytorium wraz ze wzorami Factory i Gateway, aby obsłużyć moją trwałość. Korzystam z bazy danych w mojej aplikacji, ale to nie powinno mieć znaczenia, ponieważ powinienem móc wymienić bramkę i przejść na inny rodzaj uporczywości, jeśli chciałbym.

Problem, który stworzyłem dla siebie, polega na tym, że tworzę te same obiekty dla liczby posiadanych tabel. Na przykład będą to obiekty, których potrzebuję do obsługi tabeli comments.

class Comment extends Model {

    protected $id;
    protected $author;
    protected $text;
    protected $date;
}

class CommentFactory implements iFactory {

    public function createFrom(array $data) {
        return new Comment($data);
    }
}

class CommentGateway implements iGateway {

    protected $db;

    public function __construct(\Database $db) {
        $this->db = $db;
    }

    public function persist($data) {

        if(isset($data['id'])) {
            $sql = 'UPDATE comments SET author = ?, text = ?, date = ? WHERE id = ?';
            $this->db->prepare($sql)->execute($data['author'], $data['text'], $data['date'], $data['id']);
        } else {
            $sql = 'INSERT INTO comments (author, text, date) VALUES (?, ?, ?)';
            $this->db->prepare($sql)->execute($data['author'], $data['text'], $data['date']);
        }
    }

    public function retrieve($id) {

        $sql = 'SELECT * FROM comments WHERE id = ?';
        return $this->db->prepare($sql)->execute($id)->fetch();
    }

    public function delete($id) {

        $sql = 'DELETE FROM comments WHERE id = ?';
        return $this->db->prepare($sql)->execute($id)->fetch();
    }
}

class CommentRepository {

    protected $gateway;
    protected $factory;

    public function __construct(iFactory $f, iGateway $g) {
        $this->gateway = $g;
        $this->factory = $f;
    }

    public function get($id) {

        $data = $this->gateway->retrieve($id);
        return $this->factory->createFrom($data);
    }

    public function add(Comment $comment) {

        $data = $comment->toArray();
        return $this->gateway->persist($data);
    }
}

Wtedy wygląda mój kontroler

class Comment {

    public function view($id) {

        $gateway = new CommentGateway(Database::connection());
        $factory = new CommentFactory();
        $repo = new CommentRepository($factory, $gateway);

        return Response::view('comment/view', $repo->get($id));
    }
}

Pomyślałem więc, że prawidłowo używam wzorców projektowych i przestrzegam dobrych praktyk, ale problem polega na tym, że kiedy dodam nową tabelę, muszę stworzyć te same klasy tylko z innymi nazwami. Rodzi to we mnie podejrzenie, że mogę robić coś złego.

Pomyślałem o rozwiązaniu, w którym zamiast interfejsów miałem abstrakcyjne klasy, które używając nazwy klasy określają tabelę, którą muszą manipulować, ale to nie wydaje się właściwe, co zrobić, jeśli zdecyduję się na przechowywanie plików lub memcache, gdzie nie ma tabel.

Czy podchodzę do tego poprawnie, czy jest inna perspektywa, na którą powinienem patrzeć?

Emilio Rodrigues
źródło
Czy podczas tworzenia nowej tabeli zawsze używasz tego samego zestawu zapytań SQL (lub bardzo podobnego zestawu) do interakcji z nim? Czy fabryka zawiera jakąkolwiek sensowną logikę w prawdziwym programie?
Ixrec
@Ixrec zwykle w bramie i repozytorium zwykle znajdują się niestandardowe metody, które wykonują bardziej złożone zapytania SQL, takie jak łączenia, problem polega na tym, że funkcje utrwalania, pobierania i usuwania - zdefiniowane przez interfejs - są zawsze takie same, z wyjątkiem nazwy tabeli i ewentualnie ale mało prawdopodobna jest kolumna klucza podstawowego, więc muszę powtarzać je w każdej implementacji. Fabryka bardzo rzadko ma jakąkolwiek logikę i czasami pomijam ją w ogóle, a brama zwraca obiekt zamiast danych, ale stworzyłem fabrykę dla tego przykładu, ponieważ powinien to być odpowiedni projekt?
Emilio Rodrigues,
Prawdopodobnie nie jestem uprawniony do udzielenia prawidłowej odpowiedzi, ale mam wrażenie, że 1) klasy Factory i Repository nie robią nic pożytecznego, więc lepiej byłoby porzucić je i pracować bezpośrednio z komentarzem i komentarzem Gateway 2) powinno być możliwe do wprowadzenia Wspólnej utrzymują / odzyskać / funkcji Usuń w jednym miejscu zamiast kopiowaniem wklejanie ich, być może w abstrakcyjnej klasy „wdrożeń default” (trochę jak co kolekcje w Java zrobić)
Ixrec

Odpowiedzi:

12

Problem, który rozwiązujesz, jest dość podstawowy.

Ten sam problem napotkałem, pracując dla firmy, która stworzyła dużą aplikację J2EE, która składała się z kilkuset stron internetowych i ponad półtora miliona linii kodu Java. Ten kod używał ORM (JPA) do utrwalania.

Problem ten nasila się, gdy używasz technologii firm trzecich w każdej warstwie architektury, a wszystkie technologie wymagają własnej reprezentacji danych.

Twojego problemu nie można rozwiązać na poziomie używanego języka programowania. Używanie wzorców jest dobre, ale jak widzisz, powoduje powtarzanie kodu (lub dokładniej mówiąc: powtarzanie wzorów).

Moim zdaniem istnieją tylko 3 możliwe rozwiązania. W praktyce rozwiązania te sprowadzają się do tego samego.

Rozwiązanie 1: Użyj innej struktury utrwalania, która pozwala określić tylko to, co należy utrwalić. Prawdopodobnie istnieją takie ramy. Problem z tym podejściem polega na tym, że jest on dość naiwny, ponieważ nie wszystkie wzorce będą związane z trwałością. Będziesz także chciał używać wzorców dla kodu interfejsu użytkownika, więc będziesz potrzebował struktury GUI, która może ponownie użyć reprezentacji danych wybranej przez ciebie struktury trwałości. Jeśli nie możesz ich ponownie użyć, musisz napisać kod płyty kotłowej, aby połączyć reprezentacje danych w ramach GUI i frameworku trwałości. To znowu jest sprzeczne z zasadą DRY.

Rozwiązanie 2: Użyj innego - mocniejszego - języka programowania, który ma konstrukcje, które pozwalają wyrazić powtarzalny projekt, dzięki czemu możesz ponownie użyć kodu projektu. Prawdopodobnie nie jest to dla ciebie opcja, ale załóżmy, że tak jest przez chwilę. Z drugiej strony, kiedy zaczniesz tworzyć interfejs użytkownika na wierzchu warstwy trwałej, będziesz chciał, aby język znów był wystarczająco mocny, aby obsługiwać tworzenie GUI bez konieczności pisania kodu płyty kotłowej. Jest mało prawdopodobne, aby istniał język wystarczająco silny, aby robić to, co chcesz, ponieważ większość języków opiera się na zewnętrznych platformach do tworzenia GUI, z których każdy wymaga własnej reprezentacji danych do działania.

Rozwiązanie 3: Zautomatyzuj powtarzanie kodu i projektowanie za pomocą jakiejś formy generowania kodu. Martwisz się o konieczność ręcznego kodowania powtórzeń wzorów i projektów, ponieważ ręczne kodowanie powtarzalnego kodu / projektu narusza zasadę DRY. Obecnie istnieją bardzo potężne frameworki generatorów kodów. Istnieją nawet „językowe stoły robocze”, które pozwalają szybko (pół dnia, gdy nie masz doświadczenia) stworzyć własny język programowania i wygenerować dowolny kod (PHP / Java / SQL - dowolny plik tekstowy możliwy do wyobrażenia) przy użyciu tego języka. Mam doświadczenie z XText, ale MetaEdit i MPS również wydają się być w porządku. Radzę sprawdzić jeden z tych językowych stanowisk roboczych. Dla mnie było to najbardziej wyzwalające doświadczenie w moim życiu zawodowym.

Za pomocą Xtext możesz zmusić maszynę do generowania powtarzalnego kodu. Xtext generuje nawet edytor podświetlania składni z uzupełnianiem kodu dla specyfikacji własnego języka. Od tego momentu po prostu bierzesz bramę i klasę fabryczną i zamieniasz je w szablony kodu, dziurawszy w nich dziury. Podajesz je do swojego generatora (który jest wywoływany przez parser twojego języka, który jest również w pełni generowany przez Xtext), a generator wypełni dziury w twoich szablonach. Wynik jest generowany kod. Od tego momentu możesz usunąć dowolne powtórzenie kodu w dowolnym miejscu (kod trwałości kodu GUI itp.).

Chris-Jan Twigt
źródło
Dziękuję za odpowiedź, poważnie wziąłem pod uwagę generowanie kodu, a nawet zaczynam wdrażać rozwiązanie. Są to 4 klasy kotłów, więc chyba mógłbym to zrobić w samym PHP. Chociaż nie rozwiązuje to problemu z powtarzającym się kodem, myślę, że kompromisy są tego warte - mając możliwość łatwej konserwacji i łatwej zmiany, nawet jeśli kod jest powtarzalny.
Emilio Rodrigues
To pierwszy raz słyszałem o XText i wygląda bardzo potężnie. Dziękuję za poinformowanie mnie o tym!
Matthew James Briggs,
8

Problem, przed którym stoisz, jest stary: kod trwałych obiektów często wygląda podobnie dla każdej klasy, jest to po prostu kod podstawowy. Dlatego niektórzy sprytni ludzie wymyślili Object Relational Mappers - rozwiązują dokładnie ten problem. Zobacz poprzedni post SO, aby uzyskać listę ORM dla PHP.

Gdy istniejące ORM nie spełniają twoich potrzeb, istnieje również alternatywa: możesz napisać własny generator kodu, który pobiera meta opis twoich obiektów w celu utrwalenia i generuje z tego powtarzającą się część kodu. W rzeczywistości nie jest to zbyt trudne, robiłem to w przeszłości dla różnych języków programowania, jestem pewien, że będzie można zaimplementować takie rzeczy również w PHP.

Doktor Brown
źródło
Stworzyłem taką funkcjonalność, ale przestawiłem się z niej na tę, ponieważ kiedyś obiekt danych obsługiwał zadania utrwalania danych, które nie są zgodne z SRP. Na przykład kiedyś miałem Model::getByPKmetodę i w powyższym przykładzie byłbym w stanie to zrobić, Comment::getByPKale pobieranie danych z bazy danych i konstruowanie obiektu było zawarte w klasie obiektów danych, co jest problemem, który próbuję rozwiązać za pomocą wzorców projektowych .
Emilio Rodrigues,
ORM nie muszą umieszczać logiki trwałości w obiekcie modelu. Jest to wzorzec Active Record i chociaż popularne są alternatywy. Sprawdź, jakie ORM są dostępne, i powinieneś znaleźć taki, który nie ma tego problemu.
Jules
@Jules, co jest bardzo dobrym punktem, skłoniło mnie do myślenia i zastanawiałem się - jaki byłby problem posiadania zarówno implementacji ActiveRecord, jak i Data Mappera w mojej aplikacji. Wtedy mógłbym użyć każdego z nich, kiedy ich potrzebuję - to rozwiąże mój problem przepisywania tego samego kodu za pomocą wzorca ActiveRecord, a wtedy, gdy potrzebuję mapera danych, tworzenie potrzebnych klas nie byłoby tak uciążliwe dla pracy?
Emilio Rodrigues
1
Jedynym problemem, jaki widzę w tej chwili, jest to, że opracowywanie skrajnych przypadków, w których zapytanie musi połączyć dwie tabele, w których jedna korzysta z Active Record, a druga jest zarządzana przez program Data Mapper - dodałoby warstwę złożoności, która w innym przypadku nie byłaby powstają. Osobiście używałbym tylko mapera - od początku nie lubiłem Active Record - ale wiem, że to tylko moja opinia, a inni się nie zgadzają.
Jules