Как вы можете разложить конструктор?

21

Допустим, у меня есть класс Enemy, и конструктор будет выглядеть примерно так:

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

Это выглядит плохо, потому что конструктор имеет так много параметров, но когда я создаю экземпляр Enemy, мне нужно указать все эти вещи. Я также хочу, чтобы эти атрибуты были в классе Enemy, чтобы я мог перебирать их список и получать / устанавливать эти параметры. Я подумал, может быть, подкласс Enemy в EnemyB, EnemyA, в то время как жестко запрограммировал их maxHp и другие специфические атрибуты, но тогда я бы потерял доступ к их жестко закодированным атрибутам, если бы захотел перебрать список Enemy (состоящий из EnemyA, EnemyB и EnemyC ы).

Я просто пытаюсь научиться правильно кодировать. Если это имеет значение, я работаю в Java / C ++ / C #. Любая точка в правильном направлении приветствуется.

Travis
источник
5
Нет ничего плохого в том, чтобы иметь один конструктор, который связывает все атрибуты. Фактически, в некоторых средах постоянства это требуется. Ничто не говорит о том, что у вас не может быть нескольких конструкторов, возможно, с методом проверки правильности, который будет вызван после выполнения кусочной конструкции.
BobDalgleish
1
Мне бы хотелось задать вопрос, собираетесь ли вы когда-нибудь создавать объекты Enemy в коде с использованием литералов. Если вы этого не сделаете, и я не понимаю, почему вы это сделаете, то создайте конструкторы, которые извлекают данные из интерфейса базы данных, или из строки сериализации, или ...
Zan Lynx

Ответы:

58

Решение состоит в том, чтобы объединить параметры в составные типы. Ширина и высота концептуально связаны - они определяют размеры врага и обычно будут необходимы вместе. Они могут быть заменены Dimensionsтипом или, возможно, Rectangleтипом, который также включает позицию. С другой стороны, может быть более целесообразно сгруппировать positionи speedв MovementDataтип, особенно если ускорение позже входит в картину. Из контекста я предполагаю maxHp, что attackDamageи defenseт. Д. Также принадлежат к одному Statsтипу. Таким образом, пересмотренная подпись может выглядеть примерно так:

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

Мелкие детали того, где рисовать линии, будут зависеть от остальной части вашего кода и от того, какие данные обычно используются вместе.

Doval
источник
21
Я также добавил бы, что наличие такого количества значений может указывать на нарушение принципа единой ответственности. А группировка ценностей в конкретные объекты - это первый шаг к разделению этих обязанностей.
Эйфорическое
2
Я не думаю, что список значений является проблемой SRP; большинство из них, вероятно, предназначены для конструкторов базовых классов. Каждый класс в иерархии может нести одну ответственность. Enemyэто просто класс, который нацелен на Player, но их общий базовый класс Combatantнуждается в статистике боя.
MSalters
@MSalters Это не обязательно указывает на проблему SRP, но это может. Если ему нужно сделать достаточное перемалывание чисел, эти функции могут попасть в класс Enemy, когда они должны быть статическими / свободными функциями (если он использует Dimensions/ MovementDataкак простые старые контейнеры данных) или методами (если он превращает их в абстрактные данные). типы / объекты). Например, если бы он еще не создал Vector2тип, он мог бы в итоге заняться векторной математикой Enemy.
Довал
24

Возможно, вы захотите взглянуть на шаблон Builder . По ссылке (с примерами шаблонов и альтернатив):

Шаблон The Builder - хороший выбор при разработке классов, у конструкторов или статических фабрик которых будет больше, чем несколько параметров, особенно если большинство этих параметров являются необязательными. Клиентский код намного проще для чтения и записи со сборщиками, чем с традиционным шаблоном телескопического конструктора, и сборщики намного безопаснее, чем JavaBeans.

Рори Хантер
источник
4
Небольшой фрагмент кода был бы полезен. Это отличный шаблон для построения сложных объектов или структур с различными входами. Вы также можете специализировать компоновщики, такие как EnemyABuilder, EnemyBBuilder и т. Д., Которые инкапсулируют различные общие свойства. Это своего рода оборотная сторона фабричного паттерна (как показано ниже), но я лично предпочитаю Builder.
Роб
1
Спасибо, и шаблоны Builder, и шаблоны Factory выглядят так, как будто бы они хорошо работают с тем, что я пытаюсь сделать в целом. Я думаю, что сочетание «Строитель / Фабрика» и предложения Довала может быть тем, что я ищу. Изменить: я думаю, я могу отметить только один ответ; Я передам его Довалу, так как он отвечает на вопрос по теме, но остальные также полезны для моей конкретной проблемы. Спасибо вам всем.
Трэвис
Я думаю, что стоит отметить, что если ваш язык поддерживает фантомные типы, то вы можете написать шаблон компоновщика, который обеспечивает вызов некоторых / всех функций SetX. Это также позволяет гарантировать, что они будут вызваны только один раз (при желании).
Томас Эдинг
1
@ Mark16 Как упоминалось в ссылке, > шаблон Builder имитирует именованные необязательные параметры, которые можно найти в Ada и Python. Вы упомянули, что вы также используете C # в этом вопросе, и этот язык поддерживает именованные / необязательные аргументы (начиная с C # 4.0), так что это может быть другим вариантом.
Боб
5

Использование подклассов для предварительной установки некоторых значений нежелательно. Только подкласс, когда новый тип врага имеет другое поведение или новые атрибуты.

Шаблон завода обычно используются , чтобы абстрагироваться над точным используемым классом, но он также может быть использован , чтобы обеспечить шаблоны для создания объекта:

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", ...);
Амон
источник
0

Я бы зарезервировал подклассы для классов, которые представляют объект, который вы, возможно, захотите использовать независимо, например, класс символов, где все символы, а не только враги имеют имя, скорость, maxHp или класс для представления спрайтов, присутствующих на экране с шириной, высота, положение.

Я не вижу ничего плохого по своей сути в конструкторе с большим количеством входных параметров, но если вы хотите немного разбить его на части, у вас может быть один конструктор, который устанавливает большинство параметров, и другой (перегруженный) конструктор, который можно использовать установить конкретные и другие установить значения по умолчанию.

В зависимости от того, какой язык вы выберете, некоторые могут установить значения по умолчанию для входных параметров вашего конструктора, например:

Enemy(float height = 42, float width = 42);
Encaitar
источник
0

Пример кода для добавления к ответу Рори Хантера (на 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;
       }
   }
}

Теперь вы можете создавать новые экземпляры Enemy следующим образом:

Enemy myEnemy = new Enemy.Builder().withName("John").withX(x).build();
Toon Borgers
источник
1
Программисты это тур концептуальные вопросы и ответы , как ожидается, объяснить вещи . Создание дампов кода вместо объяснения похоже на копирование кода из IDE на доску: это может показаться знакомым и даже иногда понятным, но это кажется странным ... просто странным. У доски нет компилятора
gnat