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 , que é usada para mostrar informações de um Student :

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

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

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

para que seja mais fácil testar Window individualmente:

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

Mas descobri 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 de showInfo

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

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

    
por ggrr 12.05.2016 / 05:12
fonte

10 respostas

131

Usar um objeto personalizado para agrupar parâmetros relacionados é, na verdade, um padrão recomendado. Como uma refatoração, ela é chamada de Introduce Parameter Object .

Seu problema está em outro lugar. Primeiro, o Window genérico não deve saber nada sobre o Student. Em vez disso, você deve ter algum tipo de StudentWindow que saiba apenas exibir Students . Segundo, não há absolutamente nenhum problema em criar Student instance para testar StudentWindow desde que Student não contenha nenhuma lógica complexa que complique drasticamente o teste de StudentWindow . Se tiver essa lógica, fazer Student de uma interface e zombar dela deve ser o preferido.

    
por 12.05.2016 / 06:02
fonte
26

Você diz que é

not quite easy to test individually, because it needs a real Student object to call the function

Mas você pode 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.

    
por 12.05.2016 / 10:12
fonte
22

Em termos leigos:

  • O que você chama de "objeto personalizado" geralmente é chamado de objeto.
  • Você não pode evitar passar objetos como parâmetros ao projetar qualquer programa ou API não trivial, ou usar qualquer API ou biblioteca não trivial.
  • É perfeitamente correto 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, então as que escrevemos não são "custom" , elas são apenas.

Editar:

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

showInfo(new Student(8812372,"Peter Parker",16,8.9));
    
por 12.05.2016 / 13:08
fonte
3
  1. Em seu exemplo de aluno, presumo que seja trivial chamar o construtor Student para criar um aluno para passar para showInfo. Então não há problema.
  2. Assumindo o exemplo O aluno é deliberadamente banalizado para essa pergunta e é mais difícil de criar, então você poderia usar um teste duplo . Existem várias opções para testes de duplas, zombarias, stubs, etc. que são comentadas no artigo de Martin Fowler para escolher.
  3. Se você quiser tornar a função showInfo mais genérica, pode fazer com que ela itere sobre as variáveis públicas, ou talvez os acessadores públicos do objeto passem e executem a lógica de exibição para todos eles. Então você poderia passar qualquer objeto que estivesse de acordo com esse contrato e funcionaria como esperado. Este seria um bom lugar para usar uma interface. Por exemplo, passe um objeto Showable ou ShowInfoable para a função showInfo que pode mostrar não apenas informações dos alunos, mas informações de qualquer objeto que implementam a interface (obviamente essas interfaces precisam de nomes melhores dependendo de quão específico ou genérico você deseja que o objeto seja transmitido). ser e o que um aluno é uma subclasse de).
  4. Geralmente, é mais fácil transmitir primitivas, e às vezes necessárias para o desempenho, mas quanto mais você puder agrupar conceitos semelhantes, mais compreensível será seu código. A única coisa a observar é tentar não fazê-lo e acabar com o enterprise fizzbuzz .
por 12.05.2016 / 11:14
fonte
3

Steve McConnell, no Code Complete, abordou essa questão, discutindo os benefícios e desvantagens de passar objetos para métodos em vez de usar propriedades.

Perdoe-me se eu tiver alguns detalhes errados, estou trabalhando de memória, já faz mais de um ano desde que eu 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 deve precisar saber nada sobre o objeto fora das propriedades que ele usará como parte de suas operações. Além disso, ao longo do tempo, se o objeto for alterado, isso pode ter consequências indesejadas no método usando o objeto.

Ele também disse que se você acaba com um método que aceita muitos argumentos diferentes, então isso é provavelmente um sinal de que o método está fazendo muito e deve ser dividido em 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 feito usando uma matriz de string quando você pensa sobre isso).

    
por 12.05.2016 / 12:01
fonte
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 do 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, você não pode colocar a chamada do método real no teste é um sinal de que sua API é ruim.

    
por 12.05.2016 / 12:52
fonte
1

Você deve passar o que faz sentido, algumas ideias:

Mais fácil de testar. Se o objeto precisa ser editado, o que requer menos refatoração? Reutilizar esta função para outros fins é útil? Qual é a menor quantidade de informação que eu preciso para dar essa função para fazer o seu propósito? (Ao dividi-lo - ele pode permitir que você reutilize este código - fique atento ao cair no buraco do design de fazer essa função e, em seguida, limpe 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 tem certeza e só precisa prosseguir, escolha uma direção / sua própria ou uma sugestão aqui, e se você atingir um ponto em que você pensa 'oh, eu deveria ter feito isso maneira '- você provavelmente pode voltar e refatorá-lo facilmente. (Por exemplo, se você tiver a classe Professor - ela só precisa da mesma propriedade configurada 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 passado - porque como eu codifico, ele explicará mais facilmente o que esta função está fazendo.

    
por 12.05.2016 / 22:16
fonte
1

Uma rota comum em torno disso é 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) {

    }
}

Isso fica um pouco bagunçado às vezes, mas as coisas ficam um pouco mais claras em Java se você usa 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;
            }

        };
    }
}

Você pode testar a classe Window dando a ela um objeto HasInfo 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) {

    }
}
    
por 13.05.2016 / 10:39
fonte
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 modelo) sendo passado para uma janela (aparentemente um objeto em nível de visão). Um objeto intermediário Controller ou Presenter pode ser benéfico se você ainda não tiver um, permitindo que você isole sua interface de usuário de seu modelo. O controlador / apresentador deve fornecer uma interface que possa ser usada para substituí-lo por testes de interface do usuário, e deve usar interfaces para se referir a objetos de modelo e objetos de visualização para poder isolá-lo de ambos para testes. Talvez seja necessário fornecer uma maneira abstrata de criar ou carregar esses itens (por exemplo, objetos Factory, objetos Repository ou similares).

  • A transferência de partes relevantes de seus objetos de modelo para um Objeto de Transferência de Dados é uma abordagem útil para a interface quando o modelo se torna muito complexo.

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

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

por 14.05.2016 / 09:16
fonte
0

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

Geralmente, em uma linguagem OOP clássica, o termo "objeto" passou a significar "instância de classe". As instâncias de classe podem ser muito pesadas - propriedades públicas e privadas (e aquelas entre elas), 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 mantém algumas primitivas. Em C ++, objetos como esses eram conhecidos como structs (e eles ainda existem em linguagens como C #). As estruturas foram, de fato, projetadas exatamente para o uso do qual você fala - elas agruparam objetos relacionados e primitivos quando eles 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 , então você está bem usando um objeto. (Nos bastidores, no entanto, existem algumas diferenças que você deve estar ciente - por exemplo, uma struct é um tipo de valor, não um tipo de referência.) Basicamente, contanto que você mantenha seu objeto simples, será fácil para testar manualmente. Ferramentas e linguagens modernas permitem mitigar isso um pouco (através de interfaces, estruturas de simulação, injeção de dependência, etc.)

    
por 13.05.2016 / 01:29
fonte