質問

Background

I'm writing an application in Java and I'm using Guice for DI.

(It's Android and RoboGuice to be exact, although it probably makes no difference in this context).

Class design

It's an app for keeping score for a popular cardgame - Hearts.

The game consists of various consecutive deals, whose rules differ. Eg. in one deal players are penalized for taking Hearts, in another - for taking Jokers and Kings.

My object design involves several classes:

  • Deal for identifying each deal

  • ScoreCalculator for calculating penalties (eg. each Heart may be worth -2 points). Penalties may vary from deal to deal.

  • ScoreValidator for validating the scores (for instance, it's not possible for each player to take 4 Hearts, because there isn't as many in the deck)

If each Deal class had one corresponding ScoreCalculator and ScoreValidator, dependency injection would be trivial.

But this is not the case. Calculating score for some of the deals can be very specific (distinguishing them from the others), while for the rest it's based on nothing more but multiplying the number of taken cards by the penalty parameter (-2 or -4 etc.)

So, LotteryDeal is associated with a LotteryCalculator, but NoQueens and NoGents both require a class I named SimpleCalculator.

It takes a single integer parameter, which is a multiplier (penalty score).

This is my current solution, in which I implemented Deal as an enum (but I'm not happy with it and I want to drop it):

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

I'm not removing some complexities that are out of the scope of this question (such as the PossibleDealResults class, which I did not describe to you), as it doesn't seem to be very relevant.

The main point is that all dependencies are hard-coded, as you can see, which is not really flexible, for example because there are many different variations of the game, with various scoring rules.

Let's say I'd like to use dependency injection to allow for more flexibility and perhaps even switching between different rules sets more easily - by switching to a different Module in order to re-resolve dependencies if there is a need.

Where's the problem?

I think I have some grasp on how to do it in general.

My question concerns injecting the SimpleCalculator object.

I'd need it with a parameter of -2 for TakeNothingDeal, but -18 for the NoKingOfHeartsDeal.

How to achieve it with Guice?

I'd like to keep the class parameterized and avoid creating a MinusTwoSimpleCalculator and a MinusEighteen... one.

I'm not really sure what's the proper way to achieve that, without abusing the framework (or more general DI design guidelines).

What have you tried?

Not much in terms of actual code. I'm a bit stuck.

I know there's bindConstant, but I can't see how I could make use of it in this case. It requires annotations, but if use Deal-specific annotations - I mean, create a Deal.multiplier field and then annotate it with something to the effect of "inject -2 here, please", what did I really do? I just went back to hard-coding dependencies manually and I'm not really using Guice anymore.

I read about AssistedInject, too, but I can't figure out how it could be of help here, either.

I don't want to overengineer this nor to work against the framework. What's the correct approach? Happy to clarify if the problem is somehow unclear.

役に立ちましたか?

解決

You have a lot of options, actually. Here are three:

Factory object

Frankly, I don't think this design needs Guice for this particular problem. Instead, create a simple interface to populate with relevant switch statements:

interface DealFactory {
  ScoreCalculator getFromDeal(Deal deal);
  ScoreValidator getFromDeal(Deal deal);
}

You might be thinking, "But that works on objects telescopically! Those methods would be better left on Deal." You'd be right, mostly, but one key factor of OOP (and Dependency Injection) is to encapsulate what varies. Having a single set of rules declared statically in Deal is the opposite of the flexibility you want. (The enum itself is fine; there are a finite number of deal types regardless of the rules in play.)

Here, you could easily bind the DealFactory to some lightweight object that provides exactly the right ScoreCalculator and ScoreValidator for any given Deal, and write as many DealFactory objects as you'd like for each set of rules. At that point you can declare the currently-in-play DealFactory in a module and inject it wherever you want.

Also bear in mind that a factory implementation could easily work with Guice and injected bindings:

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 
    }
  } /* ... */
}

Private modules

A similar problem to yours is sometimes known as the "robot legs" problem, as if you're writing a common Leg object that needs to refer to a LeftFoot in some trees and a RightFoot in others. Setting aside the (better) above solution for a second, you can set up private modules, which allow you bind things privately to expose only a few public dependencies:

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

See what I mean? Certainly possible, but a lot of work for Hearts rules. A custom class is a better match for this particular problem. Your need to specify multipliers is shallow; if Deal needed P, which needed Q, which needed R, which needs S, which needs the same ScoreCalculator, then a PrivateModule solution looks much more appealing than passing your calculator around everywhere.

@Provides methods

If you still want to solve this in Guice, but you don't want to write a lot of private modules that expose one binding each, you can take matters into your own hands with @Provides methods.

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

Again, this will create a binding for every type of Deal, which is probably a bad idea, but it's a bit more lightweight than the above. To some degree you're doing Guice's job for it—creating objects, I mean—but should you need any dependencies Guice can provide, you can inject them as method parameters on the @Provides method itself.

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top