Principio SECCO nelle buone pratiche?


11

Sto cercando di seguire il principio DRY nella mia programmazione il più forte possibile. Recentemente ho imparato modelli di progettazione in OOP e ho finito per ripetermi un bel po '.

Ho creato un modello di repository insieme a modelli Factory e Gateway per gestire la mia persistenza. Sto usando un database nella mia applicazione, ma non dovrebbe importare dal momento che dovrei essere in grado di scambiare il Gateway e passare a un altro tipo di persistenza, se lo desidero.

Il problema che ho finito per creare da solo è che creo gli stessi oggetti per il numero di tabelle che ho. Ad esempio questi saranno gli oggetti di cui ho bisogno per gestire una tabella 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);
    }
}

Quindi il mio controller sembra

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

Quindi ho pensato di utilizzare correttamente i modelli di progettazione e di mantenere le buone pratiche, ma il problema è che quando aggiungo una nuova tabella, devo creare le stesse classi solo con altri nomi. Ciò solleva il sospetto in me che io possa fare qualcosa di sbagliato.

Ho pensato a una soluzione in cui invece di interfacce avevo classi astratte che usando il nome della classe capiscono la tabella che devono manipolare ma che non sembra la cosa giusta da fare, e se decidessi di passare a un archivio di file o memcache dove non ci sono tabelle.

Mi sto avvicinando correttamente a questo, o c'è una prospettiva diversa che dovrei guardare?


Quando crei una nuova tabella, usi sempre lo stesso set di query SQL (o un set estremamente simile) per interagire con esso? Inoltre, la Fabbrica incapsula qualche logica significativa nel programma reale?
Ixrec,

@Ixrec di solito ci saranno metodi personalizzati nel gateway e nel repository che eseguono query sql più complesse come i join, il problema è che le funzioni persist, recuperare ed eliminare - definite dall'interfaccia - sono sempre le stesse tranne il nome della tabella e possibilmente ma è improbabile che la colonna chiave primaria, quindi devo ripetere quelli in ogni implementazione. La fabbrica mantiene molto raramente qualsiasi logica e, a volte, la salto del tutto e il gateway restituisce l'oggetto invece dei dati, ma ho creato una fabbrica per questo esempio, dal momento che dovrebbe essere la progettazione corretta?
Emilio Rodrigues,

Probabilmente non sono qualificato per dare una risposta corretta, ma ho l'impressione che 1) le classi Factory e Repository non stiano davvero facendo nulla di utile, quindi faresti meglio ad abbandonarli e lavorare direttamente con Comment e CommentGateway 2) Dovrebbe essere possibile mettere le funzioni persistenti / recuperare / eliminare comuni in un unico posto anziché copiarle, magari in una classe astratta di "implementazioni predefinite" (un po 'come fanno le Collezioni in Java)
Ixrec

Risposte:


12

Il problema che stai affrontando è abbastanza fondamentale.

Ho riscontrato lo stesso problema quando ho lavorato per un'azienda che ha realizzato una grande applicazione J2EE composta da diverse centinaia di pagine Web e oltre un milione e mezzo di righe di codice Java. Questo codice utilizzava ORM (JPA) per la persistenza.

Questo problema peggiora quando si utilizzano tecnologie di terze parti in ogni livello dell'architettura e tutte le tecnologie richiedono la propria rappresentazione dei dati.

Il problema non può essere risolto a livello del linguaggio di programmazione in uso. L'uso dei modelli è buono ma come vedi causa la ripetizione del codice (o più precisamente: ripetizione dei disegni).

Per come la vedo io ci sono solo 3 possibili soluzioni. In pratica, queste soluzioni sono le stesse.

Soluzione 1: utilizzare un altro framework di persistenza che consente di indicare solo ciò che deve essere persistito. C'è probabilmente un tale quadro in giro. Il problema con questo approccio è che è piuttosto ingenuo perché non tutti i modelli saranno correlati alla persistenza. Volete anche usare i pattern per il codice dell'interfaccia utente, quindi avreste bisogno di un framework GUI che possa riutilizzare le rappresentazioni dei dati del framework di persistenza che scegliete. Se non riesci a riutilizzarli, dovrai scrivere il codice della piastra della caldaia per collegare le rappresentazioni dei dati del framework GUI e del framework di persistenza .. e questo è di nuovo contrario al principio DRY.

Soluzione 2: utilizzare un altro linguaggio di programmazione più potente con costrutti che consentano di esprimere il design ripetitivo in modo da poter riutilizzare il codice di progettazione. Questa probabilmente non è un'opzione per te, ma supponiamo che lo sia per un momento. Poi di nuovo quando inizi a creare un'interfaccia utente sopra il livello di persistenza, vorrai che la lingua sia di nuovo abbastanza potente da supportare la creazione della GUI senza dover scrivere il codice della piastra della caldaia. È improbabile che esista una lingua abbastanza potente per fare ciò che desideri poiché la maggior parte delle lingue si affida a framework di terze parti per la costruzione della GUI, ciascuno dei quali richiede la propria rappresentazione dei dati per funzionare.

