DRY-Prinzip in guten Praktiken?

11

Ich versuche, das DRY-Prinzip in meiner Programmierung so genau wie möglich zu befolgen. Vor kurzem habe ich Designmuster in OOP gelernt und mich am Ende ziemlich oft wiederholt.

Ich habe ein Repository-Muster zusammen mit einem Factory- und einem Gateway-Muster erstellt, um meine Persistenz zu gewährleisten. Ich verwende eine Datenbank in meiner Anwendung, aber das sollte keine Rolle spielen, da ich das Gateway austauschen und auf Wunsch zu einer anderen Art von Persistenz wechseln kann.

Das Problem, das ich letztendlich für mich selbst erstellt habe, ist, dass ich dieselben Objekte für die Anzahl der Tabellen erstelle, die ich habe. Zum Beispiel sind dies die Objekte, die ich brauche, um eine Tabelle zu behandeln 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);
    }
}

Dann sieht mein Controller aus wie

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));
    }
}

Also dachte ich, ich würde Designmuster richtig verwenden und bewährte Methoden beibehalten, aber das Problem bei dieser Sache ist, dass ich beim Hinzufügen einer neuen Tabelle dieselben Klassen nur mit anderen Namen erstellen muss. Dies lässt den Verdacht aufkommen, dass ich etwas falsch mache.

Ich dachte an eine Lösung, bei der ich anstelle von Schnittstellen abstrakte Klassen hatte, die unter Verwendung des Klassennamens die Tabelle ermitteln, die sie bearbeiten müssen, aber das scheint nicht das Richtige zu sein. Was ist, wenn ich mich entscheide, zu einem Dateispeicher zu wechseln oder Memcache, in dem keine Tabellen vorhanden sind.

Nähere ich mich dem richtig oder gibt es eine andere Perspektive, die ich betrachten sollte?

Emilio Rodrigues
quelle
Verwenden Sie beim Erstellen einer neuen Tabelle immer denselben Satz von SQL-Abfragen (oder einen äußerst ähnlichen Satz), um mit dieser zu interagieren? Verkapselt die Factory auch eine sinnvolle Logik im realen Programm?
Ixrec
@Ixrec In der Regel gibt es im Gateway und im Repository benutzerdefinierte Methoden, die komplexere SQL-Abfragen wie Joins ausführen. Das Problem besteht darin, dass die von der Schnittstelle definierten Funktionen zum Speichern, Abrufen und Löschen bis auf den Tabellennamen und möglicherweise immer gleich sind aber unwahrscheinlich die Primärschlüsselspalte, so muss ich diese in jeder Implementierung wiederholen. Die Fabrik hält sehr selten jede Logik und manchmal ich es überhaupt überspringen und haben das Gateway das Objekt anstelle der Daten zurückgeben, aber ich erstellt eine Fabrik für dieses Beispiel , da es sein sollte , die richtige Gestaltung?
Emilio Rodrigues
Ich bin wahrscheinlich nicht qualifiziert, eine richtige Antwort zu geben, aber ich habe den Eindruck, dass 1) die Factory- und Repository-Klassen nichts wirklich Nützliches tun, also sollten Sie sie besser fallen lassen und nur mit Comment und CommentGateway direkt arbeiten 2) Es sollte möglich sein, die allgemeinen Funktionen zum Speichern / Abrufen / Löschen an einem einzigen Ort zu platzieren, anstatt sie zu kopieren, möglicherweise in einer abstrakten Klasse von "Standardimplementierungen" (ähnlich wie in den Sammlungen in Java)
Ixrec

Antworten:

12

Das Problem, das Sie ansprechen, ist ziemlich grundlegend.

Ich habe das gleiche Problem festgestellt, als ich für ein Unternehmen gearbeitet habe, das eine große J2EE-Anwendung erstellt hat, die aus mehreren hundert Webseiten und über eineinhalb Millionen Zeilen Java-Code bestand. Dieser Code verwendete ORM (JPA) für die Persistenz.

