Principe SEC dans les bonnes pratiques?

11

J'essaie de suivre le principe DRY dans ma programmation aussi fort que possible. Récemment, j'ai appris des modèles de conception en POO et j'ai fini par me répéter beaucoup.

J'ai créé un modèle de référentiel avec des modèles d'usine et de passerelle pour gérer ma persistance. J'utilise une base de données dans mon application, mais cela ne devrait pas avoir d'importance car je devrais pouvoir échanger la passerelle et passer à un autre type de persistance si je le souhaitais.

Le problème que j'ai fini par me créer est que je crée les mêmes objets pour le nombre de tables que j'ai. Par exemple, ce seront les objets dont j'ai besoin pour gérer une table 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);
    }
}

Ensuite, mon contrôleur ressemble

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

J'ai donc pensé utiliser correctement les modèles de conception et conserver les bonnes pratiques, mais le problème avec cette chose est que lorsque j'ajoute une nouvelle table, je dois créer les mêmes classes uniquement avec d'autres noms. Cela me fait soupçonner que je fais peut-être quelque chose de mal.

J'ai pensé à une solution où, au lieu d'interfaces, j'avais des classes abstraites qui, à l'aide du nom de classe, définissaient le tableau qu'elles devaient manipuler, mais cela ne semblait pas être la bonne chose à faire, si je décidais de passer à un stockage de fichiers ou memcache où il n'y a pas de tables.

Suis-je en train de l'aborder correctement ou y a-t-il une perspective différente que je devrais envisager?

Emilio Rodrigues
la source
Lorsque vous créez une nouvelle table, utilisez-vous toujours le même ensemble de requêtes SQL (ou un ensemble extrêmement similaire) pour interagir avec elle? En outre, l'usine encapsule-t-elle une logique significative dans le programme réel?
Ixrec
@Ixrec, il y aura généralement des méthodes personnalisées dans la passerelle et le référentiel qui effectueront des requêtes SQL plus complexes comme les jointures, le problème est que les fonctions de persistance, de récupération et de suppression - définies par l'interface - sont toujours les mêmes, sauf pour le nom de la table et éventuellement mais il est peu probable que la colonne de clé primaire, donc je dois répéter ceux dans chaque implémentation. L'usine contient très rarement une logique et parfois je la saute du tout et je demande à la passerelle de renvoyer l'objet au lieu des données, mais j'ai créé une usine pour cet exemple car elle devrait être la bonne conception?
Emilio Rodrigues
Je ne suis probablement pas qualifié pour donner une réponse correcte, mais j'ai l'impression que 1) les classes Factory et Repository ne font vraiment rien d'utile, alors vous feriez mieux de les abandonner et de travailler directement avec Comment et CommentGateway 2) Il devrait être possible de mettre les fonctions communes persist / retrieve / delete en un seul endroit plutôt que de les copier-coller, peut-être dans une classe abstraite d '"implémentations par défaut" (un peu comme ce que font les collections en Java)
Ixrec

Réponses:

12

Le problème que vous abordez est assez fondamental.

J'ai rencontré le même problème lorsque j'ai travaillé pour une entreprise qui créait une grande application J2EE composée de plusieurs centaines de pages Web et de plus d'un million et demi de lignes de code Java. Ce code utilisait ORM (JPA) pour la persistance.

Ce problème s'aggrave lorsque vous utilisez des technologies tierces dans chaque couche de l'architecture et que les technologies nécessitent toutes leur propre représentation des données.

Votre problème ne peut pas être résolu au niveau du langage de programmation que vous utilisez. L'utilisation de modèles est bonne, mais comme vous le voyez, cela provoque la répétition du code (ou plus exactement: répétition des conceptions).

D'après moi, il n'y a que 3 solutions possibles. En pratique, ces solutions se résument au même.

Solution 1: utilisez un autre cadre de persistance qui vous permet d'indiquer uniquement ce qui doit être conservé. Il existe probablement un tel cadre. Le problème avec cette approche est qu'elle est plutôt naïve car tous les modèles ne seront pas liés à la persistance. Vous voudrez également utiliser des modèles pour le code d'interface utilisateur, vous aurez donc besoin d'un cadre d'interface graphique qui peut réutiliser les représentations de données du cadre de persistance que vous choisissez. Si vous ne pouvez pas les réutiliser, vous devrez écrire du code de plaque de chaudière pour relier les représentations de données du framework GUI et du framework de persistance .. et cela est à nouveau contraire au principe DRY.

Solution 2: utilisez un autre langage de programmation - plus puissant - qui a des constructions qui vous permettent d'exprimer la conception répétitive afin que vous puissiez réutiliser le code de conception. Ce n'est probablement pas une option pour vous, mais supposez-le un instant. Ensuite, lorsque vous commencez à créer une interface utilisateur au-dessus de la couche de persistance, vous souhaiterez à nouveau que le langage soit suffisamment puissant pour prendre en charge la création de l'interface graphique sans avoir à écrire le code de la plaque de la chaudière. Il est peu probable qu'il existe un langage suffisamment puissant pour faire ce que vous voulez, car la plupart des langues s'appuient sur des cadres tiers pour la construction de l'interface graphique qui nécessitent chacun leur propre représentation des données pour fonctionner.

