Separar a maioria das classes no campo de dados somente classes e somente métodos (se possível) é um bom ou antipadrão?

10

Por exemplo, uma classe geralmente possui membros e métodos, por exemplo:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public void printInfo(){
        System.out.println("Name:"+this.name+",weight:"+this.weight);
    }

    public void draw(){
        //some draw code which uses this.image
    }
}

Porém, depois de ler sobre o princípio de responsabilidade única e o princípio aberto aberto, prefiro separar uma classe no DTO e a classe auxiliar apenas com métodos estáticos, por exemplo:

public class CatData{
    public String name;
    public int weight;
    public Image image;
}

public class CatMethods{
    public static void printInfo(Cat cat){
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat){
        //some draw code which uses cat.image
    }
}

Eu acho que se encaixa no princípio de responsabilidade única, porque agora a responsabilidade do CatData é manter apenas os dados, não se importa com os métodos (também para o CatMethods). E também se encaixa no princípio aberto fechado, porque adicionar novos métodos não precisa alterar a classe CatData.

Minha pergunta é: é um bom ou um antipadrão?

ocomfd
fonte
15
Fazer qualquer coisa às cegas em qualquer lugar, porque você aprendeu um novo conceito é sempre um antipadrão.
Kayaman
4
Qual seria o sentido das classes se é sempre melhor separar os dados dos métodos que os modificam? Isso soa como programação não orientada a objetos (que certamente tem seu lugar). Como isso está marcado como "orientado a objetos", presumo que você deseje usar as ferramentas fornecidas pelo OOP?
user1118321
4
Outra questão que prova que o SRP é tão mal compreendido que deveria ser banido. Manter dados em vários campos públicos não é uma responsabilidade .
user949300
1
@ user949300, se a classe for responsável por esses campos, movê-los para outro lugar no aplicativo certamente terá efeito nulo. Ou, dito de outra maneira, você está enganado: eles 100% são de responsabilidade.
David Arno
2
@DavidArno e R Schmitz: A contenção de campos não conta como responsabilidade para os propósitos do SRP. Se houvesse, não haveria OOP, pois tudo seria um DTO e, em seguida, classes separadas conteriam procedimentos para trabalhar com os dados. Uma única responsabilidade pertence a uma única parte interessada . (Embora isso pareça mudar um pouco a cada iteração do principal) #
user949300

Respostas:

10

Você mostrou dois extremos ("todos os métodos privados e todos (talvez não relacionados) em um objeto" versus "tudo público e nenhum método dentro do objeto"). IMHO boa modelagem OO é nenhum deles , o ponto ideal está em algum lugar no meio.

Um teste decisivo de quais métodos ou lógicas pertencem a uma classe e o que pertence a fora é examinar as dependências que os métodos introduzirão. Métodos que não introduzem dependências adicionais são bons, desde que se ajustem bem à abstração do objeto especificado . Métodos que exigem dependências externas adicionais (como uma biblioteca de desenhos ou uma biblioteca de E / S) raramente são um bom ajuste. Mesmo que você desapareça as dependências usando a injeção de dependência, ainda pensaria duas vezes se realmente for necessário colocar esses métodos na classe de domínio.

Portanto, nem você deve tornar todos os membros públicos, nem precisa implementar métodos para todas as operações em um objeto dentro da classe. Aqui está uma sugestão alternativa:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public String getInfo(){
        return "Name:"+this.name+",weight:"+this.weight;
    }
    public Image getImage(){
        return image;
    }
}

Agora, o Catobjeto fornece lógica suficiente para permitir que o código circundante seja facilmente implementado printInfoe draw, sem expor todos os atributos em público. O lugar certo para esses dois métodos provavelmente não é uma classe divina CatMethods(desde printInfoe drawprovavelmente são preocupações diferentes, então eu acho que é muito improvável que eles pertençam à mesma classe).

Eu posso imaginar um CatDrawingControllerque implementa draw(e talvez use injeção de dependência para obter um objeto Canvas). Também posso imaginar outra classe que implementa alguma saída e uso do console getInfo(portanto, isso printInfopode se tornar obsoleto nesse contexto). Mas, para tomar decisões sensatas sobre isso, é preciso conhecer o contexto e como a Catclasse será realmente usada.

