Por que devo usar métodos separados de inicialização e limpeza em vez de colocar lógica no construtor e destruidor dos componentes do mecanismo?

9

Estou trabalhando no meu próprio mecanismo de jogo e atualmente estou projetando meus gerentes. Eu li que para gerenciamento de memória, usar Init()e CleanUp()funções é melhor do que usar construtores e destruidores.

Eu tenho procurado exemplos de código C ++, para ver como essas funções funcionam e como eu posso implementá-las no meu mecanismo. Como funciona Init()e CleanUp()trabalho, e como posso implementá-las em meu motor?

Friso
fonte
Para C ++, consulte stackoverflow.com/questions/3786853/… Os principais motivos para usar o Init () são 1) Evitar exceções e falhas no construtor com funções auxiliares 2) Ser capaz de usar métodos virtuais da classe derivada 3) Contornar dependências circulares 4) como método particular para a duplicação do código evitar
brita_

Respostas:

12

É bem simples, na verdade:

Em vez de ter um construtor que faz sua configuração,

// c-family pseudo-code
public class Thing {
    public Thing (a, b, c, d) { this.x = a; this.y = b; /* ... */ }
}

... peça ao seu construtor que faça pouco ou nada e escreva um método chamado .initor .initialize, que faria o que seu construtor faria normalmente.

public class Thing {
    public Thing () {}
    public void initialize (a, b, c, d) {
        this.x = a; /*...*/
    }
}

Então agora, em vez de apenas ir como:

Thing thing = new Thing(1, 2, 3, 4);

Você pode ir:

Thing thing = new Thing();

thing.doSomething();
thing.bind_events(evt_1, evt_2);
thing.initialize(1, 2, 3, 4);

O benefício é que agora você pode usar injeção de dependência / inversão de controle com mais facilidade em seus sistemas.

Ao invés de dizer

public class Soldier {
    private Weapon weapon;

    public Soldier (name, x, y) {
        this.weapon = new Weapon();
    }
}

Você pode construir o soldado, dar a ele um método de equipar, onde você entrega uma arma para ele, e ENTÃO chama todas as demais funções do construtor.

Então agora, em vez de subclassificar inimigos em que um soldado tem uma pistola e outro tem um rifle e outro tem uma espingarda, e essa é a única diferença, você pode apenas dizer:

Soldier soldier1 = new Soldier(),
        soldier2 = new Soldier(),
        soldier3 = new Soldier();

soldier1.equip(new Pistol());
soldier2.equip(new Rifle());
soldier3.equip(new Shotgun());

soldier1.initialize("Bob",  32,  48);
soldier2.initialize("Doug", 57, 200);
soldier3.initialize("Mike", 92,  30);

O mesmo acordo com a destruição. Se você tiver necessidades especiais (remover ouvintes de eventos, remover instâncias de matrizes / quaisquer estruturas com as quais estiver trabalhando, etc.), você as chamaria manualmente, para saber exatamente quando e onde no programa que estava acontecendo.

EDITAR


Como Kryotan apontou, abaixo, isso responde às perguntas originais "Como" , mas na verdade não faz um bom trabalho com o "Por que".

Como você provavelmente pode ver na resposta acima, pode não haver muita diferença entre:

var myObj = new Object();
myObj.setPrecondition(1);
myObj.setOtherPrecondition(2);
myObj.init();

e escrever

var myObj = new Object(1,2);

enquanto apenas tendo uma função construtora maior.
Há um argumento a ser feito para objetos com 15 ou 20 pré-condições, o que tornaria um construtor muito, muito difícil de trabalhar, e facilitaria as coisas para ver e lembrar, puxando essas coisas para a interface , para que você possa ver como a instanciação funciona, um nível acima.

A configuração opcional de objetos é uma extensão natural disso; opcionalmente, definindo valores na interface, antes de executar o objeto.
O JS possui alguns ótimos atalhos para essa idéia, que parecem deslocados em linguagens do tipo c de tipo mais forte.

Dito isso, as chances são de que, se você estiver lidando com uma lista de argumentos tão longa em seu construtor, que seu objeto seja muito grande e faça muito, como é. Novamente, isso é uma coisa de preferência pessoal, e há exceções amplamente, mas se você estiver passando 20 coisas para um objeto, é provável que você encontre uma maneira de fazer com que esse objeto faça menos, criando objetos menores .

Um motivo mais pertinente e amplamente aplicável seria o fato de a inicialização de um objeto depender de dados assíncronos, que você não possui atualmente.

Você sabe que precisa do objeto e, portanto, irá criá-lo de qualquer maneira, mas para que ele funcione corretamente, ele precisa de dados do servidor ou de outro arquivo que agora precisa carregar.

Novamente, se você está passando os dados necessários para um init gigantesco ou construindo uma interface não é realmente importante para o conceito, é importante para a interface do seu objeto e o design do seu sistema ...