Soluzione 3: automatizzare la ripetizione del codice e la progettazione utilizzando una qualche forma di generazione del codice. La tua preoccupazione è di dover codificare a mano ripetizioni di modelli e disegni poiché codificare a mano codice / design ripetitivi viola il principio DRY. Oggi ci sono framework di generatori di codice molto potenti. Esistono anche "banchi di lavoro linguistici" che ti consentono di creare rapidamente (mezza giornata quando non hai esperienza) il tuo linguaggio di programmazione e generare qualsiasi codice (PHP / Java / SQL - qualsiasi file di testo pensabile) usando quel linguaggio. Ho esperienza con XText ma anche MetaEdit e MPS sembrano andare bene. Vi consiglio caldamente di controllare uno di questi banchi di lavoro linguistici. Per me è stata l'esperienza più liberatrice della mia vita professionale.

Usando Xtext puoi fare in modo che la tua macchina generi il codice ripetitivo. Xtext genera anche un editor di evidenziazione della sintassi per te con il completamento del codice per la tua specifica lingua. Da quel momento prendi semplicemente il tuo gateway e la tua classe di fabbrica e li trasformi in modelli di codice praticando dei buchi. Li dai al tuo generatore (che viene chiamato da un parser della tua lingua che è anche completamente generato da Xtext) e il generatore riempirà i buchi nei tuoi modelli. Il risultato è un codice generato. Da quel momento in poi puoi eliminare qualsiasi ripetizione di codice ovunque (codice di persistenza del codice GUI ecc.).


Grazie per la risposta, ho preso in seria considerazione la generazione del codice e sto persino iniziando a implementare una soluzione. Sono 4 classi di piatti, quindi immagino di poterlo fare in PHP stesso. Anche se questo non risolve il problema del codice ripetuto, penso che ne valgano la pena, avendo un alto livello di manutenzione e facilmente modificabile anche se il codice ripetitivo.
Emilio Rodrigues,

Questo è il primo che ho sentito parlare di XText e sembra molto potente. Grazie che me ne rendi conto!
Matthew James Briggs,

8

Il problema che affronti è vecchio: il codice per gli oggetti persistenti sembra spesso simile per ogni classe, è semplicemente un codice di tipo boilerplate. Ecco perché alcune persone intelligenti hanno inventato gli Object Relational Mapper : risolvono esattamente questo problema. Vedi questo ex post SO per un elenco di ORM per PHP.

Quando gli ORM esistenti non subiscono le tue esigenze, esiste anche un'alternativa: puoi scrivere il tuo generatore di codice, che prende una meta descrizione dei tuoi oggetti per persistere e genera da ciò la parte ripetitiva del codice. Questo non è in realtà troppo difficile, l'ho fatto in passato per alcuni linguaggi di programmazione diversi, sono sicuro che sarà anche possibile implementare tali cose anche in PHP.


Ho creato tale funzionalità ma sono passato da questa a questa perché in passato l'oggetto dati gestiva le attività di persistenza dei dati che non sono conformi a SRP. Ad esempio, avevo un Model::getByPKmetodo e nell'esempio precedente sarei in grado di farlo, Comment::getByPKma ottenere i dati dal database e costruire l'oggetto è tutto contenuto nella classe dell'oggetto dati, che è il problema che sto cercando di risolvere usando i modelli di progettazione .
Emilio Rodrigues,

Gli ORM non devono posizionare la logica di persistenza nell'oggetto modello. Questo è il modello Active Record e, sebbene popolare, ci sono alternative. Dai un'occhiata a quali ORM sono disponibili e dovresti trovarne uno che non presenta questo problema.
Jules il

@Jules questo è un ottimo punto, mi ha fatto pensare e mi chiedevo: quale sarebbe il problema di avere entrambe le implementazioni ActiveRecord e Data Mapper disponibili nella mia applicazione. Quindi potrei usare ognuno di quelli quando ne ho bisogno - questo risolverà il mio problema di riscrivere lo stesso codice usando il modello ActiveRecord e quindi quando ho effettivamente bisogno di un mappatore di dati non sarebbe così difficile creare le classi necessarie per il lavoro?
Emilio Rodrigues,

1
L'unico problema che posso vedere con questo in questo momento è che elaborare i casi limite quando una query deve unire due tabelle in cui una utilizza Record attivo e l'altra è gestita dal tuo Mappatore dati - aggiungerebbe un livello di complessità che altrimenti non non sorgere. Personalmente, userei semplicemente il mapper - non mi è mai piaciuto Active Record sin dall'inizio - ma so che è solo la mia opinione, e altri non sono d'accordo.
Jules,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.