Princípio DRY em boas práticas?

11

Estou tentando seguir o princípio DRY na minha programação o máximo que posso. Recentemente, tenho aprendido padrões de design no OOP e acabei me repetindo bastante.

Eu criei um padrão de repositório junto com os padrões de fábrica e gateway para lidar com minha persistência. Estou usando um banco de dados no meu aplicativo, mas isso não deve importar, pois eu posso trocar o Gateway e alternar para outro tipo de persistência, se assim o desejar.

O problema que acabei criando para mim mesmo é que eu crio os mesmos objetos para o número de tabelas que tenho. Por exemplo, esses serão os objetos que eu preciso para manipular uma tabela 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);
    }
}

Então meu controlador parece

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

Por isso, pensei que estava usando padrões de design corretamente e mantendo boas práticas, mas o problema é que, quando adiciono uma nova tabela, tenho que criar as mesmas classes apenas com outros nomes. Isso levanta suspeitas em mim de que posso estar fazendo algo errado.

Pensei em uma solução em que, em vez de interfaces, tinha classes abstratas que, usando o nome da classe, descobrem a tabela que precisam manipular, mas isso não parece a coisa certa a se fazer, e se eu decidir mudar para um armazenamento de arquivo ou memcache onde não há tabelas.

Estou abordando isso corretamente ou há uma perspectiva diferente que eu deveria estar olhando?

Emilio Rodrigues
fonte
Ao criar uma nova tabela, você sempre usa o mesmo conjunto de consultas SQL (ou um conjunto extremamente semelhante) para interagir com ela? Além disso, o Factory está encapsulando alguma lógica significativa no programa real?
Ixrec 22/08/2015
@Ixrec normalmente haverá métodos personalizados no gateway e no repositório que executam consultas sql mais complexas, como junções, o problema é que as funções de persistência, recuperação e exclusão - definidas pela interface - são sempre as mesmas, exceto o nome da tabela e possivelmente mas improvável a coluna da chave primária, por isso tenho que repeti-las em todas as implementações. A fábrica raramente mantém uma lógica e, às vezes, eu a pulo e o gateway retorna o objeto em vez dos dados, mas criei uma fábrica para este exemplo, pois ele deveria ser o design apropriado?
Emilio Rodrigues
Provavelmente, não estou qualificado para dar uma resposta adequada, mas tenho a impressão de que 1) as classes Factory e Repository não estão realmente fazendo nada útil, portanto é melhor abandoná-las e trabalhar apenas com Comment and CommentGateway diretamente 2) Deveria ser possível colocar as funções persistentes / recuperar / excluir comuns em um único local, em vez de copiá-las, talvez em uma classe abstrata de "implementações padrão" (como o que as coleções em Java fazem)
Ixrec

Respostas:

12

O problema que você está abordando é bastante fundamental.

Eu experimentei o mesmo problema quando trabalhei para uma empresa que criou um grande aplicativo J2EE que consistia em várias centenas de páginas da web e mais de um milhão e meio de linhas de código Java. Este código usava ORM (JPA) para persistência.

Esse problema piora quando você usa tecnologias de terceiros em todas as camadas da arquitetura e todas as tecnologias exigem sua própria representação de dados.

Seu problema não pode ser resolvido no nível da linguagem de programação que você está usando. O uso de padrões é bom, mas, como você vê, causa repetição de código (ou seja, com mais precisão: repetição de projetos).

Na minha opinião, existem apenas 3 soluções possíveis. Na prática, essas soluções se resumem à mesma.

Solução 1: use alguma outra estrutura de persistência que permita indicar apenas o que deve ser persistido. Provavelmente existe uma estrutura assim. O problema dessa abordagem é que ela é bastante ingênua, porque nem todos os seus padrões serão relacionados à persistência. Você também desejará usar padrões para o código da interface com o usuário, de modo que precisará de uma estrutura da GUI que possa reutilizar as representações de dados da estrutura de persistência escolhida. Se você não puder reutilizá-los, precisará escrever o código da placa da caldeira para conectar as representações de dados da estrutura da GUI e da estrutura de persistência. E isso é contrário ao princípio DRY novamente.

Solução 2: use outra linguagem de programação - mais poderosa - que possua construções que permitam expressar o design repetitivo para que você possa reutilizar o código de design. Provavelmente, essa não é uma opção para você, mas suponha que seja por um momento. Então, novamente, quando você começar a criar uma interface de usuário sobre a camada de persistência, desejará que o idioma seja poderoso o suficiente para suportar a criação da GUI sem precisar escrever o código da placa da caldeira. É improvável que exista uma linguagem suficientemente poderosa para fazer o que você deseja, uma vez que a maioria das linguagens depende de estruturas de terceiros para criação de GUI, cada uma exigindo que sua própria representação de dados funcione.

