如何将这段代码转换为使用依赖注入模式的形式?

7

好的,我有以下情况。最初我的代码是这样的:

public class MainBoard {
    private BoardType1 bt1;
    private BoardType2 bt2;
    private BoardType3 bt3;
    ...
    private readonly Size boardSize;

    public MainBoard(Size boardSize) {
        this.boardSize = boardSize;

        bt1 = new BoardType1(boardSize);
        bt2 = new BoardType2(boardSize);
        bt3 = new BoardType3(boardSize);
    }
}

现在,我决定重构那段代码,使得类之间的依赖关系被注入进去:

public class MainBoard {
    private IBoardType1 bt1;
    private IBoardType2 bt2;
    private IBoardType3 bt3;
    ...
    private Size boardSize;

    public MainBoard(Size boardSize, IBoardType1 bt1, IBoardType2 bt2, IBoardType3 bt3) {
        this.bt1 = bt1;
        this.bt2 = bt2;
        this.bt3 = bt3;
    }
}

我的问题是如何处理板子大小?我的意思是,在第一种情况下,我只需传递所需的板子大小,它就会做出一切来创建其他正确大小的板子。在依赖注入的情况下,可能不再是这样。在这种情况下,你们怎么做?你们会在 MainBoard 的构造函数上放置任何检查,以确保传递了正确的大小吗?你只是假设类的客户端会负责传递相同大小的3种板子,这样就没有麻烦了吗?
编辑
为什么我要这样做?因为我需要对 MainBoard 进行单元测试。我需要能够将三个子板设置为某些状态,以便测试我的 MainBoard 是否按照我的期望工作。
谢谢

我认为可能需要更多的信息 - 没有更多的上下文很难回答这样的问题。 - Benp44
没错!问问自己,“主板的行为是什么?”这就是你在单元测试中要写的内容。MainBoard可能以相同的方式与所有子板交互,这可能需要一个单一的抽象接口,也可能不需要。你测试MainBoard与其组成部分的交互。然后你使用工厂创建MainBoard和它的组成部分。最后,你用工厂调用替换原来对MainBoard的构造函数的调用。对于调用新工厂的类,重复这个想法。 - Cliff
所有的板子都有不同的行为/方法。这不是多态的情况。 - devoured elysium
你不应该编写代码以允许单元测试,而应该使用一个好的单元测试框架。使用PowerMock或类似工具,要么模拟“new”调用,要么在测试用例中实例化对象后设置其内部状态。 - Max Stewart
8个回答

5
我认为第二种情况中的boardSize参数是不必要的,但我会添加一个断言来确保3个棋盘大小确实相等。但总的来说,第二种情况对我来说似乎有些可疑。除非您真的需要向主板注入不同类型的板子,否则我建议采用第一种方法。即使这样,我也会考虑使用例如棋盘factory而不是将3个棋盘参数传递给构造函数,例如。
public interface IBoardBuilderFactory {
    public IBoardType1 createBoardType1(Size boardSize);
    public IBoardType2 createBoardType2(Size boardSize);
    public IBoardType3 createBoardType3(Size boardSize);
}

这将确保三个板的“板系列”和大小方面的一致性。
为了决定是否在此处选择DI,我们应该更多地了解上下文/领域模型,特别是主板和子板之间的关系。

