Principio SECO en buenas prácticas?

11

Estoy tratando de seguir el principio DRY en mi programación tan duro como puedo. Recientemente he estado aprendiendo patrones de diseño en OOP y terminé repitiéndome bastante.

He creado un patrón de repositorio junto con los patrones de Factory y Gateway para manejar mi persistencia. Estoy usando una base de datos en mi aplicación, pero eso no debería importar, ya que debería poder cambiar la puerta de enlace y cambiar a otro tipo de persistencia si lo desea.

El problema que terminé creando para mí es que creo los mismos objetos para la cantidad de tablas que tengo. Por ejemplo, estos serán los objetos que necesito para manejar una tabla 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);
    }
}

Entonces mi controlador se ve como

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

Entonces pensé que estaba usando patrones de diseño correctamente y mantenía buenas prácticas, pero el problema con esto es que cuando agrego una nueva tabla, tengo que crear las mismas clases solo con otros nombres. Esto me hace sospechar que puedo estar haciendo algo mal.

Pensé en una solución donde, en lugar de interfaces, tenía clases abstractas que, usando el nombre de la clase, calculan la tabla que necesitan manipular, pero eso no parece ser lo correcto, ¿qué sucede si decido cambiar a un almacenamiento de archivos o memcache donde no hay tablas.

¿Estoy abordando esto correctamente, o hay una perspectiva diferente que debería mirar?

Emilio Rodrigues
fuente
Cuando crea una nueva tabla, ¿utiliza siempre el mismo conjunto de consultas SQL (o un conjunto extremadamente similar) para interactuar con ella? Además, ¿encapsula Factory alguna lógica significativa en el programa real?
Ixrec
@Ixrec comúnmente habrá métodos personalizados en la puerta de enlace y el repositorio que realizan consultas sql más complejas como las uniones, el problema es que las funciones de persistencia, recuperación y eliminación, definidas por la interfaz, son siempre las mismas, excepto por el nombre de la tabla y posiblemente pero es poco probable la columna de clave principal, por lo que tengo que repetirlas en cada implementación. La fábrica rara vez tiene alguna lógica y, a veces, la omito y hago que la puerta de enlace devuelva el objeto en lugar de los datos, pero creé una fábrica para este ejemplo, ya que debería ser el diseño adecuado.
Emilio Rodrigues
Probablemente no estoy calificado para dar una respuesta adecuada, pero tengo la impresión de que 1) las clases Factory y Repository realmente no están haciendo nada útil, por lo que sería mejor deshacerse de ellas y trabajar solo con Comment y CommentGateway directamente 2) Debería ser posible colocar las funciones comunes de persistencia / recuperación / eliminación en un solo lugar en lugar de copiarlas y pegarlas, tal vez en una clase abstracta de "implementaciones predeterminadas" (algo así como lo que hacen las Colecciones en Java)
Ixrec

Respuestas:

12

El problema que está abordando es bastante fundamental.

Experimenté el mismo problema cuando trabajé para una empresa que creó una gran aplicación J2EE que constaba de varios cientos de páginas web y más de un millón y medio de líneas de código Java. Este código usaba ORM (JPA) para persistencia.

Este problema empeora cuando usa tecnologías de terceros en cada capa de la arquitectura y todas las tecnologías requieren su propia representación de datos.

Su problema no puede resolverse al nivel del lenguaje de programación que está utilizando. El uso de patrones es bueno pero, como puede ver, causa la repetición del código (o dicho con mayor precisión: repetición de diseños).

A mi modo de ver, solo hay 3 posibles soluciones. En la práctica, estas soluciones se reducen a lo mismo.

Solución 1: utilice algún otro marco de persistencia que le permita establecer solo lo que debe persistir. Probablemente existe un marco de este tipo. El problema con este enfoque es que es bastante ingenuo porque no todos sus patrones estarán relacionados con la persistencia. También querrá usar patrones para el código de la interfaz de usuario, por lo que necesitará un marco de GUI que pueda reutilizar las representaciones de datos del marco de persistencia que elija. Si no puede reutilizarlos, deberá escribir un código de placa de caldera para unir las representaciones de datos del marco GUI y el marco de persistencia ... y esto es contrario al principio DRY nuevamente.

Solución 2: use otro lenguaje de programación más potente que tenga construcciones que le permitan expresar el diseño repetitivo para que pueda reutilizar el código de diseño. Probablemente esta no sea una opción para usted, pero suponga que por un momento lo es. Por otra parte, cuando comience a crear una interfaz de usuario en la parte superior de la capa de persistencia, querrá que el lenguaje vuelva a ser lo suficientemente potente como para admitir la creación de la GUI sin tener que escribir el código de la placa de caldera. Es poco probable que haya un lenguaje lo suficientemente potente como para hacer lo que desea, ya que la mayoría de los idiomas se basan en marcos de terceros para la construcción de GUI que requieren su propia representación de datos para funcionar.

