Devemos evitar objetos personalizados como parâmetros?

49

Suponha que eu tenha um objeto personalizado, Student :

public class Student{
    public int _id;
    public String name;
    public int age;
    public float score;
}

E uma classe, Window , usada para mostrar informações de um Aluno :

public class Window{
    public void showInfo(Student student);
}

Parece bastante normal, mas achei que o Window não é muito fácil de testar individualmente, porque ele precisa de um objeto Student real para chamar a função. Então, eu tento modificar showInfo para que ele não aceite um objeto Student diretamente:

public void showInfo(int _id, String name, int age, float score);

para que seja mais fácil testar o Window individualmente:

showInfo(123, "abc", 45, 6.7);

Mas eu achei que a versão modificada tem outros problemas:

  1. Modificar Aluno (por exemplo: adicionar novas propriedades) requer a modificação da assinatura do método showInfo

  2. Se o Student tivesse muitas propriedades, a assinatura do método seria muito longa.

Então, usando objetos personalizados como parâmetro ou aceite cada propriedade em objetos como parâmetro, qual é mais sustentável?

ggrr
fonte
40
E o seu 'aprimorado' showInforequer uma String real, um float real e duas entradas reais. Como é fornecer um Stringobjeto real melhor do que fornecer um Studentobjeto real ?
Bart van Ingen Schenau
28
Um grande problema ao passar os parâmetros diretamente: agora você tem dois intparâmetros. No site de chamada, não há confirmação de que você está passando na ordem correta. E se você trocar ide age, ou firstNamee lastName? Você está apresentando um ponto potencial de falha que pode ser muito difícil de detectar até que exploda na sua cara e você o adiciona em todos os sites de chamadas .
Chris Hayes
38
@ChrisHayes ah, o velho showForm(bool, bool, bool, bool, int)método - Eu amo aqueles ...
Boris a aranha
3
@ChrisHayes pelo menos não JS ...
Jens Schauder
2
uma propriedade subvalorizado de testes: se é difícil para criar / usar seus próprios objetos em testes, a sua API poderia usar algum trabalho :)
Eevee

Respostas:

131

Usar um objeto personalizado para agrupar parâmetros relacionados é, na verdade, um padrão recomendado. Como refatoração, é chamado de Introduzir Objeto de Parâmetro .

Seu problema está em outro lugar. Primeiro, o genérico não Windowdeve saber nada sobre o Student. Em vez disso, você deve StudentWindowsaber que apenas a exibição é exibida Students. Segundo, não há absolutamente nenhum problema em criar uma Studentinstância para testar StudentWindow, desde Studentque não contenha nenhuma lógica complexa que complicaria drasticamente o teste StudentWindow. Se ele tiver essa lógica, Studenté preferível criar uma interface e zombar.

Eufórico
fonte
14
Vale a pena avisar que você poderá ter problemas se o novo objeto não for realmente um agrupamento lógico. Não tente calçar todos os parâmetros configurados em um único objeto; decidir caso a caso. O exemplo da pergunta parece claramente ser um bom candidato para isso. O Studentagrupamento faz sentido e é provável que surja em outras áreas do aplicativo.
Jpmc26 12/16
Pedante falar se você já tem o objeto, por exemplo Student, seria um Preserve Whole Objeto
gillifirca abuzittin
4
Lembre-se também da Lei de Demeter . Há um equilíbrio a ser atingido, mas o tldr não funciona a.b.cse o seu método for necessário a. Se o seu método chegar ao ponto em que você precisa ter aproximadamente mais de 4 parâmetros ou 2 níveis de acesso à propriedade, provavelmente precisará ser fatorado. Observe também que esta é uma diretriz - como todas as outras diretrizes, requer critério do usuário. Não o siga cegamente.
Dan Pantry
7
Achei a primeira frase desta resposta extremamente difícil de analisar.
helrich
5
@ Qwerky Eu discordo muito. Aluno REALMENTE soa como uma folha no gráfico de objetos (exceto outros objetos triviais, como talvez Nome, DateOfBirth etc.), apenas um contêiner para o estado de um aluno. Não há nenhuma razão para que seja difícil construir um aluno, porque deve ser um tipo de registro. Criar duplas de teste para um aluno parece uma receita para testes difíceis de manter e / ou dependência pesada de alguma estrutura de isolamento sofisticada.
Sara
26