Essa é realmente a maneira como interpretei os críticos do Modelo de Domínio Anêmico de Fowler - para uma lógica geralmente reutilizável (sem dependências externas), as próprias classes de domínio são um bom lugar, portanto devem ser usadas para isso. Mas isso não significa implementar nenhuma lógica lá, muito pelo contrário.

Observe também que o exemplo acima ainda deixa espaço para tomar uma decisão sobre (im) mutabilidade. Se a Catclasse não expor nenhum setter e Imagefor imutável, esse design permitirá tornar Catimutável (o que a abordagem do DTO não fará). Mas se você acha que a imutabilidade não é necessária ou não é útil para o seu caso, você também pode ir nessa direção.

Doc Brown
fonte
Acionar downvoters felizes, isso fica chato. Se você tem alguns críticos, por favor me avise. Ficarei feliz em esclarecer qualquer mal-entendido, se você tiver algum.
Doc Brown
Embora eu geralmente concorde com sua resposta ... acho que getInfo()pode ser enganoso para alguém que faz essa pergunta. Ele sutilmente mistura responsabilidades de apresentação como printInfo()faz, derrotando a finalidade de DrawingControllerclasse
Adriano Repetti
@AdrianoRepetti Não vejo que isso esteja derrotando o objetivo de DrawingController. Eu concordo com isso getInfo()e printInfo()são nomes horríveis. Considere toString()eprintTo(Stream stream)
candied_orange
Candid @ não é (apenas) sobre o nome, mas sobre o que ele faz. Esse código é apenas sobre um detalhe da apresentação e, efetivamente, apenas abstraímos a saída, não o conteúdo processado e sua lógica.
Adriano Repetti
@ AdrianoRepetti: honestamente, este é apenas um exemplo artificial, para se encaixar na pergunta original e demonstrar meu ponto de vista. Em um sistema real, haverá um contexto circundante que permitirá tomar decisões sobre melhores métodos e nomes, é claro.
Doc Brown
6

Resposta tardia, mas não consigo resistir.

A maioria das classes X em Y é boa ou é antipadrão?

Na maioria dos casos, a maioria das regras, aplicadas sem pensar, na maioria das vezes dá terrivelmente errado (incluindo esta).

Deixe-me contar uma história sobre o nascimento de um objeto em meio ao caos de algum código processual, rápido e sujo, que aconteceu, não por design, mas por desespero.

Meu estagiário e eu somos uma programação em pares para criar rapidamente algum código descartável para raspar uma página da web. Não temos absolutamente nenhuma razão para esperar que esse código continue por muito tempo, então estamos apenas produzindo algo que funcione. Agarramos a página inteira como um barbante e cortamos as coisas de que precisamos da maneira mais incrível que você possa imaginar. Não julgue. Funciona.

Agora, ao fazer isso, criei alguns métodos estáticos para cortar. Meu estagiário criou uma classe de DTO muito parecida com a sua CatData.

Quando olhei pela primeira vez para o DTO, ele me incomodou. Os anos de dano que Java causou ao meu cérebro me fizeram recuar nos campos públicos. Mas estávamos trabalhando em c #. O C # não precisa de getters e setters prematuros para preservar seu direito de tornar os dados imutáveis ​​ou encapsulados posteriormente. Sem alterar a interface, você pode adicioná-los sempre que quiser. Talvez apenas para que você possa definir um ponto de interrupção. Tudo sem contar a seus clientes nada sobre isso. Sim C #. Boo Java.

Então eu segurei minha língua. Eu assisti enquanto ele usava meus métodos estáticos para inicializar essa coisa antes de usá-la. Tivemos cerca de 14 deles. Era feio, mas não tínhamos motivos para nos importar.

Então precisávamos disso em outros lugares. Nos descobrimos querendo copiar e colar o código. 14 linhas de inicialização sendo lançadas. Estava começando a ficar doloroso. Ele hesitou e me pediu idéias.

Relutantemente, perguntei: "você consideraria um objeto?"

Ele olhou para o DTO e torceu o rosto, confuso. "É um objeto".