嗯,注入一个工厂似乎是个好主意。让我想一想。 - devoured elysium
嗯,如果这3个板子有相同的构造函数,那么工厂就是完美的。但是如果它们有不同的构造函数,我就必须将它们传递给工厂的构造函数。它确实可以强制保持这3个板子的尺寸一致,但我也会得到一个非常丑陋的工厂构造函数:( - devoured elysium
@devoured,不确定我是否理解你的意思...在你的代码示例中显示的具体板类型确实有类似的构造函数,每个构造函数都需要一个单独的“Size”参数。 - Péter Török
是的,对此我感到抱歉。在这种情况下,您所提供的内容是完美的。但我担心以后它们可能会有不同的构造函数,就是这样! - devoured elysium
注入这样一个工厂意味着主板现在需要知道单个板的构造函数参数。更好的方法是注入一个已经设置了boardSize的工厂,它只返回值(或者可以看看我的另一种解决方案)。 - WW.
@WW,在经典意义上,工厂是无状态的。带有状态的工厂更像是生成器模式。对我来说,MainBoard控制其兄弟板的构建参数完全合理-根据其功能,它可能需要知道板的大小。但是,如果主板在其他方法中不使用板的大小,则您的建议确实可能更好。正如我上面所指出的,我们没有足够的信息来决定。 - Péter Török

3
在这种情况下,是否应该应用依赖注入(或依赖反转)还有待商榷。我认为您的 MainBoard 在第一个示例中负责管理其创建的 BoardTypes 的生命周期。如果您现在注入了 BoardTypes,则必须由 MainBoard消费者处理此责任。
这是灵活性和对消费者方面的额外职责之间的权衡。
另一方面,如果有理由让 BoardType 的生命周期被外部处理,则可以使用依赖反转。然后,您的 MainBoard 构造函数需要确保其依赖项得到适当满足。这将包括检查它们的 Size 是否相等。

我这样做是为了方便测试类。如果像第一种情况那样,测试将变得困难,甚至不可能。 - devoured elysium
1
你仍然可以使用第二个构造函数,但是为了生产方便,可以使用一个方便的重载或工厂。 - Johannes Rudolph

2
依赖注入的主要优点是与被注入对象的更改隔离。因此,在您的情况下,唯一明显的变量是大小。您将把板子注入到主板中,以便主板不再需要了解或担心大小。此外,除非您的应用程序需要在不同的板子类型之间维护3种不同的行为,否则建议使用单个抽象定义来定义板子类型接口。
public class MainBoard {
    private IBoardType bt1;
    private IBoardType bt2;
    private IBoardType bt3;

    public MainBoard(IBoardType bt1, IBoardType bt2, IBoardType bt3) {
        this.bt1 = bt1;
        this.bt2 = bt2;
        this.bt3 = bt3;
    }
}

注入框架或组装代码的责任是确保这些板子被赋予正确的大小。可以通过多种方式实现,例如主板和注入板都从单个外部源派生其大小。也许您的应用程序相对于主板调整注入板的大小。

因此,您可以具有外部逻辑,例如:

public class BoardAssembler {
   public static MainBoard assembleBoard(Size size) {
      Size innerBoardSize = deriveSizeOfInternalBoards(size);
      return new MainBoard(new BoardType1(innerBoardSize), new BoardType2(innerBoardSize), new BoardType3(innerBoardSize));
   }
}

实际上,你所追求的是在构建MainBoard时逻辑的反转。从那里开始,将所有内容提取到工厂或某个邪恶的单例工厂或静态方法中。问问自己,“MainBoard是在哪里创建的?”还要问,“需要哪些组件和参数?”一旦将所有实例化逻辑移动到工厂中,维护Mainboard及其所有依赖关系可能会变得更简单。


1

BoardTypeX类包含哪些类型的信息?将此对象注入到您的MainBoard中是否有意义。依赖注入和模式并不总是解决问题的方法,您不应该仅仅因为可以使用它而使用它。在这里,某种工厂模式可能会更好地发挥作用。

public class MainBoard {
    private IBoardType1 bt1;
    private IBoardType2 bt2;
    private IBoardType3 bt3;
    ...
    private Size boardSize;

    public MainBoard(IBoardBuilderFactory factory) {
        this.bt1 = factory.buildBoard(boardSize);
        //...
    }
}

如果棋盘大小应该由外部确定,也许你根本不需要将其存储。也许工厂在其他地方建造时决定使用什么样的棋盘大小。
无论如何,设计模式的重点是帮助你以干净和可维护的方式完成任务。它们并不是必须遵循的硬性规定。

我这样做是因为单元测试的需要。我需要能够将子板设置为特定状态,以便测试MainBoard类。 - devoured elysium
为了测试目的,你可以创建一个TestBoardBuilderFactory,它可以从buildBoard()方法返回不同状态的游戏板。也许这个类有子类,或者有不同的构造函数等。 - Java Drinker

1

-编辑- 删除了我的大部分回复,因为其他人比我先回答了 :)

另一个选择是工厂。根据您的要求,您可能会发现使用工厂来解决问题更好(或不是)。这里有一个关于工厂与 DI 的好的 SO 讨论 here。您甚至可以考虑通过 DI 将一个工厂传递到您的类构造函数中 - 因此,您的构造函数需要一个大小和一个工厂类,并将其推迟到工厂类(传递大小)以获取板子。


看起来大多数人已经讨论过工厂了 ;) - Matt Roberts

0

MainBoard的范围内,boardSize实际上是一个常量。你只想要它存在一个单一的值。你需要像这样的代码:

int boardSize = 24;  // Or maybe you get this from reading a file or command line
MainBoardScope mainBoardScope = new mainBoardScope( boardSize );

如果你天真地将24设为全局变量或常量,那么这里就会产生难以发现的依赖关系,因为类会依赖于静态获取该常量或全局变量,而不是通过声明的接口(即构造函数)获得。 mainBoardScope 包含一组具有相同生命周期的对象的“单例”。与旧式的单例不同,这些对象不是全局的也不是通过静态方式访问。然后,请考虑这段代码,在启动应用程序(或更大范围的应用程序中的此作用域)时运行以构建对象图:
   MainBoardFactory factory = new MainBoardFactory( mainBoardScope );
   MainBoard board = factory.createMainBoard();

createMainBoard 方法中,您将使用作用域中的 boardSize 来创建三个子板块:
   IBoardType1 b1 = injectBoardType1( myScope );
   IBoardType2 b2 = injectBoardType2( myScope );
   IBoardType3 b3 = injectBoardType3( myScope );
   return new MainBoard( scope.getBoardSize, b1, b2, b3 );

你需要MainBoard来检查传递给构造函数的三个板子是否大小正确吗?如果是你的代码创建这些板子,那么为injectMainBoard()创建一个单元测试。MainBoard的工作不是确保它被正确构建,而是工厂的工作去创建它,而单元测试则是工厂确保它被正确创建的工作。


0

你可以从通过 DI 传递的 Boards 中获取 boardsize。这样,你就可以完全省去 boardSize 变量。


1
这是正确的,但如果我不采取措施确保所有三个板都具有相同的大小,就可能会出现奇怪的错误。 - devoured elysium

0
你可以有一个BoardTypeFactory来创建BoardTypes,就像这样: ``` IBoardType bt1 = BoardTypeFactory.Create(boardSize); ```
请注意,关于如何编写工厂有很多博客文章和Stack Overflow的回答,上面是一个简单的示例。
然后,你可以调用: ``` new MainBoard(boardSize, bt1, .... ```
将用于创建棋盘的源大小传递进去。

我不明白这样做能带来什么好处。我仍然面临同样的问题,无法保证在将它们传递给MainBoard类时棋盘大小的一致性。 - devoured elysium
我之前所说的“guaranteeing”,实际上更像是“强制执行”的意思。 - devoured elysium
嗨。由于在工厂的.Create()调用和new MainBoard()调用中都使用了相同的“boardSize”变量,因此您正在使用一致的板大小,因为完全相同的值被传递给两个方法调用。 - Jason Evans

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接