Dieses Problem wird noch schlimmer, wenn Sie Technologien von Drittanbietern in jeder Ebene der Architektur verwenden und alle Technologien eine eigene Datendarstellung erfordern.

Ihr Problem kann auf der Ebene der von Ihnen verwendeten Programmiersprache nicht gelöst werden. Die Verwendung von Mustern ist gut, aber wie Sie sehen, führt dies zu einer Wiederholung des Codes (genauer gesagt: Wiederholung von Designs).

Aus meiner Sicht gibt es nur 3 mögliche Lösungen. In der Praxis laufen diese Lösungen auf dasselbe hinaus.

Lösung 1: Verwenden Sie ein anderes Persistenz-Framework, mit dem Sie nur angeben können, was beibehalten werden muss. Es gibt wahrscheinlich einen solchen Rahmen. Das Problem bei diesem Ansatz ist, dass er eher naiv ist, da nicht alle Ihre Muster persistenzbezogen sind. Sie möchten auch Muster für Benutzeroberflächencode verwenden, sodass Sie dann ein GUI-Framework benötigen, das die Datendarstellungen des von Ihnen ausgewählten Persistenz-Frameworks wiederverwenden kann. Wenn Sie sie nicht wiederverwenden können, müssen Sie Kesselplattencode schreiben, um die Datendarstellungen des GUI-Frameworks und des Persistenz-Frameworks zu überbrücken. Dies widerspricht erneut dem DRY-Prinzip.

Lösung 2: Verwenden Sie eine andere - leistungsstärkere - Programmiersprache mit Konstrukten, mit denen Sie das sich wiederholende Design ausdrücken können, damit Sie den Designcode wiederverwenden können. Dies ist wahrscheinlich keine Option für Sie, aber nehmen Sie an, dass dies für einen Moment der Fall ist. Wenn Sie jedoch eine Benutzeroberfläche über der Persistenzschicht erstellen, möchten Sie, dass die Sprache wieder leistungsfähig genug ist, um die Erstellung der GUI zu unterstützen, ohne dass Kesselplattencode geschrieben werden muss. Es ist unwahrscheinlich, dass es eine Sprache gibt, die leistungsfähig genug ist, um das zu tun, was Sie wollen, da die meisten Sprachen für die Erstellung von GUI auf Frameworks von Drittanbietern angewiesen sind, für deren Funktion jeweils eine eigene Datendarstellung erforderlich ist.

Lösung 3: Automatisieren Sie die Wiederholung von Code und Design mithilfe einer Codegenerierung. Ihre Sorge ist, dass Sie Wiederholungen von Mustern und Designs von Hand codieren müssen, da das Codieren von repetitivem Code / Design von Hand gegen das DRY-Prinzip verstößt. Heutzutage gibt es sehr leistungsfähige Codegenerator-Frameworks. Es gibt sogar "Sprachworkbenches", mit denen Sie schnell (einen halben Tag, wenn Sie keine Erfahrung haben) Ihre eigene Programmiersprache erstellen und mit dieser Sprache beliebigen Code (PHP / Java / SQL - jede denkbare Textdatei) generieren können. Ich habe Erfahrung mit XText, aber MetaEdit und MPS scheinen auch in Ordnung zu sein. Ich rate Ihnen dringend, sich eine dieser Sprachwerkbänke anzusehen. Für mich war es die befreiendste Erfahrung in meinem Berufsleben.

Mit Xtext können Sie Ihren Computer den sich wiederholenden Code generieren lassen. Xtext generiert sogar einen Syntax-Hervorhebungseditor für Sie mit Code-Vervollständigung für Ihre eigene Sprachspezifikation. Von diesem Punkt an nehmen Sie einfach Ihr Gateway und Ihre Factory-Klasse und verwandeln sie in Codevorlagen, indem Sie Löcher in sie stanzen. Sie geben sie an Ihren Generator weiter (der von einem Parser Ihrer Sprache aufgerufen wird, der ebenfalls vollständig von Xtext generiert wird), und der Generator füllt die Lücken in Ihren Vorlagen. Das Ergebnis ist generierter Code. Von diesem Punkt an können Sie jede Wiederholung von Code überall herausnehmen (GUI-Code-Persistenzcode usw.).