"Quero dizer um objeto real"

"Hã?"

"Deixe-me mostrar uma coisa. Você decide se é útil"

Eu escolhi um novo nome e rapidamente criei algo que se parecia um pouco com isso:

public class Cat{
    CatData(string catPage) {
        this.catPage = catPage
    }
    private readonly string catPage;

    public string name() { return chop("name prefix", "name suffix"); }
    public string weight() { return chop("weight prefix", "weight suffix"); }
    public string image() { return chop("image prefix", "image suffix"); }

    private string chop(string prefix, string suffix) {
        int start = catPage.indexOf(prefix) + prefix.Length;
        int end = catPage.indexOf(suffix);
        int length = end - start;
        return catPage.Substring(start, length);
    }
}

Isso não fez nada que os métodos estáticos já não estavam fazendo. Mas agora eu havia sugado os 14 métodos estáticos para uma classe em que eles poderiam ficar sozinhos com os dados em que trabalhavam.

Não forcei meu estagiário a usá-lo. Eu apenas o ofereci e deixei que ele decidisse se queria seguir os métodos estáticos. Fui para casa pensando que ele provavelmente seguiria o que já tinha trabalhado. No dia seguinte, descobri que ele o estava usando em vários lugares. Organizou o restante do código, que ainda era feio e processual, mas agora esse pouco de complexidade estava escondido atrás de um objeto. Foi um pouco melhor.

Agora, toda vez que você acessa, está fazendo um bom trabalho. Um DTO é um valor em cache rápido e agradável. Eu me preocupei com isso, mas percebi que poderia adicionar o cache, se precisarmos, sem tocar em nenhum código de uso. Então eu não vou incomodar até que nos importemos.

Estou dizendo que você deve sempre manter objetos OO sobre DTOs? Não. Os DTOs brilham quando você precisa cruzar um limite que o impede de mover métodos. Os DTOs têm o seu lugar.

Mas o mesmo acontece com objetos OO. Aprenda a usar as duas ferramentas. Saiba o que cada um custa. Aprenda a deixar o problema, a situação e o estagiário decidirem. Dogma não é seu amigo aqui.


Como minha resposta já é ridiculamente longa, deixe-me desapontá-lo de alguns conceitos errados com uma revisão do seu código.

Por exemplo, uma classe geralmente possui membros e métodos, por exemplo:

public class Cat{
    private String name;
    private int weight;
    private Image image;

    public void printInfo(){
        System.out.println("Name:"+this.name+",weight:"+this.weight);
    }

    public void draw(){
        //some draw code which uses this.image
    }
}

Onde está o seu construtor? Isso não está me mostrando o suficiente para saber se é útil.

Porém, depois de ler sobre o princípio de responsabilidade única e o princípio aberto aberto, prefiro separar uma classe no DTO e a classe auxiliar apenas com métodos estáticos, por exemplo:

public class CatData{
    public String name;
    public int weight;
    public Image image;
}

public class CatMethods{
    public static void printInfo(Cat cat){
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat){
        //some draw code which uses cat.image
    }
}

Eu acho que se encaixa no princípio de responsabilidade única, porque agora a responsabilidade do CatData é manter apenas os dados, não se importa com os métodos (também para o CatMethods).

Você pode fazer muitas coisas tolas em nome do Princípio da Responsabilidade Única. Eu poderia argumentar que Cat Strings e Cat ints devem ser separadas. Todos os métodos de desenho e imagens devem ter sua própria classe. Como o seu programa em execução é de responsabilidade única, você deve ter apenas uma classe. : P

Para mim, a melhor maneira de seguir o Princípio da Responsabilidade Única é encontrar uma boa abstração que permita colocar complexidade em uma caixa para que você possa escondê-la. Se você pode dar um bom nome para evitar que as pessoas se surpreendam com o que descobrem quando olham para dentro, você o seguiu bastante bem. Esperar que dite mais decisões do que está pedindo problemas. Honestamente, as duas listagens de código fazem isso, então não vejo por que o SRP é importante aqui.

E também se encaixa no princípio aberto fechado, porque adicionar novos métodos não precisa alterar a classe CatData.