Você diz que é

não é muito fácil testar individualmente, porque ele precisa de um objeto Student real para chamar a função

Mas você pode simplesmente criar um objeto de aluno para passar para sua janela:

showInfo(new Student(123,"abc",45,6.7));

Não parece muito mais complexo ligar.

Tom.Bowen89
fonte
7
O problema surge quando Studentse refere a um University, que se refere a muitas Facultys e Campuss, com Professors e Buildings, nenhum dos quais showInforealmente usa, mas você não tenha definido qualquer interface que permite que os testes para "saber" que e fornecer apenas o estudante relevante dados, sem criar toda a organização. O exemplo Studenté um objeto de dados simples e, como você diz, os testes devem ter prazer em trabalhar com ele.
Steve Jessop
4
O problema surge quando o estudante se refere a uma universidade, que se refere a muitas faculdades e campuss, com professores e edifícios, sem descanso para os ímpios.
Abuzittin gillifirca
11
@abuzittingillifirca, "Objeto Mãe" é uma solução, também o objeto do aluno pode ser muito complexo. Talvez seja melhor ter o UniversityId e um serviço (usando injeção de dependência) que fornecerá um objeto University a partir de um UniversityId.
31416 Ian
12
Se o Student for muito complexo ou difícil de inicializar, apenas faça uma zombaria. O teste é muito mais poderoso com estruturas como Mockito ou outros idiomas equivalentes.
Borjab
4
Se showInfo não se importa com a Universidade, configure-o como nulo. Os nulos são horríveis na produção e os testes enviados por Deus. Ser capaz de especificar um parâmetro como nulo em um teste comunica a intenção e diz que "isso não é necessário aqui". Embora eu também considere criar algum tipo de modelo de exibição para estudantes que contêm apenas os dados relevantes, considerando que showInfo soa como um método em uma classe de interface do usuário.
Sara
22

Em termos leigos:

  • O que você chama de "objeto personalizado" geralmente é chamado de objeto.
  • Não é possível evitar a passagem de objetos como parâmetros ao projetar qualquer programa ou API não trivial ou ao usar qualquer API ou biblioteca não trivial.
  • Não há problema em passar objetos como parâmetros. Dê uma olhada na API Java e você verá muitas interfaces que recebem objetos como parâmetros.
  • As classes nas bibliotecas que você usa foram escritas por meros mortais como você e eu; portanto, as que escrevemos não são "personalizadas" , apenas são.

Editar:

Como @ Tom.Bowen89 afirma, não é muito mais complexo testar o método showInfo:

showInfo(new Student(8812372,"Peter Parker",16,8.9));
Tulains Córdova
fonte
3
  1. No seu exemplo de aluno, presumo que seja trivial chamar o construtor Student para criar um aluno para passar para showInfo. Portanto, não há problema.
  2. Assumindo o exemplo, o aluno é deliberadamente banalizado para esta pergunta e é mais difícil de construir, então você pode usar um teste duplo . Existem várias opções para duplas de teste, zombarias, stubs, etc. mencionadas no artigo de Martin Fowler para você escolher.
  3. Se você quiser tornar a função showInfo mais genérica, pode iterá-la sobre as variáveis ​​públicas, ou talvez os acessadores públicos do objeto passem e executem a lógica de exibição para todas elas. Em seguida, você poderia passar qualquer objeto que estivesse em conformidade com esse contrato e ele funcionaria conforme o esperado. Este seria um bom lugar para usar uma interface. Por exemplo, passe um objeto Showable ou ShowInfoable para a função showInfo que possa mostrar não apenas informações de alunos, mas informações de qualquer objeto que implemente a interface (obviamente essas interfaces precisam de nomes melhores, dependendo de quão específico ou genérico você deseja que o objeto possa ser transmitido. ser e de que aluno é uma subclasse).
  4. Muitas vezes, é mais fácil repassar primitivas e, às vezes, necessárias para o desempenho, mas quanto mais você agrupar conceitos semelhantes, mais compreensível será o seu código. A única coisa a observar é tentar não exagerar e acabar com o fracasso da empresa .