Chris-Jan Twigt
quelle
Vielen Dank für die Antwort. Ich habe die Codegenerierung ernsthaft in Betracht gezogen und beginne sogar mit der Implementierung einer Lösung. Es sind 4 Boilerplate-Klassen, also könnte ich das wohl in PHP selbst machen. Dies löst zwar nicht das Problem des sich wiederholenden Codes, aber ich denke, dass sich die Kompromisse lohnen - mit hoch wartbarem und leicht änderbarem Code, obwohl sich der Code wiederholt.
Emilio Rodrigues
Dies ist das erste Mal, dass ich von XText gehört habe und es sieht sehr mächtig aus. Danke, dass Sie mich darauf aufmerksam gemacht haben!
Matthew James Briggs
8

Das Problem, mit dem Sie konfrontiert sind, ist ein altes: Code für persistente Objekte sieht für jede Klasse oft ähnlich aus, es ist einfach Boilerplate-Code. Deshalb haben einige kluge Leute Object Relational Mappers erfunden - sie lösen genau dieses Problem. In diesem früheren SO-Beitrag finden Sie eine Liste der ORMs für PHP.

Wenn vorhandene ORMs nicht Ihren Anforderungen entsprechen, gibt es auch eine Alternative: Sie können Ihren eigenen Codegenerator schreiben, der eine Meta-Beschreibung Ihrer Objekte verwendet, um zu bestehen, und daraus den sich wiederholenden Teil des Codes generiert. Das ist eigentlich nicht allzu schwer, ich habe dies in der Vergangenheit für einige verschiedene Programmiersprachen gemacht, ich bin sicher, dass es auch möglich sein wird, solche Dinge auch in PHP zu implementieren.

Doc Brown
quelle
Ich habe eine solche Funktionalität erstellt, aber ich habe darauf umgestellt, weil das Datenobjekt früher Datenpersistenzaufgaben abwickelte, die nicht mit SRP übereinstimmen. Zum Beispiel hatte ich eine Model::getByPKMethode und im obigen Beispiel wäre ich dazu in der Lage, Comment::getByPKaber das Abrufen der Daten aus der Datenbank und das Erstellen des Objekts sind alle in der Datenobjektklasse enthalten. Dies ist das Problem, das ich mithilfe von Entwurfsmustern zu lösen versuche .
Emilio Rodrigues
ORMs müssen die Persistenzlogik nicht im Modellobjekt platzieren. Dies ist das Active Record-Muster, und obwohl es beliebt ist, gibt es Alternativen. Schauen Sie sich an, welche ORMs verfügbar sind, und Sie sollten eine finden, bei der dieses Problem nicht auftritt.
Jules
@Jules, das ist ein sehr guter Punkt, es hat mich zum Nachdenken gebracht und ich habe mich gefragt, was das Problem wäre, wenn sowohl eine ActiveRecord- als auch eine Data Mapper-Implementierung in meiner Anwendung verfügbar wären. Dann könnte ich jeden von diesen verwenden, wenn ich sie brauche - dies löst mein Problem, denselben Code mithilfe des ActiveRecord-Musters neu zu schreiben , und wenn ich dann tatsächlich einen Daten-Mapper benötige , wäre es nicht so schwierig , die erforderlichen Klassen zu erstellen für die Arbeit?
Emilio Rodrigues
1
Das einzige Problem, das ich derzeit dabei sehen kann, besteht darin, die Randfälle zu ermitteln, in denen eine Abfrage zwei Tabellen verknüpfen muss, in denen eine Active Record verwendet und die andere von Ihrem Data Mapper verwaltet wird. Dies würde eine Komplexitätsebene hinzufügen, die dies sonst nicht tun würde nicht entstehen. Persönlich würde ich nur den Mapper verwenden - ich habe Active Record von Anfang an nie gemocht - aber ich weiß, dass dies nur meine Meinung ist und andere anderer Meinung sind.
Jules