如何分解构造函数?


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#。朝正确方向的任何观点表示赞赏。


5
拥有一个绑定所有属性的构造函数并没有什么不好。实际上,在某些持久性环境中,这是必需的。没有什么可以说您不能有多个构造函数,也许是在进行分段构造之后调用有效性检查方法。
BobDalgleish

1
我不得不质疑您是否打算使用文字在代码中构造Enemy对象。如果你不这样做,我不明白为什么你会,然后建立一个从数据库接口,或串行串,或者......拉的数据构造
咱山猫


Answers:


58

解决方案是将参数捆绑为复合类型。宽度和高度在概念上相关-它们指定敌人的尺寸,通常一起需要。可以将它们替换为一个Dimensions类型,或者也可以替换为一个Rectangle包含位置的类型。另一方面,将其分组positionspeed分成一个MovementData类型可能更有意义,尤其是在稍后加速进入图像时。从方面,我认为maxHpattackDamagedefense,等也属于一个共同Stats的类型。因此,修改后的签名可能看起来像这样:

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

画线的细节取决于代码的其余部分以及通常一起使用的数据。


21
我还要补充一点,如果值太多,可能表明违反了“单一责任原则”。将值分组为特定对象是分离这些责任的第一步。
欣快2014年

2
我认为值列表不是SRP的问题;它们中的大多数可能是针对基类构造函数的。层次结构中的每个类都可以承担一个责任。Enemy只是针对的类别Player,但他们的共同基础类别Combatant需要战斗统计资料。
MSalters 2014年

@MSalters不一定表示SRP问题,但可以。如果他需要进行足够的数字运算,那么这些函数应该以静态/自由函数(如果他使用Dimensions/ MovementData用作普通的旧数据容器)或方法(如果他将其转换为抽象数据)的方式进入Enemy类。类型/对象)。例如,如果他还没有创建Vector2类型,那么他可能最终会在中进行向量数学运算Enemy
2014年

24

您可能想看看Builder模式。从链接(带有模式示例和替代示例):

当设计其构造函数或静态工厂将具有多个参数的类时,[Builder]模式是一个不错的选择,特别是如果其中大多数参数是可选的。使用构建器比使用传统的伸缩构造器模式更容易读写客户端代码,并且构建器比JavaBeans安全得多。


4
简短的代码段将很有帮助。这是用于构建具有各种输入的复杂对象或结构的绝佳模式。您还可以专门化构建器,例如封装各种共享属性的EnemyABuilder,EnemyBBuilder等。这是Factory模式的另一面(如下所述),但是我个人更喜欢Builder。
罗布2014年

1
谢谢,Builder模式和Factory模式看起来都可以很好地与我总体上想做的事情一起使用。我认为Builder / Factory和Doval的建议可能是我想要的。编辑:我想我只能标记一个答案;我将它提供给Doval,因为它可以回答主题问题,但其他人对我的特定问题同样有帮助。谢谢你们。
特拉维斯2014年

我认为值得注意的是,如果您的语言支持幻像类型,那么您可以编写一个生成器模式来强制调用某些/所有SetX函数。它还允许确保它们也仅被调用一次(如果需要)。
Thomas Eding 2014年

1
@ Mark16如链接中所述,>生成器模式模拟Ada和Python中的命名可选参数。您提到您在问题中也使用C#,并且该语言确实支持命名/可选参数(自C#4.0起),因此这可能是另一种选择。
2014年

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);

0

要添加到Rory Hunter的答案中的代码示例(使用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();

1
程序员会游览概念性问题,并给出答案解释问题。抛出代码转储而不是进行解释就像将代码从IDE复制到白板一样:它看起来很熟悉,甚至有时是可以理解的,但是感觉很奇怪……只是很奇怪。白板没有编译器
Mar14
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.