Como você pode decompor um construtor?

21

Digamos que eu tenho uma classe Enemy, e o construtor seria algo como:

public Enemy(String name, float width, float height, Vector2 position, 
             float speed, int maxHp, int attackDamage, int defense... etc.){}

Isso parece ruim porque o construtor tem muitos parâmetros, mas quando eu crio uma instância Enemy, preciso especificar todas essas coisas. Também quero esses atributos na classe Enemy, para que eu possa percorrer uma lista deles e obter / definir esses parâmetros. Eu estava pensando em talvez subclassificar o Inimigo no EnemyB, EnemyA, enquanto codificava seu maxHp e outros atributos específicos, mas então eu perderia o acesso aos seus atributos codificados se quisesse percorrer uma lista de Enemy (consistindo em EnemyA, EnemyB e Do InemyC).

Eu só estou tentando aprender a codificar de forma limpa. Se isso faz diferença, eu trabalho em Java / C ++ / C #. Qualquer ponto na direção certa é apreciado.

Travis
fonte
5
Não há nada ruim em ter um construtor que vincule todos os atributos. De fato, em alguns ambientes de persistência, é necessário. Nada diz que você não pode ter vários construtores, talvez com o método de verificação de validade a ser chamado depois de fazer a construção em partes.
BobDalgleish
1
Eu teria que questionar se você pretende construir objetos Enemy no código usando literais. Se não o fizer, e eu não vejo por que você iria, em seguida, construir construtores que puxam os dados a partir de uma interface de banco de dados, ou uma seqüência de serialização, ou ...
Zan Lynx

Respostas:

58

A solução é agrupar os parâmetros em tipos compostos. Largura e Altura estão conceitualmente relacionados - eles especificam as dimensões do inimigo e geralmente serão necessários juntos. Eles podem ser substituídos por um Dimensionstipo, ou talvez um Rectangletipo que também inclua a posição. Por outro lado, pode fazer mais sentido agrupar positione formar speedum MovementDatatipo, especialmente se a aceleração mais tarde entrar em cena. A partir do contexto eu assumo maxHp, attackDamage, defense, etc também pertencem juntos em um Statstipo. Portanto, uma assinatura revisada pode ser algo como isto:

public Enemy(String name, Dimensions dimensions, MovementData movementData, Stats stats)

Os detalhes de onde desenhar as linhas dependerão do restante do seu código e de quais dados são comumente usados ​​juntos.

Doval
fonte
21
Eu também acrescentaria que ter tantos valores pode indicar violação do Princípio da Responsabilidade Única. E agrupar valores em objetos específicos é o primeiro passo para separar essas responsabilidades.
Euphoric
2
Não acho que a lista de valores seja um problema do SRP; a maioria deles provavelmente é destinada a construtores da classe base. Cada classe na hierarquia pode ter uma única responsabilidade. Enemyé apenas a classe que tem como alvo o Player, mas sua classe base comum Combatantprecisa das estatísticas de luta.
MSalters
@MSalters Não indica necessariamente um problema no SRP, mas pode. Se ele precisar fazer trituração de número suficiente, essas funções podem ser inseridas na classe Enemy quando devem ser funções estáticas / livres (se ele usa Dimensions/ MovementDatacomo simples contêineres de dados antigos) ou métodos (se ele as transformar em dados abstratos tipos / objetos). Como exemplo, se ele ainda não tivesse criado um Vector2tipo, ele poderia ter acabado fazendo matemática vetorial Enemy.
Doval
24

Você pode dar uma olhada no padrão Builder . No link (com exemplos do padrão versus alternativas):

[O] padrão Builder é uma boa opção ao projetar classes cujos construtores ou fábricas estáticas teriam mais do que um punhado de parâmetros, especialmente se a maioria desses parâmetros for opcional. O código do cliente é muito mais fácil de ler e escrever com os construtores do que com o padrão tradicional de construtores telescópicos, e os construtores são muito mais seguros que o JavaBeans.