Solução 3: automatize a repetição de código e o design usando alguma forma de geração de código. Sua preocupação é ter que codificar manualmente repetições de padrões e designs, já que o código / design repetitivo de código manual viola o princípio DRY. Atualmente, existem estruturas de gerador de código muito poderosas por aí. Existem até "bancadas de trabalho de linguagem" que permitem criar (sua própria linguagem de programação) rapidamente (meio dia quando você não tem experiência) e gerar qualquer código (PHP / Java / SQL - qualquer arquivo de texto possível) usando essa linguagem. Tenho experiência com o XText, mas o MetaEdit e o MPS parecem estar bem também. Eu recomendo fortemente que você verifique uma dessas bancadas de idiomas. Para mim, foi a experiência mais libertadora da minha vida profissional.

Usando o Xtext, sua máquina pode gerar o código repetitivo. O Xtext gera até um editor de destaque de sintaxe para você, com o preenchimento de código para sua própria especificação de idioma. A partir desse ponto, você simplesmente pega seu gateway e a classe de fábrica e os transforma em modelos de código, perfurando-os. Você os alimenta ao seu gerador (que é chamado por um analisador de seu idioma que também é completamente gerado pelo Xtext) e o gerador preencherá os buracos nos seus modelos. O resultado é um código gerado. A partir desse ponto, você pode realizar qualquer repetição de código em qualquer lugar (código de persistência do código da GUI, etc.).

Chris-Jan Twigt
fonte
Obrigado pela resposta, considerei seriamente a geração de código e estou começando a implementar uma solução. São 4 classes padrão, então acho que eu poderia fazer isso no próprio PHP. Embora isso não resolva a questão do código repetido, acho que as compensações valem a pena - sendo altamente mantenível e facilmente alterável, apesar de código repetitivo.
Emilio Rodrigues
Esta é a primeira vez que ouvi falar do XText e parece muito poderoso. Obrigado, me informando disso!
Matthew James Briggs
8

O problema que você enfrenta é antigo: o código para objetos persistentes geralmente se parece com cada classe, é simplesmente um código padrão. É por isso que algumas pessoas inteligentes inventaram os mapeadores relacionais de objetos - eles resolvem exatamente esse problema. Veja esta antiga postagem do SO para obter uma lista dos ORMs para PHP.

Quando os ORMs existentes não atendem às suas necessidades, há também uma alternativa: você pode escrever seu próprio gerador de código, que requer uma meta descrição de seus objetos para persistir e gera a parte repetida do código a partir disso. Na verdade, isso não é muito difícil, eu fiz isso no passado para algumas linguagens de programação diferentes, tenho certeza que também será possível implementar essas coisas também em PHP.

Doc Brown
fonte
Criei essa funcionalidade, mas mudei para isso porque costumava fazer com que o objeto de dados manipulasse tarefas de persistência de dados que não estão em conformidade com o SRP. Por exemplo, eu costumava ter Model::getByPKmétodo e, no exemplo acima, eu poderia fazer, Comment::getByPKmas obter os dados do banco de dados e construir o objeto está todo contido na classe de objeto de dados, que é o problema que estou tentando resolver usando padrões de design .
Emilio Rodrigues
Os ORMs não precisam colocar a lógica de persistência no objeto de modelo. Esse é o padrão do Active Record e, embora popular, existem alternativas. Veja quais ORMs estão disponíveis e você deve encontrar uma que não tenha esse problema.
Jules
@Jules, esse é um ponto muito bom, me fez pensar e fiquei pensando - qual seria o problema de ter implementações ActiveRecord e Data Mapper disponíveis no meu aplicativo. Então, eu poderia usar cada um deles quando precisar deles - isso resolverá meu problema de reescrever o mesmo código usando o padrão ActiveRecord e, quando eu realmente precisar de um mapeador de dados, não seria difícil criar as classes necessárias para o trabalho?
Emilio Rodrigues
1
O único problema que posso ver com isso agora é que resolver os casos extremos quando uma consulta precisa juntar duas tabelas em que uma usa o Active Record e a outra é gerenciada pelo seu Data Mapper - isso adicionaria uma camada de complexidade que de outra forma não seria necessária. surja. Pessoalmente, eu usaria o mapeador - nunca gostei do Active Record desde o início -, mas sei que é apenas minha opinião e outros discordam.
Jules