Encaitar
fonte
3

Steve McConnell, do Code Complete, abordou esse problema, discutindo os benefícios e as desvantagens de passar objetos para métodos em vez de usar propriedades.

Perdoe-me se eu entendi errado alguns detalhes, estou trabalhando de memória, pois já faz mais de um ano desde que tive acesso ao livro:

Ele chega à conclusão de que é melhor você não usar um objeto, enviando apenas as propriedades absolutamente necessárias para o método. O método não precisa saber nada sobre o objeto fora das propriedades que ele usará como parte de suas operações. Além disso, com o tempo, se o objeto for alterado, isso poderá ter consequências indesejadas no método usando o objeto.

Ele também abordou que, se você acabar com um método que aceita muitos argumentos diferentes, isso provavelmente é um sinal de que o método está fazendo muito e deve ser dividido em mais métodos menores.

No entanto, às vezes, às vezes, você realmente precisa de muitos parâmetros. O exemplo que ele dá seria de um método que constrói um endereço completo, usando muitas propriedades de endereço diferentes (embora isso possa ser contornado usando uma matriz de cadeias de caracteres quando você pensa sobre isso).

user1666620
fonte
7
Eu tenho o código completo 2. Há uma página inteira dedicada a esse problema. A conclusão é que os parâmetros devem estar no nível correto de abstração. Às vezes, isso exige a passagem de um objeto inteiro, às vezes apenas atributos individuais.
Venha de
UV. O código de referência completo é duplo mais do que bom. Uma boa consideração de design em vez de testar a conveniência. Prefiro o design, acho que McConnell diria em nosso contexto. Portanto, uma excelente conclusão seria "integrar o objeto de parâmetro ao design" ( Studentneste caso). E é assim que o teste informa o design , abraçando completamente a resposta com mais votos, mantendo a integridade do design.
Radarbob
2

É muito mais fácil escrever e ler testes se você passar o objeto inteiro:

public class AStudentView {
    @Test 
    public void displays_failing_grade_warning_when_a_student_with_a_failing_grade_is_shown() {
        StudentView view = aStudentView();
        view.show(aStudent().withAFailingGrade().build());
        Assert.that(view, displaysFailingGradeWarning());
    }

    private Matcher<StudentView> displaysFailingGradeWarning() {
        ...
    }
}

Para comparação,

view.show(aStudent().withAFailingGrade().build());

A linha pode ser escrita, se você passar valores separadamente, como:

showAStudentWithAFailingGrade(view);

onde a chamada de método real está enterrada em algum lugar como

private showAStudentWithAFailingGrade(StudentView view) {
    int someId = .....
    String someName = .....
    int someAge = .....
    // why have been I peeking and poking values I don't care about
    decimal aFailingGrade = .....
    view.show(someId, someName, someAge, aFailingGrade);
}

Para chegar ao ponto, que você não pode colocar a chamada de método real no teste é um sinal de que sua API está incorreta.

abuzittin gillifirca
fonte
1

Você deve passar o que faz sentido, algumas idéias:

Mais fácil de testar. Se o (s) objeto (s) precisam ser editados, o que requer menos refatoração? É útil reutilizar esta função para outros fins? Qual é a menor quantidade de informações que preciso fornecer a essa função para realizar seu objetivo? (Ao dividi-lo - pode permitir que você reutilize esse código - tenha cuidado para não cair no buraco do design de fazer essa função e depois gargalhar tudo para usar exclusivamente esse objeto.)