Bem não. O princípio de fechamento aberto não é sobre a adição de novos métodos. Trata-se de poder alterar a implementação de métodos antigos e ter que editar nada. Nada que use você e não seus métodos antigos. Em vez disso, você escreve um novo código em outro lugar. Alguma forma de polimorfismo fará isso muito bem. Não veja isso aqui.

Minha pergunta é: é um bom ou um antipadrão?

Bem, inferno, como eu deveria saber? Olha, fazer de qualquer maneira tem benefícios e custos. Quando você separa o código dos dados, pode alterar sem precisar recompilar o outro. Talvez isso seja extremamente importante para você. Talvez isso apenas torne seu código desnecessariamente complicado.

Se isso faz você se sentir melhor, não está longe de algo que Martin Fowler chama de objeto de parâmetro . Você não precisa apenas levar primitivas para o seu objeto.

O que eu gostaria que você fizesse é desenvolver um senso de como fazer sua separação, ou não, nos dois estilos de codificação. Porque acredite ou não, você não está sendo forçado a escolher um estilo. Você apenas tem que viver com sua escolha.

candied_orange
fonte
2

Você se deparou com um tópico de debate que vem criando argumentos entre desenvolvedores há mais de uma década. Em 2003, Martin Fowler cunhou a frase "Modelo de Domínio Anêmico" (ADM) para descrever essa separação de dados e funcionalidade. Ele - e outros que concordam com ele - argumentam que "Modelos de Domínio Rico" (mistura de dados e funcionalidade) é "OO adequado" e que a abordagem da ADM é um anti-padrão não-OO.

Sempre houve quem rejeitasse esse argumento e esse lado se tornou cada vez mais ousado nos últimos anos com a adoção por mais desenvolvedores de técnicas de desenvolvimento funcional. Essa abordagem incentiva ativamente a separação de preocupações de dados e funções. Os dados devem ser imutáveis ​​o máximo possível, para que o encapsulamento do estado mutável se torne uma preocupação. Não há benefício em anexar funções diretamente a esses dados em tais situações. Se é então "não OO" ou não, não interessa absolutamente a essas pessoas.

Independentemente do lado da cerca em que você se sentar (eu me sento muito firme no lado "Martin Fowler está falando muito velho"), seu uso de métodos estáticos printInfoe drawquase universalmente é desaprovado. É difícil zombar dos métodos estáticos ao escrever testes de unidade. Portanto, se tiverem efeitos colaterais (como imprimir ou desenhar em alguma tela ou outro dispositivo), não devem ser estáticos ou devem ter o local de saída passado como parâmetro.

Então você pode ter uma interface:

public interface CatMethods {
    void printInfo(Cat cat);
    void draw(Cat cat);
}

E uma implementação que é injetada no restante do sistema em tempo de execução (com outras implementações sendo usadas para teste):

internal class CatMethodsForScreen implements CatMethods {
    public void printInfo(Cat cat) {
        System.out.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public void draw(Cat cat) {
        //some draw code which uses cat.image
    }
}

Ou adicione parâmetros extras para remover os efeitos colaterais desses métodos:

public static class CatMethods {
    public static void printInfo(Cat cat, OutputHandler output) {
        output.println("Name:"+cat.name+",weight:"+cat.weight);
    }

