如何安全地创建相互依赖的对象?

时间:2014-05-18 20:10:15

标签: java

假设我开始使用以下两个类:

public class Game {
    private final Player self;
    private final Player opponent;

    public Game(final Player self, final Player opponent) {
        this.self = Objects.requireNonNull(self);
        this.opponent = Objects.requireNonNull(opponent);
    }
}

public class Player {
    private final String name;

    public Player (final String name) {
        this.name = Objects.requireNonNull(name);
    }
}

现在我发现我需要访问Game课程中的其他玩家(因此转到Player对象)。

一种方法是将以下内容添加到Player类:

    private Game game;

    public void setGame(final Game game) {
        this.game = Objects.requireNonNull(game);
    }

然而现在它打破了game对象的不变性,有没有办法保持相互创建的对象的不可变性?

或者我是否需要采用手动强制的可变性和安全性(从常规客户端角度,而不是从多线程同步角度来看)?例如,每当有人尝试多次setGame时抛出异常。

总而言之,这是我试图解决的相互依赖:

Player playerSelf = new Player(/* non-existing game */, "Self");
Player playerOpponent = new Player(/* non-existing game */, "Opponent");
Game game = new Game(playerSelf, playerOpponent);

Game game = new Game(/* non-existing player */, /* non-existing player */);
Player playerSelf = new Player(game, "Self");
Player playerOpponent = new Player(game, "Opponent");

是否存在一个模式,例如Builder模式,它有助于构造函数参数的爆炸,如果想在不使用Builder模式的情况下避免暴露,可以以打破不变性的方式解决?

2 个答案:

答案 0 :(得分:6)

每当你有一个循环依赖,打破它。它将有助于减少代码的耦合,提高可测试性并保持理智。为什么Player首先需要访问其他玩家?您可能试图在其中添加太多功能。也许你可以把它移到Game?或者说,插入Strategy对象的Player

另外,请记住,不变性并不总是答案。有些东西,比如游戏状态,本质上是可变的。试图将它们变成不可改变的物体,必然会让生活变得悲惨。

答案 1 :(得分:3)

Immutablity是一个伟大的目标,但Java的100%不变性真的很难:

Erlang使每个数据结构都不可变,当语言在该级别支持时,这是很好的。不幸的是,Java并没有在它需要的水平上支持它,使它毫不费力和轻松。

那说这种结构排序有多种解决方案:

类似于MVC模式,GamePlayer对象甚至根本不了解对方可能是您的最佳解决方案。但它是最复杂的,可能更多的代码,而不是可行的答案。在此之前,我可能会单独就该解决方案发布另一个答案。

以下是一些更简单的解决方案。

内部类解决方案:

在此解决方案中,内部类始终具有对其外部类的隐式引用。无需传入Game对象,因为它始终位于Player类的实例范围内。

import javax.annotation.Nonnull;

public class Q23726363B
{
    public static void main(final String[] args)
    {
        final Game game = new Game(args[0], args[1]);
    }

    public static class Game
    {
        private final Player p1;
        private final Player p2;

        public Game(@Nonnull final String p1, @Nonnull final String p2)
        {
            this.p1 = new Player(p1);
            this.p2 = new Player(p2);
        }

        public class Player
        {
            private final String name;

            private Player(@Nonnull final String name) {this.name = name;}

            public Game getGame() { return Game.this; }
        }
    }
}

工厂方法解决方案:

将Game对象设为Player对象工厂。通过创建两个对象private的构造函数,您可以保证它们被正确构造并通过不提供公开更改它们的方式来引用函数不可变

使用FactoryMethod,如下所示:

import javax.annotation.Nonnull;

public class Q23726363A
{
    public static void main(final String[] args)
    {
        final Game game = Game.startGame(args[0], args[1]);
    }

    public static class Game
    {
        public static Game startGame(@Nonnull final String playerOneName, @Nonnull final String playerTwoName)
        {
            final Player p1 = new Player(playerOneName);
            final Player p2 = new Player(playerTwoName);
            final Game game = new Game(p1, p2);
            p1.setCurrentGame(game);
            p2.setCurrentGame(game);
            return game;
        }

        private final Player player1;
        private final Player player2;

        private Game(@Nonnull final Player player1, @Nonnull final Player player2)
        {
            this.player1 = player1;
            this.player2 = player2;
        }
    }

    public static class Player
    {
        private final String name;
        private Game currentGame;

        private Player(@Nonnull final String name)
        {
            this.name = name;
        }

        private void setCurrentGame(@Nonnull final Game currentGame)
        {
            this.currentGame = currentGame;
        }
    }
}

注意:

您可能想在Player构造函数中创建Game个对象,并将this传递给Player个对象构造函数,以设置对Game的引用对象。

抵制这种诱惑,这被称为泄漏this引用,它在构造函数中是,因为它失去了{{1}的所有保证对象完全形成。

这两种解决方案仍然具有相互循环的依赖关系。

第一个并不是很糟糕,因为Player类是Game的内部类。第二种是简单但天真的解决方案,适用于小规模应用,但不适用于更大更复杂的应用。