提取参数对象有什么好处?

What are the advantages of extracting parameter objects?

IntelliJ-IDEA 中有一个重构工具可以让我从方法中提取参数对象。

这将执行如下操作:

public interface ThirdPartyPoint {
    float getX();
    float getY();
}

之前:

class Main {
    public static float distanceBetween(float x1, y1, x2, y2) {
        return distanceBetween(x1, y1), x2, y2);
    }

    public static float distanceBetween(ThirdPartyPoint point1, ThirdPartyPoint point2) {
        return distanceBetween(point1.getX(), point1.getY(), point2.getX(), point2.getY());
    }
}

之后:

class Main {

    public static float distanceBetween(Point point1, Point point2) {
        return Math.sqrt(Math.pow(point2.getX() - point1.getX(), 2) + Math.pow(point2.getY() - point1.getY(), 2));
    }

    public static float distanceBetween(ThirdPartyPoint point1, ThirdPartyPoint point2) {
        return distanceBetween(new Point(point1.getX(), point2.getY()), new Point(point2.getX(), point2.getY()));
    }

    private static class Point {
        private final float x;
        private final float y;

        private Point(float x, float y) {
            this.x = x;
            this.y = y;
        }

        public float getX() {
            return x;
        }

        public float getY() {
            return y;
        }
    }
}

为什么比以前好多了?

现在,如果我必须使用这个方法,我需要在每次调用它时创建一个新的点对象。而以前,我只能使用原始类型。

我的感觉是方法签名通常应该朝相反的方向发展。例如,如果你有一些函数可以找出一个名字有多受欢迎,就像这样:

public int nameRanking(String name) {
    // do something with name
}

然后你像这样提取一个参数对象:

public int nameRanking(Person person) {
    // do something with person.getName()
}

这不会让事情变得更糟吗?例如,如果在重构菜单中创建 Person class 之后,我决定删除 getName() 方法,因为我不希望该名称对所有人公开, 但其他 classes 使用了 nameRanking 功能?现在我需要更改我的 nameRanking 函数。如果我使用内置字符串 class,我知道注入此函数的任何内容都不会改变。

我认为您的第二个示例(使用 namePerson)与您的第一个示例非常不同。在第一个示例中,您没有发送更多或更少的信息,而是以不同的形式发送完全相同的信息。在你的第二个例子中,正如你所指出的,你最终发送了很多该功能不需要执行其职责的信息。根据经验,您总是希望将发送函数的信息限制为执行任务所需的信息。在您的情况下,这是该人的姓名。这有助于减少耦合并使您的函数不必了解太多其他 class 的 behavior/structure 来执行它的任务。您可以假设您的姓名排名功能有一天可能会用于根据年龄组和其他信息进行加权排名,因此,最好发送整个 Person 对象,但这不是现在案例,所以我不会进入那个。

让我们回到您的第一个示例。正如我所提到的,在那种情况下,您最终会以另一种形式发送相同的信息。在哪种情况下,这会比发送更长的原语列表更好?假设我们正在编码 Red Square。在这个游戏中,你必须控制周围的红色方块并躲避移动的蓝色矩形。我将向您展示一些代码的两种不同编写方式。

第一个实现可能如下所示:

int x2 = game.player.pos_x
int y2 = game.player.pos_y
int w2 = game.player.width
int h2 = game.player.height
for(Enemy enemy : game.enemies) {
    int x2 = enemy.pos_x
    int y2 = enemy.pos_y
    int w2 = enemy.width
    int h2 = enemy.height

    // If it is colliding with the player
    if(collides(x1, y1, w1, h1, x1, y2, w2, h2)) {
        die()
    }

    // If it is going out of bounds, bump it back inside
    if(isInBounds(0, 0, game.map.width, game.map.height, x2, y2, w2, h2)) {
        enemy.bump()
    }

    //... More functions that use x1, y1, w1, w1, ...
}

这是第二个实现,我们有 introduced a Parameter ObjectRectangle:

Rectangle playerRect = game.player.rect
for(Enemy enemy : game.enemies) {
    Rectangle enemyRect = enemy.rect

    // If it is colliding with the player
    if(collides(playerRect)) {
        die()
    }

    // If it is going out of bounds, bump it back inside
    if(isInBounds(game.map.rect, enemyRect)) {
        enemy.bump()
    }

    //... More functions that use playerRect and enemyRect
}

第二种实现的一些优点包括:

  1. 更具可读性的函数调用(更少的参数)
  2. 减少乱序发送参数的错误空间(如果我发送 (width, height, x, y) 而不是 (x, y, width, height) 会怎么样?)
  3. 您一定要始终处理整个矩形实例(没有机会发送您玩家的 x 而不是敌人的 x 的函数,或类似的错误)

我相信您使用 Point 的第一个示例与 Rectangle 的示例比您的第二个示例更相似(您最终将更紧密的耦合注入到您不需要的信息中)。如果您有一组经常一起发送给不同函数的参数,这可能表明它们作为一个实体运行,并且会受益于 "Introduce Parameter Object" 重构。例如,在您的 Point 示例中,您很少(从不?)使用 xy 作为独立信息。您总是将它们作为一个实体,一个点来使用 (x,y).

请记住,在使用自动重构工具时始终要小心。 “Introduce Parameter Object”不应该盲目地用在每个单独的函数调用上。正如我之前所说,您应该问问自己,将这些参数捆绑为一个实体是否有意义(从概念上讲)。话虽如此,重构可以帮助您的代码更具可读性,并减少您必须发送函数的参数数量,从而减少参数顺序错误或不匹配。不相信我?您是否注意到第一个 collides(...) 函数调用中的细微错误? :-)