Por que não se tornou um padrão comum usar setters no construtor?


23

Acessores e modificadores (também conhecidos como setters e getters) são úteis por três razões principais:

  1. Eles restringem o acesso às variáveis.
    • Por exemplo, uma variável pode ser acessada, mas não modificada.
  2. Eles validam os parâmetros.
  3. Eles podem causar alguns efeitos colaterais.

Universidades, cursos on-line, tutoriais, artigos de blog e exemplos de código na Web estão todos enfatizando a importância dos acessadores e modificadores, quase parecem um "must have" para o código hoje em dia. Assim, é possível encontrá-los mesmo quando eles não fornecem nenhum valor adicional, como o código abaixo.

public class Cat {
    private int age;

    public int getAge() {
        return this.age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

Dito isto, é muito comum encontrar modificadores mais úteis, aqueles que realmente validam os parâmetros e lançam uma exceção ou retornam um booleano se uma entrada inválida foi fornecida, algo como isto:

/**
 * Sets the age for the current cat
 * @param age an integer with the valid values between 0 and 25
 * @return true if value has been assigned and false if the parameter is invalid
 */
public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

Mas mesmo assim, quase nunca vejo os modificadores serem chamados de um construtor, portanto, o exemplo mais comum de uma classe simples com a qual me deparo é:

public class Cat {
    private int age;

    public Cat(int age) {
        this.age = age;
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Mas alguém poderia pensar que essa segunda abordagem é muito mais segura:

public class Cat {
    private int age;

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    public int getAge() {
        return this.age;
    }

    /**
     * Sets the age for the current cat
     * @param age an integer with the valid values between 0 and 25
     * @return true if value has been assigned and false if the parameter is invalid
     */
    public boolean setAge(int age) {
        //Validate your parameters, valid age for a cat is between 0 and 25 years
        if(age > 0 && age < 25) {
            this.age = age;
            return true;
        }
        return false;
    }

}

Você vê um padrão semelhante em sua experiência ou sou apenas eu tendo azar? E se você faz, então o que você acha que está causando isso? Existe uma desvantagem óbvia para o uso de modificadores dos construtores ou eles são apenas considerados mais seguros? É algo mais?


1
@stg, obrigado, ponto interessante! Você poderia expandi-lo e respondê-lo com um exemplo de uma coisa ruim que talvez aconteça?
precisa saber é o seguinte


8
@stg Na verdade, é um anti-padrão usar métodos substituíveis no construtor e muitas ferramentas de qualidade de código apontam isso como um erro. Você não sabe o que a versão substituída fará e pode fazer coisas desagradáveis, como deixar "isso" escapar antes que o construtor termine, o que pode levar a todo tipo de problemas estranhos.
Michał Kosmulski 31/08/16

1
@gnat: Não acredito que seja uma duplicata. Os métodos de uma classe devem chamar seus próprios getters e setters? , devido à natureza das chamadas do construtor serem invocadas em um objeto que não está totalmente formado.
precisa

3
Observe também que sua "validação" apenas deixa a idade não inicializada (ou zero, ou ... outra coisa, dependendo do idioma). Se você deseja que o construtor falhe, verifique o valor de retorno e crie uma exceção em caso de falha. Tal como está, sua validação acabou de introduzir um bug diferente.
inútil

Respostas:


37

Raciocínio filosófico muito geral

Normalmente, solicitamos que um construtor forneça (como pós-condições) algumas garantias sobre o estado do objeto construído.

Normalmente, também esperamos que os métodos de instância possam assumir (como pré-condições) que essas garantias já são válidas quando são chamadas e que eles só precisam se certificar de não quebrá-las.

Chamar um método de instância de dentro do construtor significa que algumas ou todas essas garantias ainda não foram estabelecidas, o que dificulta o raciocínio sobre se as pré-condições do método de instância são satisfeitas. Mesmo se você acertar, pode ser muito frágil diante de, por exemplo. reordenar chamadas de método de instância ou outras operações.

Os idiomas também variam na maneira como resolvem chamadas para métodos de instância que são herdados das classes base / substituídos por subclasses, enquanto o construtor ainda está em execução. Isso adiciona outra camada de complexidade.

Exemplos específicos

  1. Seu próprio exemplo de como você acha que isso deve parecer está errado:

    public Cat(int age) {
        //Use the modifier instead of assigning the value directly.
        setAge(age);
    }

    isso não verifica o valor de retorno de setAge. Aparentemente, ligar para o setter não é garantia de correção, afinal.

  2. Erros muito fáceis, como depender da ordem de inicialização, como:

    class Cat {
      private Logger m_log;
      private int m_age;
    
      public void setAge(int age) {
        // FIXME temporary debug logging
        m_log.write("=== DEBUG: setting age ===");
        m_age = age;
      }
    
      public Cat(int age, Logger log) {
        setAge(age);
        m_log = log;
      }
    };

    onde meu registro temporário quebrou tudo. Ops!

Também existem linguagens como C ++, em que chamar um setter do construtor significa uma inicialização padrão desperdiçada (que pelo menos para algumas variáveis ​​de membro vale a pena evitar)

Uma proposta simples

É verdade que a maioria dos códigos não é escrita assim, mas se você deseja manter seu construtor limpo e previsível e ainda reutilizar sua lógica de pré e pós-condição, a melhor solução é:

class Cat {
  private int m_age;
  private static void checkAge(int age) {
    if (age > 25) throw CatTooOldError(age);
  }

  public void setAge(int age) {
    checkAge(age);
    m_age = age;
  }

  public Cat(int age) {
    checkAge(age);
    m_age = age;
  }
};

ou melhor ainda, se possível: codifique a restrição no tipo de propriedade e valide seu próprio valor na atribuição:

class Cat {
  private Constrained<int, 25> m_age;

  public void setAge(int age) {
    m_age = age;
  }

  public Cat(int age) {
    m_age = age;
  }
};

E, finalmente, para completar, um invólucro de auto-validação em C ++. Observe que, embora ainda esteja fazendo a validação tediosa, porque essa classe não faz mais nada , é relativamente fácil verificar

template <typename T, T MAX, T MIN=T{}>
class Constrained {
    T val_;
    static T validate(T v) {
        if (MIN <= v && v <= MAX) return v;
        throw std::runtime_error("oops");
    }
public:
    Constrained() : val_(MIN) {}
    explicit Constrained(T v) : val_(validate(v)) {}

    Constrained& operator= (T v) { val_ = validate(v); return *this; }
    operator T() { return val_; }
};

OK, não está realmente completo, deixei de fora várias cópias e movo construtores e atribuições.


4
Resposta fortemente não atendida! Deixe-me acrescentar apenas porque o OP viu a situação descrita em um livro de texto não a torna uma boa solução. Se houver duas maneiras em uma classe para definir um atributo (por construtor e setter), e se desejar impor uma restrição a esse atributo, todas as maneiras deverão verificar a restrição, caso contrário, é um erro de design .
Doc Brown

Então, você consideraria o uso de métodos setter em uma má prática do construtor se não houver 1) lógica nos setters ou 2) a lógica nos setters é independente do estado? Eu não faço isso com frequência, mas eu pessoalmente tenho usado métodos setter em um construtor exatamente pelo último motivo (quando há algum tipo de condição no setter que sempre deve se aplicar à variável membro
Chris Maggiulli

Se houver algum tipo de condição que sempre deve ser aplicada, isso é lógico no setter. Eu certamente acho que é melhor, se possível, codificar essa lógica no membro, para que ela seja forçada a ser independente e não possa adquirir dependências no restante da classe.
Inútil

13

Quando um objeto está sendo construído, por definição, ele não está totalmente formado. Considere como o levantador que você forneceu:

public boolean setAge(int age) {
    //Validate your parameters, valid age for a cat is between 0 and 25 years
    if(age > 0 && age < 25) {
        this.age = age;
        return true;
    }
    return false;
}

E se parte da validação setAge()incluísse uma verificação na agepropriedade para garantir que o objeto só aumentasse de idade? Essa condição pode se parecer com:

if (age > this.age && age < 25) {
    //...

Parece uma mudança bastante inocente, já que os gatos só podem se mover no tempo em uma direção. O único problema é que você não pode usá-lo para definir o valor inicial de, this.ageporque assume que this.agejá possui um valor válido.

Evitar setters em construtores deixa claro que a única coisa que o construtor está fazendo é definir a variável de instância e ignorar qualquer outro comportamento que ocorra no setter.


OK ... mas, se você realmente não testar a construção do objeto e expor os possíveis erros, existem possíveis erros de qualquer maneira . E agora você tem dois dois problemas: você tem esse bug em potencial oculto e precisa executar verificações de faixa etária em dois lugares.
precisa saber é

Não há problema, pois em Java / C # todos os campos são zerados antes da chamada do construtor.
Deduplicator

2
Sim, chamar métodos de instância de construtores pode ser difícil de raciocinar e geralmente não é o preferido. Agora, se você deseja mover sua lógica de validação para um método particular / estático de classe final e chamá-la do construtor e do setter, tudo bem.
inútil

1
Não, os métodos que não são de instância evitam a complexidade dos métodos de instância, porque não tocam em uma instância parcialmente construída. Obviamente, você ainda precisa verificar o resultado da validação para decidir se a construção foi bem-sucedida.
inútil

1
Esta resposta é IMHO perdendo o ponto. "Quando um objeto está sendo construído, por definição, ele não está totalmente formado" - no meio do processo de construção, é claro, mas quando o construtor é concluído, qualquer restrição pretendida aos atributos deve se manter, caso contrário, é possível contornar essa restrição. Eu chamaria isso de uma falha grave de design.
Doc Brown

6

Algumas boas respostas já estão aqui, mas ninguém até agora parecia ter notado que você está perguntando aqui duas coisas em uma, o que causa alguma confusão. IMHO sua postagem é melhor vista como duas perguntas separadas :

  1. se houver uma validação de uma restrição em um setter, também não deveria estar no construtor da classe para garantir que ela não possa ser ignorada?

  2. por que não chamar o setter diretamente para esse fim do construtor?

A resposta para as primeiras perguntas é claramente "sim". Um construtor, quando não lança uma exceção, deve deixar o objeto em um estado válido e, se certos valores são proibidos para determinados atributos, não faz absolutamente nenhum sentido deixar o ctor contornar isso.

No entanto, a resposta para a segunda pergunta é normalmente "não", desde que você não tome medidas para evitar a substituição do levantador em uma classe derivada. Portanto, a melhor alternativa é implementar a validação de restrição em um método privado, que pode ser reutilizado no setter e no construtor. Não vou aqui repetir o exemplo @Useless, que mostra exatamente esse design.


Ainda não entendo por que é melhor extrair lógica do setter para que o construtor possa usá-la. ... Como isso é realmente diferente do que simplesmente chamar os levantadores em uma ordem razoável? Especialmente considerando que, se seus levantadores são tão simples quanto "deveriam", a única parte do levantador que seu construtor, neste momento, não está chamando é a configuração real do campo subjacente ...
svidgen

2
@svidgen: veja o exemplo do JimmyJames: em suma, não é uma boa ideia usar métodos substituíveis em um ctor, que causarão problemas. Você pode usar o setter no controlador se for final e não chamar nenhum outro método substituível. Além disso, separar a validação da maneira de manipulá-la quando ela falhar oferece a oportunidade de manipulá-la diferentemente no ctor e no setter.
Doc Brown

Bem, estou ainda menos convencido agora! Mas, obrigado por me apontando para a outra resposta ...
svidgen

2
Em uma ordem razoável, você quer dizer com uma dependência de ordem quebradiça e invisível, facilmente quebrada por mudanças de aparência inocente , certo?
Inútil

@ inútil, bem, não ... quero dizer, é engraçado que você sugira que a ordem na qual seu código seja executado não importe. mas, esse não era realmente o ponto. Além de exemplos inventados, não consigo me lembrar de uma instância em minha experiência em que achei uma boa idéia ter validações entre propriedades em um setter - esse tipo de coisa leva necessariamente a requisitos de ordem de chamada "invisível" . Independentemente do facto de essas invocações ocorrer em um construtor ..
svidgen

2

Aqui está um pouco de código Java bobo que demonstra os tipos de problemas com os quais você pode se deparar usando métodos não finais em seu construtor:

import java.util.regex.Pattern;

public class Problem
{
  public static final void main(String... args)
  {
    Problem problem = new Problem("John Smith");
    Problem next = new NextProblem("John Smith", " ");

    System.out.println(problem.getName());
    System.out.println(next.getName());
  }

  private String name;

  Problem(String name)
  {
    setName(name);
  }

  void setName(String name)
  {
    this.name = name;
  }

  String getName()
  {
    return name;
  }
}

class NextProblem extends Problem
{
  private String firstName;
  private String lastName;
  private final String delimiter;

  NextProblem(String name, String delimiter)
  {
    super(name);

    this.delimiter = delimiter;
  }

  void setName(String name)
  {
    String[] parts = name.split(Pattern.quote(delimiter));

    this.firstName = parts[0];
    this.lastName = parts[1];
  }

  String getName()
  {
    return firstName + " " + lastName;
  }
}

Quando executo isso, obtenho um NPE no construtor NextProblem. Este é um exemplo trivial, é claro, mas as coisas podem se complicar rapidamente se você tiver vários níveis de herança.

Eu acho que a razão maior pela qual isso não se tornou comum é que torna o código um pouco mais difícil de entender. Pessoalmente, quase nunca possuo métodos setter e minhas variáveis-membro são quase invariavelmente finais (trocadilhos). Por esse motivo, os valores precisam ser definidos no construtor. Se você usa objetos imutáveis ​​(e há muitas boas razões para fazê-lo), a questão é discutível.

De qualquer forma, é um bom objetivo reutilizar a validação ou outra lógica e você pode colocá-lo em um método estático e invocá-lo do construtor e do setter.


Como isso é um problema de usar setters no construtor, em vez de um problema de herança mal implementada ??? Em outras palavras, se você está efetivamente invalidando o construtor base, por que você o chamaria?
Svidgen

1
Eu acho que a questão é que a substituição de um levantador não deve invalidar o construtor.
21416 Chris Wohlert #

@ChrisWohlert Ok ... mas, se você alterar a lógica do seu configurador para entrar em conflito com a lógica do construtor, é um problema, independentemente de o construtor usar o configurador. Ou falha no momento da construção, falha mais tarde ou falha invisível (porque você ignorou suas regras de negócios).
Svidgen #

Muitas vezes você não sabe como o Construtor da classe base é implementado (pode estar em uma biblioteca de terceiros em algum lugar). A substituição de um método substituível não deve violar o contrato da implementação da base (no entanto, e sem dúvida este exemplo o faz não definindo o namecampo).
Hulk

@svidgen O problema aqui é que a chamada de métodos substituíveis do construtor reduz a utilidade de substituí-los - você não pode usar nenhum campo da classe filho nas versões substituídas, porque eles ainda não podem ser inicializados. O que é pior, você não deve passar uma thisreferência para outro método, porque ainda não pode ter certeza de que ele foi totalmente inicializado.
Hulk

1

Depende!

Se você estiver executando uma validação simples ou emitindo efeitos colaterais impertinentes no setter que precisam ser atingidos toda vez que a propriedade for configurada: use o setter. A alternativa é geralmente extrair a lógica do setter e chamar o novo método seguido pelo conjunto real do setter e do construtor - que é efetivamente o mesmo que usar o setter! (Só que agora você está mantendo seu, reconhecidamente pequeno, levantador de duas linhas em dois lugares.)

No entanto , se você precisar executar operações complexas para organizar o objeto antes que um setter específico (ou dois!) Possa ser executado com êxito, não use o setter.

E, em ambos os casos, tenha casos de teste . Independentemente de qual rota você percorrer, se você tiver testes de unidade, terá uma boa chance de expor as armadilhas associadas a qualquer opção.


Acrescentarei também que, se minha memória de longe é remotamente precisa, esse também é o tipo de raciocínio que aprendi na faculdade. E isso não está de acordo com os projetos bem-sucedidos em que tive oportunidade de trabalhar.

Se eu tivesse que adivinhar, perdi um memorando de "código limpo" em algum momento que identificava alguns erros típicos que podem surgir do uso de setters em um construtor. Mas, da minha parte, não fui vítima de nenhum desses erros ...

Então, eu argumentaria que essa decisão em particular não precisa ser abrangente e dogmática.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.