Solution 3: automatisez la répétition du code et de la conception à l'aide d'une forme de génération de code. Votre souci est de devoir coder à la main les répétitions de motifs et de dessins car le code / dessin répétitif à codage manuel viole le principe DRY. De nos jours, il existe des frameworks de génération de code très puissants. Il existe même des «ateliers de langage» qui vous permettent de créer rapidement (une demi-journée sans expérience) votre propre langage de programmation et de générer n'importe quel code (PHP / Java / SQL - n'importe quel fichier texte imaginable) à l'aide de ce langage. J'ai de l'expérience avec XText mais MetaEdit et MPS semblent bien aussi. Je vous conseille fortement de vérifier l'un de ces ateliers linguistiques. Ce fut pour moi l'expérience la plus libératrice de ma vie professionnelle.

En utilisant Xtext, votre machine peut générer le code répétitif. Xtext génère même pour vous un éditeur de coloration syntaxique avec complétion de code pour votre propre spécification de langage. À partir de là, il vous suffit de prendre votre passerelle et votre classe d'usine et de les transformer en modèles de code en y perforant des trous. Vous les alimentez dans votre générateur (qui est appelé par un analyseur de votre langue qui est également complètement généré par Xtext) et le générateur remplira les trous dans vos modèles. Le résultat est un code généré. À partir de là, vous pouvez supprimer n'importe quelle répétition de code n'importe où (code de persistance de code GUI, etc.).

Chris-Jan Twigt
la source
Merci pour la réponse, j'ai sérieusement pris en compte la génération de code et je commence même à implémenter une solution. Ce sont 4 classes standard, donc je suppose que je pourrais le faire en PHP lui-même. Bien que cela ne résout pas le problème du code répétitif, je pense que les compromis en valent la peine - avoir un code hautement maintenable et facilement modifiable même si le code est répétitif.
Emilio Rodrigues
C'est la première fois que j'entends parler de XText et il a l'air très puissant. Merci de me le faire savoir!
Matthew James Briggs
8

Le problème auquel vous êtes confronté est ancien: le code des objets persistants ressemble souvent à chaque classe, il s'agit simplement de code passe-partout. C'est pourquoi certaines personnes intelligentes ont inventé les mappeurs relationnels d'objets - ils résolvent exactement ce problème. Voir cet ancien article SO pour une liste des ORM pour PHP.

Lorsque les ORM existants ne subissent pas vos besoins, il existe également une alternative: vous pouvez écrire votre propre générateur de code, qui prend une méta description de vos objets pour persister et génère la partie répétitive du code à partir de cela. Ce n'est en fait pas trop difficile, je l'ai fait dans le passé pour certains langages de programmation différents, je suis sûr qu'il sera également possible d'implémenter de telles choses également en PHP.

Doc Brown
la source
J'ai créé une telle fonctionnalité mais je suis passé de celle-ci à cela parce que j'avais l'habitude d'avoir l'objet de données gérer les tâches de persistance des données qui ne sont pas conformes à SRP. Par exemple, j'avais l'habitude d'avoir une Model::getByPKméthode et dans l'exemple ci-dessus, je serais capable de le faire, Comment::getByPKmais obtenir les données de la base de données et construire l'objet est tout contenu dans la classe d'objet de données, ce qui est le problème que j'essaie de résoudre en utilisant des modèles de conception .
Emilio Rodrigues
Les ORM n'ont pas à placer la logique de persistance dans l'objet modèle. Il s'agit du modèle d'enregistrement actif, et bien que populaire, il existe des alternatives. Jetez un œil aux ORM disponibles et vous devriez en trouver un qui ne présente pas ce problème.
Jules
@Jules, c'est un très bon point, cela m'a fait réfléchir et je me demandais - quel serait le problème d'avoir à la fois une implémentation ActiveRecord et un Data Mapper disponibles dans mon application. Ensuite, je pourrais utiliser chacun de ceux-ci lorsque j'en ai besoin - cela résoudra mon problème de réécriture du même code en utilisant le modèle ActiveRecord, puis lorsque j'ai réellement besoin d' un mappeur de données, il ne serait pas si difficile de créer les classes nécessaires Pour le boulot?
Emilio Rodrigues
1
Le seul problème que je peux voir avec cela en ce moment est que travailler sur les cas limites lorsqu'une requête doit joindre deux tables où l'une utilise Active Record et l'autre est gérée par votre Data Mapper - cela ajouterait une couche de complexité qui autrement ne serait pas ne te lève pas. Personnellement, je n'utiliserais que le mappeur - je n'ai jamais aimé Active Record depuis le début - mais je sais que ce n'est que mon avis, et d'autres ne sont pas d'accord.
Jules