    public static void draw(Cat cat, Canvas canvas) {
        //some draw code which uses cat.image and draws it on canvas
    }
}
David Arno
fonte
Mas por que você tentaria colocar uma linguagem OO como Java em uma linguagem funcional, em vez de usar uma linguagem funcional desde o início? É algum tipo de "Eu odeio Java, mas todo mundo usa, então eu preciso, mas pelo menos vou escrever do meu jeito". A menos que você esteja afirmando que o paradigma OO está obsoleto e desatualizado de alguma forma. Eu assino a escola de pensamento "Se você quer X, você sabe onde obtê-lo".
Kayaman
@ Kayaman " , assino o" Se você quer X, você sabe onde obtê-lo "escola de pensamento ". Eu não. Acho essa atitude míope e um pouco ofensiva. Não uso Java, mas uso C # e ignoro alguns dos recursos "real OO". Não ligo para herança, objetos com estado e coisas do gênero e escrevo muitos métodos estáticos e classes seladas (finais). O tamanho das ferramentas e da comunidade em torno do C # é inigualável. Para F #, é muito mais pobre. Então, assino a escola de pensamento "Se você quer X, peça para ele ser adicionado às suas ferramentas atuais".
David Arno
1
Eu diria que ignorar aspectos de um idioma é muito diferente de tentar misturar e combinar recursos de outros idiomas. Também não tento escrever "OO real", pois tentar seguir regras (ou princípios) em uma ciência inexata como a programação não funciona. Claro que você precisa conhecer as regras para poder quebrá-las. Aderir cegamente aos recursos pode ser ruim para o desenvolvimento da linguagem. Sem ofensa, mas IMHO, uma parte da cena do FP parece sentir "FP é a melhor coisa desde pão fatiado, por que as pessoas não o entendem". Eu nunca diria (ou pensaria) OO ou Java é "o melhor".
Kayaman
1
@Kayaman, mas o que significa "tentar misturar e combinar recursos de outros idiomas"? Você está argumentando que os recursos funcionais não devem ser adicionados ao java porque "Java é uma linguagem OO"? Se sim, esse é um argumento míope na minha opinião. Deixe a linguagem evoluir com as necessidades dos desenvolvedores. Forçá-los a mudar para outro idioma é a abordagem errada, eu acho. Certamente, alguns "defensores da PF" exageram no fato de a FP ser a melhor coisa de todos os tempos. E alguns "advogados de OO" exageram sobre o fato de OO ter "vencido a batalha porque é claramente superior". Ignore os dois.
David Arno
1
Eu não estou sendo anti-FP. Eu o uso em Java, onde é útil. Mas se um programador diz "Eu quero isso", é como um cliente dizendo "Eu quero isso". Programadores não são designers de linguagem e clientes não são programadores. Votou sua resposta, já que você diz que a "solução" do OP é mal vista. O comentário na pergunta é mais sucinto, ou seja, ele reinventou a programação procedural em uma linguagem OO. A idéia geral tem um ponto, como você mostrou. Um modelo de domínio não anêmico não significa que dados e comportamento são exclusivos, e renderizadores ou apresentadores externos seriam proibidos.
Kayaman
0

DTOs - Objetos de Transporte de Dados

são úteis para isso. Se você deseja desviar dados entre programas ou sistemas, os DTOs são desejáveis, pois fornecem um subconjunto gerenciável de um objeto que se preocupa apenas com a estrutura e a formatação dos dados. A vantagem é que você não precisa sincronizar atualizações para métodos complexos em vários sistemas (desde que os dados subjacentes não sejam alterados).

O objetivo principal do OO é vincular dados e o código que atua nesses dados. Separar um objeto lógico em classes separadas geralmente é uma má idéia.

James Anderson
fonte
-1

Muitos padrões e princípios de design entram em conflito e, finalmente, apenas seu julgamento como um bom programador o ajudará a decidir quais padrões devem ser aplicados a um problema específico.

Na minha opinião, o princípio Faça a coisa mais simples que poderia funcionar deve vencer por padrão, a menos que você tenha um motivo forte para tornar o design mais complicado. Portanto, no seu exemplo específico, eu optaria pelo primeiro design, que é uma abordagem orientada a objetos mais tradicional, com dados e funções juntos na mesma classe.

Aqui estão algumas perguntas que você pode se perguntar sobre o aplicativo:

  • Vou precisar fornecer uma maneira alternativa de renderizar uma imagem de gato? Se sim, a herança não será uma maneira conveniente de fazer isso?
  • Minha classe Cat precisa herdar de outra classe ou satisfazer uma interface?

Responder sim a qualquer uma dessas opções pode levar a um design mais complexo com DTO separado e classes funcionais.

Jordan Rieger
fonte
-1

É um bom padrão?

Você provavelmente obterá respostas diferentes aqui, porque as pessoas seguem diferentes escolas de pensamento. Portanto, antes mesmo de começar: Esta resposta está de acordo com a programação orientada a objetos, conforme descrito por Robert C. Martin no livro "Código Limpo".


POO "verdadeiro"

Tanto quanto sei, há 2 interpretações de OOP. Para a maioria das pessoas, tudo se resume a "um objeto é uma classe".

No entanto, achei a outra escola de pensamento mais útil: "Um objeto é algo que faz alguma coisa". Se você estiver trabalhando com classes, cada classe seria uma das duas coisas: um " detentor de dados " ou um objeto . Os titulares de dados mantêm dados (duh), os objetos fazem coisas. Isso não significa que um detentor de dados não possa ter métodos! Pense em list: A listrealmente não faz nada, mas possui métodos para reforçar a maneira como mantém os dados .

Titulares de dados

Você Caté um excelente exemplo de um detentor de dados. Certamente, fora do seu programa, um gato é algo que faz alguma coisa , mas dentro do seu programa, a Caté uma String name, um int weighte uma Imagem image.

O fato de os titulares de dados não fazerem nada também não significa que eles não contenham lógica de negócios! Se a sua Catclasse tem um construtor que requer name, weighte image, você encapsulado com sucesso a regra de negócio que cada gato tem aqueles. Se você pode alterar weightdepois que uma Catclasse foi criada, essa é outra regra de negócios encapsulada. Essas são coisas muito básicas, mas isso significa apenas que é muito importante não entendê-las errado - e, dessa maneira, você garante que é impossível entender errado.

Objetos

Objetos fazem alguma coisa. Se você deseja limpar objetos *, podemos mudar para "Objetos fazem uma coisa".

Imprimir as informações do gato é o trabalho de um objeto. Por exemplo, você pode chamar esse objeto " CatInfoLogger", com um método público Log(Cat). É tudo o que precisamos saber externamente: esse objeto registra as informações de um gato e, para isso, precisa de um Cat.

Por dentro, esse objeto teria referências a tudo o que precisa para cumprir sua única responsabilidade de registrar gatos. Nesse caso, isso é apenas uma referência a Systemfor System.out.println, mas na maioria dos casos, serão referências particulares a outros objetos. Se a lógica para formatar a saída antes da impressão se tornar muito complexa, é CatInfoLoggerpossível obter uma referência a um novo objeto CatInfoFormatter. Se, posteriormente, todo log também precisar ser gravado em um arquivo, você poderá adicionar um CatToFileLoggerobjeto que faça isso.

Suportes de dados x objetos - resumo

Agora, por que você faria tudo isso? Porque agora alterar uma classe muda apenas uma coisa ( a maneira como um gato é registrado no exemplo). Se você está mudando um objeto, está mudando apenas a maneira como uma determinada ação é executada; se você alterar um detentor de dados, altere apenas quais dados são mantidos e / ou de que maneira eles são mantidos.

Alterar dados pode significar que a maneira como algo é feito também precisa mudar, mas essa mudança não acontecerá até que você faça isso mudando o objeto responsável.

Exagero?

Essa solução pode parecer um pouco exagerada. Deixe-me ser franco: é . Se todo o seu programa for tão grande quanto o exemplo, não faça nenhuma das opções acima - basta escolher uma das formas propostas com um sorteio. Não há perigo de confundir outro código se não é nenhum outro código.

No entanto, qualquer software "sério" (= mais que um script ou 2) provavelmente é grande o suficiente para lucrar com uma estrutura como essa.


Responda

Separar a maioria das classes em classes DTO e auxiliares [...] é bom ou anti-padrão?

Transformar a "maioria das classes" (sem qualquer qualificação adicional) em DTOs / detentores de dados não é um anti-padrão, porque na verdade não é nenhum padrão - é mais ou menos aleatório.

Manter dados e objetos separados, no entanto, é visto como uma boa maneira de manter seu código limpo *. Às vezes, você precisará apenas de um DTO "anêmico" e pronto. Outras vezes, você precisa de métodos para impor a maneira como os dados são mantidos. Apenas não coloque a funcionalidade do seu programa com os dados.


* Isso é "limpo" com um C maiúsculo como o título do livro, porque - lembre-se do primeiro parágrafo - trata-se de uma escola de pensamento, enquanto há outras.

R. Schmitz
fonte