背景
我正在用Java编写应用程序,而我正在使用Guice进行DI。
(确切地说,这是Android和RoboGuice,尽管在这种情况下它可能没什么区别。)
班级设计
这是一款用于保持流行卡片游戏得分的应用程序 - Hearts。
游戏由各种连续交易组成,其规则不同。例如。在一次交易中,球员因为夺取红心而受到惩罚,而在另一次交易中,球员因为征服了国王和国王而被罚下。
我的对象设计涉及几个类:
Deal
用于识别每笔交易
ScoreCalculator
用于计算罚分(例如,每颗心可能值-2分)。处罚可能因交易而有所不同。
ScoreValidator
用于验证分数(例如,每个玩家不可能获得4颗心,因为套牌中没有那么多)
如果每个Deal
类都有一个对应的ScoreCalculator
和ScoreValidator
,则依赖注入将是微不足道的。
但事实并非如此。计算某些交易的得分可能非常具体(将它们与其他交易区分开来),而对于其余的交易它只是基于惩罚参数(-2或-4等)所取得的卡数量而已。 p>
因此,LotteryDeal
与LotteryCalculator
相关联,但NoQueens
和NoGents
都需要一个名为SimpleCalculator
的类。
它需要一个整数参数,这是一个乘数(罚分)。
这是我目前的解决方案,其中我将Deal
实现为枚举(但我对此并不满意,我想放弃它):
public enum Deal
{
TakeNothing(-2, PossibleDealResults.fullRange()),
NoHearts(-2, PossibleDealResults.fullRange()),
NoQueens(-2, PossibleDealResults.rangeUpTo(4)),
NoGents(-2, PossibleDealResults.rangeUpTo(8)),
NoKingOfHearts(-18, PossibleDealResults.rangeUpTo(1)),
NoLastOne(
new NoLastOneCalculator(),
new NoLastOneValidator(new NoLastOneCalculator())),
Trump1(2, PossibleDealResults.fullRange()),
Trump2(2, PossibleDealResults.fullRange()),
Trump3(2, PossibleDealResults.fullRange()),
Trump4(2, PossibleDealResults.fullRange()),
Lottery(new LotteryCalculator(), PossibleDealResults.rangeUnique(1, 4));
protected ScoreCalculator calculator;
protected PlainScoreValidator validator;
Deal(int multiplier, PossibleDealResults possibleResults)
{
this(new SimpleCalculator(multiplier), possibleResults);
}
Deal(ScoreCalculator calculator, PossibleDealResults possibleResults)
{
this(calculator, new PlainScoreValidator(possibleResults, calculator));
}
Deal(ScoreCalculator calculator, PlainScoreValidator validator)
{
Preconditions.checkNotNull(calculator, "calculator");
Preconditions.checkNotNull(validator, "validator");
this.calculator = calculator;
this.validator = validator;
}
}
我没有删除一些超出此问题范围的复杂性(例如PossibleDealResults
类,我没有向您描述),因为它似乎不太相关。
重点是所有依赖项都是硬编码的,正如您所看到的那样,这并不是非常灵活,例如因为游戏有许多不同的变体,并且有各种评分规则。
假设我想使用依赖注入来提供更大的灵活性,甚至可以更容易地在不同的规则集之间切换 - 通过切换到不同的Module
以便重新解决依赖关系(如果有的话)需要。
问题出在哪里?
我想我已经掌握了一般如何做到这一点。
我的问题是关注注入SimpleCalculator
对象。
我需要-2
的{{1}}参数,TakeNothingDeal
的参数-18
。
如何使用Guice实现它?
我想保持课程参数化,避免创建NoKingOfHeartsDeal
和MinusTwoSimpleCalculator
。
我不确定在不滥用框架(或更一般的DI设计指南)的情况下实现这一目标的正确方法是什么。
您尝试了什么?
在实际代码方面并不多。我有点卡住了。
我知道有MinusEighteen...
,但在这种情况下我无法看到如何使用它。它需要注释,但是如果使用特定于交易的注释 - 我的意思是,创建一个bindConstant
字段,然后使用“注入-2在这里,请”的效果注释它,我真的做了什么?我只是手动回到硬编码依赖项,我不再使用Guice了。
我也读过AssistedInject,但我也无法弄清楚它是如何帮助的。
我不想过分强调这一点,也不想违背框架。什么是正确的方法?很高兴澄清问题是否有些不清楚。
答案 0 :(得分:1)
实际上,你有很多选择。这是三个:
工厂对象
坦率地说,我不认为这个设计需要Guice来解决这个特殊问题。相反,创建一个简单的界面来填充相关的switch
语句:
interface DealFactory {
ScoreCalculator getFromDeal(Deal deal);
ScoreValidator getFromDeal(Deal deal);
}
你可能会想,"但是这可以通过伸缩方式对物体起作用!这些方法最好留在Deal。"大多数情况下,你是对的,但OOP(和依赖注入)的一个关键因素是封装不同的。在Deal中静态声明一组规则与您想要的灵活性相反。 (枚举本身很好;无论规则如何,都有有限数量的交易类型。)
在这里,您可以轻松地将DealFactory绑定到一个轻量级对象,该对象为任何给定的Deal提供完全正确的ScoreCalculator和ScoreValidator,并为每组规则编写尽可能多的DealFactory对象。此时,您可以在模块中声明当前正在使用的DealFactory,并将其注入任何您想要的位置。
还要记住,工厂实现可以轻松地与Guice一起使用并注入绑定:
class DealFactoryImpl implements DealFactory {
@Inject Provider<DefaultScoreCalculator> defaultScoreCalculatorProvider;
@Inject MultiplierScoreCalculator.Factory multiplerScoreCalculatorFactory;
@Override public ScoreCalculator getFromDeal(Deal deal) {
if (TakeNothing.equals(Deal)) {
return defaultScoreCalculatorProvider.get();
} else {
return multiplierScoreCalculatorFactory.create(-2); // assisted inject
}
} /* ... */
}
私人模块
与您类似的问题有时也称为"robot legs" problem,好像您正在编写一个需要在某些树中引用LeftFoot而在其他树中引用RightFoot的常见Leg对象。将(更好的)上述解决方案搁置一秒,您可以设置private modules,这允许您私下绑定事物以仅暴露一些公共依赖项:
// in your module
install(new PrivateModule() {
@Override public void configure() {
SimpleCalculator calculator = new SimpleCalculator(-2);
bind(ScoreCalculator.class).toInstance(calculator);
bind(ScoreValidator.class).toInstance(
new PlainScoreValidator(calculator, PossibleDealResults.fullRange());
expose(Deal.class).annotatedWith(TakeNothing.class); // custom annotation
}
});
明白我的意思?当然可能,但很多工作的心脏规则。自定义类更适合此特定问题。您需要指定乘数很浅;如果需要P,需要Q,需要R,需要S,需要相同的ScoreCalculator,那么PrivateModule解决方案看起来比在任何地方传递计算器更具吸引力。
@Provides methods
如果你仍想在Guice中解决这个问题,但是你不想编写很多私有模块,每个模块都会暴露一个绑定,你可以用@Provides
方法自行解决问题。
// in your module adjacent to configure()
@Provides @TakeNothing Deal anyMethodNameWorks(/* dependencies here */) {
SimpleCalculator calculator = new SimpleCalculator(-2);
ScoreValidator validator = new PlainScoreValidator(calculator,
PossibleDealResults.fullRange());
return new Deal(calculator, validator);
}
同样,这将为每种类型的交易创建一个绑定,这可能是一个坏主意,但它比上面的更轻量级。在某种程度上,你在Guice的工作 - 创建对象 - 我的意思是 - 但是如果你需要Guice可以提供的任何依赖,你可以将它们作为方法参数注入@Provides
方法本身