Devo passar um objeto para um construtor ou instanciar em classe?


10

Considere estes dois exemplos:

Passando um objeto para um construtor

class ExampleA
{
  private $config;
  public function __construct($config)
  {
     $this->config = $config;
  }
}
$config = new Config;
$exampleA = new ExampleA($config);

Instanciando uma classe

class ExampleB
{
  private $config;
  public function __construct()
  {
     $this->config = new Config;
  }
}
$exampleA = new ExampleA();

Qual é a maneira correta de lidar com a adição de um objeto como uma propriedade? Quando devo usar um sobre o outro? O teste de unidade afeta o que devo usar?



9
@FrustratedWithFormsDesigner - isso não é adequado para a Revisão de Código.
ChrisF

Respostas:


14

Eu acho que o primeiro lhe dará a capacidade de criar um configobjeto em outro lugar e passá-lo para ExampleA. Se você precisar de Injeção de Dependência, isso pode ser uma boa coisa, pois você pode garantir que todas as intenções compartilhem o mesmo objeto.

Por outro lado, talvez você ExampleA exija um configobjeto novo e limpo, para que possa haver casos em que o segundo exemplo seja mais apropriado, como casos em que cada instância poderia ter uma configuração diferente.


11
Então, A é bom para testes de unidade, B é bom para outras circunstâncias ... por que não ter os dois? Muitas vezes, tenho dois construtores, um para DI manual e outro para uso normal (eu uso o Unity for DI, que gerará construtores para atender aos seus requisitos de DI).
Ed James

3
@ EdWoodcock não é realmente sobre testes de unidade versus outras circunstâncias. O objeto requer a dependência de fora ou o gerencia por dentro. Nunca os dois também.
precisa saber é o seguinte

9

Não se esqueça da testabilidade!

Geralmente, se o comportamento da Exampleclasse depende da configuração, você deseja testá-la sem salvar / modificar o estado interno da instância da classe (ou seja, você deseja ter testes simples para o caminho feliz e para a configuração incorreta sem modificar a propriedade / membro da Exampleclasse).

Portanto, eu iria com a primeira opção de ExampleA


É por isso que eu mencionei testando - obrigado por acrescentando que :)
Prisoner

5

Se o objeto tiver a responsabilidade de gerenciar o tempo de vida da dependência, não há problema em criar o objeto no construtor (e descartá-lo no destruidor). *

Se o objeto não for responsável por gerenciar o tempo de vida da dependência, ele deve ser passado para o construtor e gerenciado de fora (por exemplo, por um contêiner de IoC).

Nesse caso, não acho que sua ClassA deva assumir a responsabilidade de criar $ config, a menos que também seja responsável por descartá-la ou se a configuração for exclusiva para cada instância da ClassA.

* Para auxiliar na testabilidade, o construtor pode fazer referência a uma classe / método de fábrica para construir a dependência em seu construtor, aumentando a coesão e a testabilidade.

//Object which manages the lifetime of its dependency (C#):
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA()
    {
        this.Config = new Config(); // Tightly coupled to Config class...
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

// Object which does not manage its dependency:
public class ClassA
{
    public Config Config { get; set; }

    public ClassA(Config config) // dependency may be injected...
    {
        this.Config = config;
    }
}

// Object which manages its dependency in a testable way:
public class ClassA : IDisposable
{
    public Config Config { get; private set; }

    public ClassA(IConfigFactory configFactory) // dependency may be mocked...
    {
        this.Config = configFactory.BuildConfig();
    }

    public void Dispose()
    {
        this.Config.Dispose();
    }
}

2

Recentemente, tive essa mesma discussão com nossa equipe de arquitetura e há alguns motivos sutis para fazê-lo de uma maneira ou de outra. Isso se resume principalmente à Injeção de Dependência (como outros observaram) e se você realmente tem ou não controle sobre a criação do objeto que você é novo no construtor.

No seu exemplo, e se a sua classe Config:

a) possui alocação não trivial, como proveniente de um pool ou método de fábrica.

b) pode falhar na alocação. Passá-lo para o construtor evita perfeitamente esse problema.

c) é realmente uma subclasse de Config.

Passar o objeto para o construtor oferece mais flexibilidade.


1

A resposta abaixo está errada, mas vou mantê-la para que outras pessoas aprendam com ela (veja abaixo)

Em ExampleA, você pode usar a mesma Configinstância em várias classes. No entanto, se houver apenas uma Configinstância em todo o aplicativo, considere aplicar o padrão Singleton Configpara evitar ter várias instâncias de Config. E se Configfor um Singleton, você pode fazer o seguinte:

class ExampleA
{
  private $config;
  public function __construct()
  {
     $this->config = Config->getInstance();
  }
}
$exampleA = new ExampleA();

Por ExampleBoutro lado, você sempre terá uma instância separada de Configpara cada instância de ExampleB.

Qual versão você deve aplicar realmente depende de como o aplicativo tratará instâncias de Config:

  • se cada instância de ExampleXdeve ter uma instância separada de Config, vá com ExampleB;
  • se cada instância de ExampleXcompartilhar uma (e apenas uma) instância de Configuse ExampleA with Config Singleton;
  • se instâncias de ExampleXpodem usar diferentes instâncias de Config, permaneça com ExampleA.

Por que converter Configem um Singleton está errado:

Devo admitir que só aprendi sobre o padrão Singleton ontem (lendo o livro Head First de padrões de design). Ingenuamente, eu o apliquei neste exemplo, mas, como muitos apontaram, de uma maneira são outras (algumas foram mais enigmáticas e apenas disseram "Você está fazendo errado!"), Essa não é uma boa ideia. Portanto, para impedir que outras pessoas cometam o mesmo erro que eu cometi, segue um resumo de por que o padrão Singleton pode ser prejudicial (com base nos comentários e no que descobri pesquisando no Google):

  1. Se ExampleArecuperar sua própria referência à Configinstância, as classes serão fortemente acopladas. Não haverá como ExampleAusar uma versão diferente de Config(por exemplo, uma subclasse). Isso é horrível se você deseja testar ExampleAusando uma instância de mock-up, Configpois não há como fornecê-la ExampleA.

  2. A premissa de que haverá uma, e apenas uma, instância de Configtalvez vale agora , mas você nem sempre pode ter certeza de que o mesmo será válido no futuro . Se em algum momento posterior se verificar que várias instâncias de Configserão desejáveis, não há como conseguir isso sem reescrever o código.

  3. Mesmo que a única instância de Configtalvez seja verdadeira por toda a eternidade, pode acontecer que você queira usar algumas subclasses de Config(embora ainda tenha apenas uma instância). Mas, como o código obtém diretamente a instância por meio getInstance()de Config, que é um staticmétodo, não há como obter a subclasse. Novamente, o código deve ser reescrito.

  4. O fato de que os ExampleAusos Configserão ocultados, pelo menos ao visualizar a API de ExampleA. Isso pode ou não ser uma coisa ruim, mas pessoalmente sinto que isso parece uma desvantagem; por exemplo, ao manter, não há uma maneira simples de descobrir quais classes serão afetadas pelas alterações Configsem examinar a implementação de todas as outras classes.

  5. Mesmo que o fato de ExampleAusar um Singleton Config não seja um problema em si, ele ainda pode se tornar um problema do ponto de vista de teste. Os objetos Singleton terão o estado que persistirá até o término do aplicativo. Isso pode ser um problema ao executar testes de unidade, pois você deseja que um teste seja isolado de outro (ou seja, que a execução de um teste não afete o resultado de outro). Para corrigir isso, o objeto Singleton deve ser destruído entre cada execução de teste (possivelmente tendo que reiniciar o aplicativo inteiro), o que pode levar muito tempo (sem mencionar tedioso e irritante).

Dito isto, fico feliz por ter cometido esse erro aqui e não na implementação de um aplicativo real. Na verdade, eu estava pensando em reescrever meu código mais recente para usar o padrão Singleton para algumas das classes. Embora eu pudesse reverter facilmente as alterações (tudo é armazenado em um SVN, é claro), ainda assim eu teria perdido tempo fazendo isso.


4
Eu não recomendaria fazê-lo. Dessa forma, você une firmemente as aulas ExampleAe Config- o que não é uma coisa boa.
Paul

@ Paul: Isso é verdade. Boa captura, não pensei nisso.
gablin

3
Eu sempre recomendaria não usar Singletons por motivos de testabilidade. São essencialmente variáveis ​​globais e é impossível zombar da dependência.
precisa saber é o seguinte

4
Eu sempre recomendaria againts usando Singletons por razões que você fez errado.
Raynos

1

A coisa mais simples a fazer é associar-se ExampleAa Config. Você deve fazer a coisa mais simples, a menos que haja uma razão convincente para fazer algo mais complexo.

Uma razão para dissociar ExampleAe Configseria melhorar a testabilidade do ExampleA. O acoplamento direto degradará a capacidade de teste de ExampleAse Configtiver métodos lentos, complexos ou com rápida evolução. Para o teste, um método é lento se executar mais do que alguns microssegundos. Se todos os métodos de Configsão simples e rápido, então eu iria tomar a abordagem simples e direta casal ExampleApara Config.


1

Seu primeiro exemplo é um exemplo do padrão de injeção de dependência. Uma classe com uma dependência externa recebe a dependência pelo construtor, setter etc.

Essa abordagem resulta em código fracamente acoplado. A maioria das pessoas pensa que o acoplamento flexível é uma coisa boa, porque você pode facilmente substituir a configuração nos casos em que uma instância específica de um objeto precisa ser configurada diferentemente das outras, você pode passar um objeto de configuração simulado para teste e, portanto, em.

A segunda abordagem está mais próxima do padrão do criador do GRASP. Nesse caso, o objeto cria suas próprias dependências. Isso resulta em código fortemente acoplado, isso pode limitar a flexibilidade da classe e dificultar o teste. Se você precisar que uma instância de uma classe tenha uma dependência diferente das outras, sua única opção é subclassificá-la.

Entretanto, pode ser o padrão apropriado nos casos em que a vida útil do objeto dependente é ditada pela vida útil do objeto dependente e em que o objeto dependente não é usado em nenhum lugar fora do objeto que depende dele. Normalmente, eu aconselho o DI a ser a posição padrão, mas você não precisa descartar completamente a outra abordagem, desde que esteja ciente de suas consequências.


0

Se sua classe não expuser as $configclasses externas, eu a criaria dentro do construtor. Dessa forma, você está mantendo seu estado interno privado.

Se os $configrequisitos exigirem que seu próprio estado interno seja definido corretamente (por exemplo, precise de uma conexão com o banco de dados ou alguns campos internos sejam inicializados) antes do uso, faz sentido adiar a inicialização para algum código externo (possivelmente uma classe de fábrica) e injetá-lo o construtor. Ou, como outros já apontaram, se precisar ser compartilhado entre outros objetos.


0

ExampleA é dissociado da classe concreta Config, que é boa , desde que o objeto seja recebido se não for do tipo Config, mas o tipo de uma superclasse abstrata de Config.

ExampleB está fortemente acoplado à classe concreta Config, que é ruim .

Instanciar um objeto cria um forte acoplamento entre classes. Isso deve ser feito em uma classe Factory.

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.