Todas essas regras de programação são apenas guias para você pensar na direção certa. Apenas não construa uma besta de código - se você não tiver certeza e precisar prosseguir, escolha uma direção / sua ou uma sugestão aqui, e se você chegar a um ponto em que pensa 'oh, eu deveria ter feito isso caminho '- você provavelmente pode voltar e refatorá-lo facilmente. (Por exemplo, se você tem turma de professor - ela precisa da mesma propriedade definida como Aluno e você muda sua função para aceitar qualquer objeto do formulário Pessoa)

Eu estaria mais inclinado a manter o objeto principal sendo transmitido - porque como eu codifico, ele explica mais facilmente o que essa função está fazendo.

Lilly
fonte
1

Uma rota comum para contornar isso é inserir uma interface entre os dois processos.

public class Student {

    public int id;
    public String name;
    public int age;
    public float score;
}

interface HasInfo {
    public String getInfo();
}

public class StudentInfo implements HasInfo {
    final Student student;

    public StudentInfo(Student student) {
        this.student = student;
    }

    @Override
    public String getInfo() {
        return student.name;
    }

}

public class Window {

    public void showInfo(HasInfo info) {

    }
}

Às vezes, isso fica um pouco confuso, mas as coisas ficam um pouco mais organizadas em Java se você usar uma classe interna.

interface HasInfo {
    public String getInfo();
}

public class Student {

    public int id;
    public String name;
    public int age;
    public float score;

    public HasInfo getInfo() {
        return new HasInfo () {
            @Override
            public String getInfo() {
                return name;
            }

        };
    }
}

Em seguida, você pode testar a Windowclasse dando a ela um HasInfoobjeto falso .

Suspeito que este seja um exemplo do Padrão Decorador .

Adicionado

Parece haver alguma confusão causada pela simplicidade do código. Aqui está outro exemplo que pode demonstrar melhor a técnica.

interface Drawable {

    public void Draw(Pane pane);
}

/**
 * Student knows nothing about Window or Drawable.
 */
public class Student {

    public int id;
    public String name;
    public int age;
    public float score;
}

/**
 * DrawsStudents knows about both Students and Drawable (but not Window)
 */
public class DrawsStudents implements Drawable {

    private final Student subject;

    public DrawsStudents(Student subject) {
        this.subject = subject;
    }

    @Override
    public void Draw(Pane pane) {
        // Draw a Student on a Pane
    }

}

/**
 * Window only knows about Drawables.
 */
public class Window {

    public void showInfo(Drawable info) {

    }
}
OldCurmudgeon
fonte
Se showInfo apenas queria exibir o nome do aluno, por que não apenas passar o nome? agrupar um campo nomeado semanticamente significativo em uma interface abstrata que contenha uma string sem nenhuma pista sobre o que a string representa parece um enorme downgrade, tanto em termos de manutenção quanto de compreensibilidade.
Sara
@kai - O uso de Studente Stringaqui para o tipo de retorno é apenas para demonstração. Provavelmente haveria parâmetros adicionais para getInfoos quais Panedesenhar se estiver desenhando. O conceito aqui é passar componentes funcionais como decoradores do objeto original .
OldCurmudgeon
nesse caso, você estaria acoplamento da entidade estudantil firmemente a sua estrutura de interface do usuário, que soa ainda pior ...
sara
11
@kai - Muito pelo contrário. Minha interface do usuário conhece apenas HasInfoobjetos. Studentsabe ser um.
OldCurmudgeon
Se você der o getInforetorno nulo, passe-o Panepara chamar, então a implementação (na Studentclasse) será acoplada repentinamente ao swing ou ao que você estiver usando. Se você fizer com que ela retorne alguma string e use 0 parâmetros, sua interface do usuário não saberá o que fazer com a string sem suposições mágicas e acoplamento implícito. Se você getInfoefetivamente retornar algum modelo de vista com propriedades relevantes, sua Studentclasse será novamente acoplada à lógica de apresentação. Eu não acho que qualquer uma dessas alternativas são desejáveis
sara
1