Mas em termos de construção do objeto, você pode fazer algo assim:

var obj_w_async_dependencies = new Object();
async_loader.load(obj_w_async_dependencies.async_data, obj_w_async_dependencies);

async_loader pode receber um nome de arquivo ou um nome de recurso ou qualquer outra coisa, carregar esse recurso - talvez carregue arquivos de som ou dados de imagem ou carregue estatísticas de caracteres salvas ...

... e então alimentaria esses dados novamente obj_w_async_dependencies.init(result);.

Esse tipo de dinâmica é encontrado com frequência em aplicativos da web.
Não necessariamente na construção de um objeto, para aplicativos de nível superior: por exemplo, as galerias podem carregar e inicializar imediatamente e, em seguida, exibir fotos à medida que entram - isso não é realmente uma inicialização assíncrona, mas onde é visto com mais frequência seria nas bibliotecas JavaScript.

Um módulo pode depender de outro e, portanto, a inicialização desse módulo pode ser adiada até que o carregamento dos dependentes seja concluído.

Em termos de instâncias específicas do jogo, considere uma Gameclasse real .

Por que não podemos chamar .startou .runno construtor?
Os recursos precisam ser carregados - o resto de tudo já foi definido e está pronto, mas se tentarmos executar o jogo sem uma conexão com o banco de dados, ou sem texturas, modelos, sons ou níveis, não será possível. um jogo particularmente interessante ...

... então qual é a diferença entre o que vemos de um típico Game, exceto pelo fato de atribuirmos ao método "ir em frente" um nome mais interessante do que .init(ou, inversamente, separar ainda mais a inicialização, para separar o carregamento, configurar as coisas que foram carregadas e executar o programa quando tudo estiver configurado).

Norguard
fonte
2
" você os chamaria manualmente, para saber exatamente quando e onde no programa que estava acontecendo. " O único momento em C ++ em que um destruidor seria chamado implicitamente é para um objeto de pilha (ou global). Objetos alocados de heap requerem destruição explícita. Portanto, sempre fica claro quando o objeto está sendo desalocado.
Nicol Bolas
6
Não é exatamente preciso dizer que você precisa desse método separado para permitir a injeção de diferentes tipos de armas ou que essa é a única maneira de evitar a proliferação de subclasses. Você pode passar as instâncias de armas através do construtor! Portanto, é -1 para mim, pois esse não é um caso de uso atraente.
Kylotan
11
-1 De mim também, pelas mesmas razões que Kylotan. Você não faz um argumento muito convincente, tudo isso poderia ter sido feito com construtores.
Paul Manta
Sim, isso poderia ser realizado com construtores e destruidores. Ele pediu casos de uso de uma técnica e por que e como, em vez de como eles funcionam ou por que funcionam. Ter um sistema baseado em componente em que você tem métodos de configuração / ligação, versus os parâmetros passados ​​pelo construtor para o DI, tudo se resume a como você deseja criar sua interface. Mas se o seu objeto requer 20 componentes IOC, você deseja colocar TODOS eles em seu construtor? Você pode? Claro que você pode. Você deveria? Talvez talvez não. Se você optar por não fazê-lo, precisará de um .init, talvez não, mas provavelmente. Ergo, caso válido.
Norguard
11
@Kylotan Na verdade, editei o título da pergunta para perguntar por quê. O OP apenas perguntou "como". Estendi a pergunta para incluir o "por que", como o "como", é trivial para quem sabe alguma coisa sobre programação ("Apenas mova a lógica que você teria para o ctor em uma função separada e chame-a") e o "por que" é mais interessante / geral.
Tetrad 31/01
17

Tudo o que você leu e disse que Init e CleanUp são melhores, também deveria ter lhe explicado o porquê. Artigos que não justificam suas reivindicações não valem a pena ser lidos.

Ter funções separadas de inicialização e desligamento pode facilitar a configuração e a destruição de sistemas, pois você pode escolher em que ordem chamá-los, enquanto os construtores são chamados exatamente quando o objeto é criado e os destruidores chamados quando o objeto é destruído. Quando você tem dependências complexas entre dois objetos, geralmente é necessário que os dois existam antes que eles se estabeleçam - mas isso geralmente é um sinal de mau design em outro lugar.

Alguns idiomas não têm destruidores nos quais você pode confiar, pois a contagem de referências e a coleta de lixo tornam mais difícil saber quando o objeto será destruído. Nessas linguagens, você quase sempre precisa de um método de desligamento / limpeza, e alguns gostam de adicionar o método init para simetria.

Kylotan
fonte
Obrigado, mas estou procurando principalmente exemplos, pois o artigo não os contém. Peço desculpas se minha pergunta não foi esclarecida sobre isso, mas eu a editei agora.
Friso
3