Solución 3: automatice la repetición de código y diseño utilizando alguna forma de generación de código. Su preocupación es tener que codificar manualmente las repeticiones de patrones y diseños, ya que la codificación manual de códigos / diseños viola el principio DRY. Hoy en día existen marcos de generador de código muy potentes. Incluso hay "bancos de trabajo de lenguaje" que le permiten crear rápidamente (medio día cuando no tiene experiencia) su propio lenguaje de programación y generar cualquier código (PHP / Java / SQL - cualquier archivo de texto pensable) usando ese lenguaje. Tengo experiencia con XText, pero MetaEdit y MPS también parecen estar bien. Le recomiendo que consulte uno de estos bancos de trabajo de idiomas. Para mí fue la experiencia más liberadora en mi vida profesional.

Usando Xtext puede hacer que su máquina genere el código repetitivo. Xtext incluso genera un editor de resaltado de sintaxis para usted con finalización de código para su propia especificación de idioma. A partir de ese momento, simplemente tome su puerta de enlace y clase de fábrica y conviértalos en plantillas de código haciendo agujeros en ellos. Los alimenta a su generador (que es llamado por un analizador de su lenguaje que también es generado completamente por Xtext) y el generador completará los huecos en sus plantillas. El resultado es el código generado. A partir de ese momento, puede eliminar cualquier repetición de código en cualquier lugar (código de persistencia del código GUI, etc.).

Chris-Jan Twigt
fuente
Gracias por la respuesta, he tomado en serio la generación de código e incluso estoy empezando a implementar una solución. Son 4 clases repetitivas, así que supongo que podría hacerlo en el propio PHP. Si bien esto no resuelve el problema del código repetitivo, creo que las compensaciones valen la pena, ya que tienen un código altamente mantenible y fácil de cambiar a pesar de que el código repetitivo.
Emilio Rodrigues
Esta es la primera vez que oigo hablar de XText y se ve muy potente. ¡Gracias por hacerme consciente de esto!
Matthew James Briggs
8

El problema que enfrenta es antiguo: el código para objetos persistentes a menudo se ve similar para cada clase, es simplemente un código repetitivo. Es por eso que algunas personas inteligentes inventaron los Mapeadores Relacionales de Objetos : resuelven exactamente ese problema. Consulte esta publicación anterior de SO para obtener una lista de ORM para PHP.

Cuando los ORM existentes no satisfacen sus necesidades, también hay una alternativa: puede escribir su propio generador de código, que toma una meta descripción de sus objetos para persistir y genera la parte repetitiva del código a partir de eso. En realidad, eso no es demasiado difícil, hice esto en el pasado para algunos lenguajes de programación diferentes, estoy seguro de que también será posible implementar tales cosas también en PHP.

Doc Brown
fuente
He creado dicha funcionalidad pero la cambié a esta porque solía hacer que el objeto de datos manejara tareas de persistencia de datos que no cumplen con SRP. Por ejemplo, solía tener un Model::getByPKmétodo y en el ejemplo anterior podría hacerlo, Comment::getByPKpero obtener los datos de la base de datos y construir el objeto está contenido dentro de la clase de objeto de datos, que es el problema que estoy tratando de resolver usando patrones de diseño .
Emilio Rodrigues
Los ORM no tienen que colocar la lógica de persistencia en el objeto modelo. Este es el patrón de registro activo, y aunque es popular, existen alternativas. Eche un vistazo a qué ORM están disponibles y debería encontrar uno que no tenga este problema.
Jules
@Jules, ese es un muy buen punto, me hizo pensar y me preguntaba: ¿cuál sería el problema de tener implementaciones ActiveRecord y Data Mapper disponibles en mi aplicación? Entonces podría usar cada uno de ellos cuando los necesite; esto resolverá mi problema de reescribir el mismo código usando el patrón ActiveRecord y luego, cuando realmente necesite un mapeador de datos, no sería tan difícil crear las clases necesarias ¿para el trabajo?
Emilio Rodrigues
1
El único problema que puedo ver con esto en este momento es que resolver los casos extremos cuando una consulta necesita unir dos tablas donde una usa Active Record y la otra es administrada por su Data Mapper: agregaría una capa de complejidad que de otro modo no No te levantes. Personalmente, solo usaría el mapeador, nunca me ha gustado Active Record desde el principio, pero sé que esa es solo mi opinión, y otros no están de acuerdo.
Julio