Rory Hunter
fonte
4
Um trecho de código curto seria útil. Esse é um ótimo padrão para criar objetos ou estruturas complicadas com várias entradas. Você também pode especializar os construtores, como EnemyABuilder, EnemyBBuilder, etc. que encapsulam as várias propriedades compartilhadas. Esse é o outro lado do padrão de fábrica (conforme respondido abaixo), mas minha preferência pessoal é pelo Builder.
Rob
1
Obrigado, os padrões Builder e Factory parecem funcionar bem com o que estou tentando fazer em geral. Acho que uma combinação da sugestão do Builder / Factory e Doval pode ser o que estou procurando. Edit: Eu acho que só posso marcar uma resposta; Vou dar a Doval, uma vez que responde à pergunta do tópico, mas os outros são igualmente úteis para o meu problema específico. Obrigado a todos.
Travis
Eu acho que vale a pena notar que, se o seu idioma suportar tipos fantasmas, você poderá escrever um padrão de construtor que imponha que algumas / todas as funções do SetX sejam chamadas. Ele também permite garantir que eles sejam chamados apenas uma vez também (se desejado).
Thomas Eding
1
@ Mark16 Conforme mencionado no link, > O padrão Builder simula parâmetros opcionais nomeados, conforme encontrado em Ada e Python. Você mencionou que também usa C # na pergunta e esse idioma suporta argumentos nomeados / opcionais (a partir do C # 4.0), portanto essa pode ser outra opção.
Bob
5

Usar subclasses para predefinir alguns valores não é desejável. Apenas subclasse quando um novo tipo de inimigo tem comportamento diferente ou novos atributos.

O padrão de fábrica geralmente é usado para abstrair sobre a classe exata usada, mas também pode ser usado para fornecer modelos para a criação de objetos:

class EnemyFactory {

    // each of these methods is essentially a template for a kind of enemy

    Enemy enemyA(String name, ...) {
        return new Enemy(name, ..., presetValue, ...);
    }

    Enemy enemyB(String name, ...) {
        return new Enemy(name, ..., otherValue, ...);
    }

    Enemy enemyC(String name, ...) {
        return new EnemySubclass(name, ..., otherValue, ...);
    }

    ...
}

EnemyFactory factory = new EnemyFactory();
Enemy a = factory.enemyA("fred", ...);
Enemy b = factory.enemyB("willy", ...);
amon
fonte
0

Eu reservaria a subclassificação para classes que representam objetos que você pode usar independentemente, por exemplo, classe de caracteres em que todos os caracteres, não apenas os inimigos, têm nome, velocidade, maxHp ou uma classe para representar sprites que estão presentes na tela com largura, altura, posição.

Não vejo nada inerentemente errado com um construtor com muitos parâmetros de entrada, mas se você quiser dividi-lo um pouco, pode haver um construtor que configure a maioria dos parâmetros e outro construtor (sobrecarregado) que pode ser usado para definir os específicos e definir outros com os valores padrão.

Dependendo do idioma que você optar por usar, alguns podem definir valores padrão para os parâmetros de entrada do seu construtor, como:

Enemy(float height = 42, float width = 42);
Encaitar
fonte
0

Um exemplo de código a ser adicionado à resposta de Rory Hunter (em Java):

public class Enemy{
   private String name;
   private float width;
   ...

   public static class Builder{
       private Enemy instance;

       public Builder(){
           this.instance = new Enemy();
       }


       public Builder withName(String name){
           instance.name = name;
           return this;
       }

       ...

       public Enemy build(){
           return instance;
       }
   }
}

Agora, você pode criar novas instâncias do Enemy como esta:

Enemy myEnemy = new Enemy.Builder().withName("John").withX(x).build();
Toon Borgers
fonte
1
Programadores é turísticos questões conceituais e respostas são esperados para explicar as coisas . Jogar dumps de código em vez de explicação é como copiar código do IDE para o quadro branco: pode parecer familiar e até às vezes compreensível, mas parece estranho ... apenas estranho. O quadro branco não tem compilador #
306