Separação de construção e inicialização

5

Estou confuso com este post de Mark Seeman.

E o comentário dele sobre IInitializable abaixo:

The problem with an Initialize method is the same as with Property Injection (A.K.A. Setter Injection): it creates a temporal coupling between the Initialize method and all other members of the class. Unless you truly can invoke any other member of the class without first invoking the Initialize method, such API design is deceitful and will lead to run-time exceptions. It also becomes much harder to ensure that the object is always in a consistent state.

Na mesma hora em que ele escreve:

This issue is similar to the issue of invoking virtual members from the constructor. Conceptually, an injected dependency is equivalent to a virtual member.

Eu acho que essa afirmação só é verdadeira se admitir que foi construído! = inicializado.

O que temos agora:
Dependências são injetadas no construtor, mas não é recomendado usá-las.
A fase de inicialização traz complexidade e deve ser evitada.

Não é contraditório?

Imagine que a classe precisa definir seu estado usando as dependências fornecidas. Carregando a configuração salva por exemplo.
A inicialização é ruim, o construtor é ruim, então onde realizar esta operação?

E outro ponto:
Não são métodos como Connection.Open () apenas outro nome para Initialize?

Pergunta:
Então, alguém pode descrever um bom padrão de inicialização no contexto da Injeção de Dependência que aborda as preocupações levantadas por Mark Seeman?

    
por Pavel Voronin 25.07.2013 / 23:49
fonte

4 respostas

4

O método Initialize dedicado é ruim - se você usar isso, você deve construir um objeto e depois não usá-lo até que tenha chamado Init, e sempre destrua se a chamada de inicialização falhar. É uma bagunça de inicialização que é muito melhor tratada no construtor.

Se você retornar apenas um objeto construído com sucesso que contenha tudo de que precisa para começar a trabalhar, terá muito mais facilidade como programador usando essa classe.

Da mesma forma, não deve haver um problema se a dependência injetada for resolvida durante a construção.

No entanto, isso significa definir o objeto DI durante a construção - 'injeção de construtor', e suponho que ele esteja falando sobre 'injeção de propriedade' onde a configuração é passada como um conjunto de chamadas de propriedades depois que o objeto é construído. Esse é o mesmo problema em que você tinha um método Init, mas agora você tem um método SetConfigX. Os nomes são diferentes, obviamente, mas o princípio é o mesmo - você acaba com um objeto semi-construído que você então preenche com o resto do seu estado antes que ele possa ser usado.

    
por 26.07.2013 / 14:20
fonte
3

I thinks this statement is true only if admit that constructed != initialized.

Você está perdendo o ponto que parece. Chamar métodos virtuais do construtor implicitamente significa que o objeto ainda não está totalmente construído (e inicializado). Os conceitos são os mesmos. Você não pode chamar métodos virtuais até que todos os construtores tenham sido executados (o objeto está totalmente inicializado). Da mesma forma, você não pode chamar métodos que usam dependências injetadas até que sejam preenchidos (o objeto está totalmente inicializado).

Dependecnies are injected in constructor but it is not recommended to use them.

Não é isso que o artigo está dizendo. Está dizendo que o construtor deve estar limitado a aceitar dependências, não procurá-las ou configurá-las, ou realmente qualquer outra coisa. Também diz que a injeção de construtor é insuficiente para algumas necessidades (dependências circulares) e inábil em outras (onde a definição de construtor força grandes hierarquias de herança).

Eu não quero colocar palavras na boca do autor do artigo, mas eu recomendaria usar injeção de construtor até que você encontre uma boa razão para não . É o mais fácil de implementar. É o mais fácil de depurar. É o mais fácil de ler.

So can anyone describe a good initialization pattern in the context of Dependency Injection that addresses the concerns Mark Seeman brings up?

Pessoalmente, gosto de fábricas que geram um objeto (ou conjunto de objetos) com todas as suas dependências preenchidas. Isso limita os locais onde o cuidado precisa ser levado aos construtores de seus componentes e às próprias fábricas. Se as dependências não forem encontradas, você receberá um erro imediatamente. Se você pegar o (s) objeto (s) de volta, você sabe que eles estão em bom estado.

Não é algo que possa ser aplicado (bem) a todos os problemas, mas, na minha experiência, pode ser aplicado à maioria dos problemas e isolar um pouco a complexidade.

    
por 26.07.2013 / 15:02
fonte
0

Dependencies are injected in constructor but it is not recommended to use them.

Acho que o autor está dizendo o oposto disso com foco no acoplamento fraco. Ele citou dizendo:

"The Constructor Injection design pattern is a extremely useful way to implement loose coupling"

O acoplamento frouxo é o núcleo do bom design de software. Torna as classes reutilizáveis, extensíveis e testáveis.

Initialize phase brings complexity and should be avoided.