Você já tem muitas respostas boas, mas aqui estão mais algumas sugestões que podem permitir que você veja uma solução alternativa:

  • Seu exemplo mostra um Aluno (claramente um objeto de modelo) sendo passado para uma Janela (aparentemente um objeto no nível da visualização). Um objeto intermediário do Controller ou Presenter pode ser benéfico se você ainda não tiver um, permitindo que você isole a interface do usuário do seu modelo. O controlador / apresentador deve fornecer uma interface que possa ser usada para substituí-la no teste da interface do usuário e deve usar interfaces para se referir aos objetos de modelo e exibir objetos para poder isolá-lo dos dois para teste. Pode ser necessário fornecer uma maneira abstrata de criar ou carregar esses objetos (por exemplo, objetos de fábrica, objetos de repositório ou similares).

  • Transferir partes relevantes dos objetos do modelo para um Objeto de Transferência de Dados é uma abordagem útil para fazer a interface quando o modelo se torna muito complexo.

  • Pode ser que seu Aluno viole o Princípio de Segregação de Interface. Nesse caso, pode ser benéfico dividi-lo em várias interfaces mais fáceis de trabalhar.

  • O Lazy Loading pode facilitar a construção de grandes gráficos de objetos.

Jules
fonte
0

Esta é realmente uma pergunta decente. O problema real aqui é o uso do termo genérico "objeto", que pode ser um pouco ambíguo.

Geralmente, em uma linguagem clássica de POO, o termo "objeto" passou a significar "instância de classe". As instâncias de classe podem ser bem pesadas - propriedades públicas e privadas (e as intermediárias), métodos, herança, dependências etc. Você realmente não gostaria de usar algo assim para simplesmente passar algumas propriedades.

Nesse caso, você está usando um objeto como um contêiner que simplesmente contém algumas primitivas. No C ++, objetos como esses eram conhecidos como structs(e ainda existem em linguagens como C #). As estruturas foram, de fato, projetadas exatamente para o uso que você fala - elas agrupavam objetos e primitivos relacionados quando tinham um relacionamento lógico.

No entanto, nas linguagens modernas, não há realmente nenhuma diferença entre uma estrutura e uma classe quando você está escrevendo o código ; portanto, você pode usar um objeto. (Nos bastidores, no entanto, há algumas diferenças das quais você deve estar ciente - por exemplo, uma estrutura é um tipo de valor, não um tipo de referência.) Basicamente, desde que você mantenha seu objeto simples, será fácil para testar manualmente. Porém, linguagens e ferramentas modernas permitem mitigar bastante isso (via interfaces, estruturas de simulação, injeção de dependência etc.)

lunchmeat317
fonte
11
Passar uma referência não é caro, mesmo que o objeto tenha um bilhão de terabytes de tamanho, porque a referência ainda tem apenas o tamanho de um int na maioria dos idiomas. Você deve se preocupar mais se o método de recebimento está exposto a uma API muito grande e se você junta as coisas de maneira indesejável. Eu consideraria criar uma camada de mapeamento que traduza objetos de negócios ( Student) em modelos de exibição ( StudentInfoou StudentInfoViewModeletc.), mas pode não ser necessário.
Sara
Classes e estruturas são muito diferentes. Um é passado por valor (ou seja, o método que o recebe obtém uma cópia ) e o outro é passado por referência (o receptor recebe um ponteiro para o original). Não entender essa diferença é perigoso.
RubberDuck
@kai Entendo que passar uma referência não é caro. O que estou dizendo é que criar uma função que requer uma instância de classe completa pode ser mais difícil de testar, dependendo das dependências dessa classe, de seus métodos etc. - pois você teria que zombar dessa classe de alguma forma.
Lunchmeat317 15/05
Pessoalmente, sou contra a zombaria de quase tudo, exceto classes de fronteira que acessam sistemas externos (arquivo / rede IO) ou não determinísticos (por exemplo, aleatórios, baseados no tempo do sistema etc.). Se uma classe que estou testando tem uma dependência que não é relevante para o recurso atual que está sendo testado, prefiro passar nulo sempre que possível. Mas se você estiver testando um método que usa um objeto de parâmetro, se esse objeto tiver várias dependências, eu ficaria preocupado com o design geral. Tais objetos devem ser leves.
Sara