Eu acho que a melhor razão é: permitir o pool.
se você possui Init e CleanUp, quando um objeto é morto, basta chamar CleanUp e enviar o objeto para uma pilha de objetos do mesmo tipo: um 'pool'.
Então, sempre que você precisar de um novo objeto, poderá popular um objeto do pool OU se o pool estiver vazio - muito ruim - você precisará criar um novo. Então você chama Init neste objeto.
Uma boa estratégia é preencher previamente o pool antes do jogo começar com um número 'bom' de objetos, para que você nunca precise criar nenhum objeto em pool durante o jogo.
Se, por outro lado, você usar 'novo' e parar de fazer referência a um objeto quando ele não for útil para você, você criará um lixo que deve ser recuperado em algum momento. Essa lembrança é especialmente ruim para linguagens de thread único como Javascript, onde o coletor de lixo para todo o código quando avalia que precisa recuperar a memória dos objetos que não estão mais em uso. O jogo trava por alguns milissegundos e a experiência de jogo é estragada.
- Você já entendeu -: se você agrupar todos os seus objetos, nenhuma lembrança acontece, portanto, não há mais lentidão aleatória.

Também é muito mais rápido chamar init em um objeto proveniente do pool do que alocar memória + init para um novo objeto.
Mas a melhoria da velocidade tem menos importância, já que muitas vezes a criação de objetos não é um gargalo de desempenho ... Com algumas exceções, como jogos frenéticos, mecanismos de partículas ou mecanismo físico usando intensamente vetores 2D / 3D para seus cálculos. Aqui, a velocidade e a criação de lixo são bastante aprimoradas usando um pool.

Rq: talvez você não precise ter um método CleanUp para seus objetos em pool se o Init () redefinir tudo.

Edit: responder a este post me motivou a finalizar um pequeno artigo que fiz sobre o pool em Javascript .
Você pode encontrá-lo aqui se estiver interessado:
http://gamealchemist.wordpress.com/

GameAlchemist
fonte
11
-1: você não precisa fazer isso apenas para ter um pool de objetos. Você pode fazer isso apenas separando a alocação da construção através do posicionamento novo e da desalocação da exclusão por uma chamada explícita do destruidor. Portanto, esse não é um motivo válido para separar construtores / destruidores de algum método inicializador.
Nicol Bolas
o canal new é específico para C ++ e também um pouco esotérico.
Kylotan
+1, pode ser possível fazer isso de outra maneira em c +. Mas não em outros idiomas ... e essa é provavelmente a única razão pela qual eu usaria o método Init em objetos de jogo.
Kikaimaru
11
@ Nicol Bolas: eu acho que você está exagerando. O fato de existirem outras maneiras de fazer pool (você menciona uma complexa, específica para C ++) não invalida o fato de que usar uma Init separada é uma maneira agradável e simples de implementar o pool em várias linguagens. minhas preferências vão, no GameDev, a respostas mais genéricas.
GameAlchemist
@VincentPiel: Como está usando o posicionamento novo e tão "complexo" em C ++? Além disso, se você estiver trabalhando em um idioma de GC, é bom que os objetos contenham objetos baseados em GC. Então eles também terão que pesquisar cada um deles? Assim, a criação de um novo objeto envolverá a obtenção de vários novos objetos de conjuntos.
Nicol Bolas
0

Sua pergunta é revertida ... Historicamente falando, a pergunta mais pertinente é:

Por que é de construção + intialisation conflated , ou seja, por que não fazemos estas etapas separadamente? Certamente isso vai contra o SoC ?

Para C ++, a intenção da RAII é que a aquisição e liberação de recursos sejam vinculadas diretamente ao tempo de vida do objeto, na esperança de que isso garanta a liberação de recursos. Faz isso? Parcialmente. Ele é 100% cumprido no contexto de variáveis ​​automáticas / baseadas em pilha, onde sair do escopo associado automaticamente chama destruidores / libera essas variáveis ​​(daí o qualificador automatic). No entanto, para variáveis ​​de heap, esse padrão muito útil se desintegra, pois você ainda é obrigado a ligar explicitamente deletepara executar o destruidor e, se você esquecer de fazê-lo, ainda será mordido pelo que o RAII tenta resolver; no contexto de variáveis ​​alocadas por heap, o C ++ fornece benefícios limitados sobre o C (delete vsfree()), confundindo a construção com a inicialização, o que afeta negativamente em termos do seguinte:

A criação de um sistema de objetos para jogos / simulações em C é altamente recomendável, pois esclarecerá bastante as limitações do RAII e outros padrões centrados em OO, através de uma compreensão mais profunda das suposições que C ++ e as linguagens clássicas de OO mais recentes fazem. (lembre-se de que o C ++ começou como um sistema OO integrado em C).

Engenheiro
fonte