Sim, mas isso não significa que sempre possa ser evitado. Os construtores não têm um valor de retorno e nem todos os idiomas suportam o lançamento de exceções. Alguns livros argumentam que um construtor deve ser sempre bem-sucedido e eu concordo com essa regra.

Aqui estão alguns exemplos de código fonte de ambos os estilos. Ambas as abordagens funcionam, mas qual é a melhor?

try
{
    FileReader f = new FileReader("something.txt");
    String str = f.read();
}
catch(FileNotFound e) {...}

ou

FileReader f = new FileReader("something.txt");
if(f.exists())
{
    String str = f.read();
}

O primeiro exemplo tem o objeto FileReader dependente de um recurso externo. Se não puder acessá-lo, ele falhará durante o construtor. Isso cria dependência além do controlador do programador e também teste de unidade.

No segundo exemplo, não há dependência. O objeto pode ser criado mesmo se o recurso não existir. A dependência agora é de responsabilidade do programador.

Então, como isso se relaciona com initializing de um objeto. É muito simples. Quem deve ser responsável? O objeto ou o programador. Isso é com o autor do objeto. Ele / ela pode ter boas razões para desistir do controle da construção do objeto para o programador que o utiliza.

Se initializing de um objeto for uma tarefa complexa, você localizará esse código em uma classe de fábrica para ter um local para fazer alterações.

Are not methods like Connection.Open() just another name for Initialize?

O método Connection.Open() é um inicializador apenas se Connection.Read() falhar se Open() não for chamado.

Aqui está o problema que o autor está falando.

Connection con = new Connection();
con->Read(); // this will fail, Open() was not called

Para corrigir o código acima. Você tem que escrever isso, e isso é um projeto ruim.

Connection con = new Connection();
con->Open("192.168.1.1");  // bug fix, forgot to call Open()
con->Read();

Eu li muitos comentários no código-fonte de programadores que escrevem "correção de bug, esqueci de chamar X (...)". O argumento é que o bug era evitável em primeiro lugar. O autor da classe Connection não usou um inicializador.

Aqui está a solução para o problema.

Connection con = new Connection("192.168.1.1");
con->Read();

Agora, como você lida com uma conexão com falha é respondido mais acima na minha resposta. O construtor lança uma exceção ou o programador deve chamar isOpen() antes de read() .

So can anyone describe a good initialization pattern in the context of Dependency Injection that addresses the concerns Mark Seeman brings up?

Pode ser difícil de entender, mas a resposta está no Single Responsibility Principle .

Para o meu exemplo com o objeto Connection . Ele quebrou a regra do SRP. O objeto Connection é aberto e lido a partir do recurso. São duas responsabilidades diferentes. Podemos consertar isso fragmentando o objeto em várias partes, cada uma com suas próprias responsabilidades.

Aqui está um exemplo;

try
{
    SocketAddress addr = new SocketAddress("192.168.1.1");
    try
    {
        Socket s = new Socket(addr);
        try
        {
            SocketReader r = new SocketReader(s);
            if(r != null)
            {
                String str = r->Read();
            }
        } catch(ReadFailure e) {..}
    } catch(ConnectionFailure e) {..}
} catch(BadAddress e) {..}

Cada objeto é responsável apenas por uma coisa.

  • SocketAddress só será construído com sucesso se o endereço for válido.
  • Socket só será construído se puder estabelecer uma conexão com o endereço.
  • SocketReader só funciona se puder ler.

Como você pode ver. Você tem que escrever muito mais código-fonte, e é por isso que muitas vezes vemos a injeção de dependência evitada. É um trabalho extra por parte do programador.

    
por 26.07.2013 / 16:39
fonte
0

Para flexibilidade, NÃO faça inicialização em construtores.

Por uma questão de simplicidade, isto é, se você puder pagar e tiver certeza de que não causará dores de cabeça mais tarde, inicialização de OD em construtores (RAII). Então você pode fazer as suposições RAII seguras usuais sobre desalocações (monolíticas?).

Toda a programação depende da ordem correta dos eventos. Não há nada " enganoso " sobre colocar as expectativas do programador para executar certas operações antes de certas outras operações (neste caso , construindo - > inicializando todos os membros - > usando o objeto). A idéia de Seeman de "engano" é patentemente absurda.

Eu tenho a tendência de favorecer a felxibilidade (a primeira) nos dias de hoje, já que tenho sido queimada muitas vezes, precisando da flexibilidade que as injeções / inicializações individuais dos membros trazem. (A propósito, eu prefiro C sobre C ++ pela mesma razão: flexibilidade.). A injeção de construtor é RAII. RAII admite (em seu próprio nome) ser uma mistura de preocupações. Como resultado, os objetos não podem entrar em um pool para serem reconfigurados / reutilizados, eles precisam ser reconstruídos, o que significa alocações de tempo de execução adicionais. Há uma falta de qualquer controle real sobre a ordem das instruções (preocupação com a DI dinâmica). E, claro, há as longas e feias listas de parâmetros.

    
por 13.12.2014 / 17:04
fonte