Limit the use of scopes or more concretely Use scopes for wiring only.
Used properly, scopes can reduce a lot of factory boiler plate. I use scopes to wire together sub-processes that may need access to its name and arguments. This is similar to the RequestScope
s provided by Guice and Spring.
But scopes are effectively a thread-local map of string to object. This is practically a Global Variable Depot. This is why I limit my scope usage.
This leads to the corollary:
Hide the scopes or more generally Hide your DI Framework
Because scopes (and DI frameworks) are essentially Global Variable Depots, I prefer to encapsulate the DIF such that the only thing that knows there is a DIF is the main method.
To do this, I define my own scope interface and my own ProcessFactory that I define within a Guice module.
Because of this, my process manager is free of references to Guice. In fact, it is little more than this method:
void run(final ProcessContext context) {
try {
this.scope.enter(context.processArgs());
this.factory.create(args.processName());
} finally {
this.scope.exit();
}
}
Here's the complete Guice module that binds and hides my use of a Guice. It implements my own ProcessScope and ProcessFactory interfaces. It binds @ProcessParameters so they can be injected and a few convienience objects as well (@ProcessName String and ProcessConfig).
public class ProcessRunnerModule extends AbstractModule {
private static final String PROCESS_RUN_SCOPE = "ProcessRunScope";
/**
* Objects of type Map<String, Object> parameterized by @ProcessParameters
* are injected in the ProcessRunScope
*/
static final Key<Map<String, Object>> PROCESS_PARAMETERS_KEY;
static {
final TypeLiteral<Map<String, Object>> MAP_TYPE = new TypeLiteral<Map<String, Object>>() {
};
PROCESS_PARAMETERS_KEY = Key.get(MAP_TYPE, ProcessParameters.class);
}
/**
* Wraps Guice scope to ProcessScope. Injects the @ProcessParameters into guice
*/
private static class GuiceScopeAdapter implements ProcessScope {
private final SimpleScope scope;
@Inject @SuppressWarnings("unused")
public GuiceScopeAdapter(@Named(PROCESS_RUN_SCOPE) SimpleScope scope) {
this.scope = scope;
}
@Override
public void enterScope(Map<String, Object> parameters) {
scope.enter();
scope.seed(PROCESS_PARAMETERS_KEY, parameters);
}
@Override
public void exitScope() {
scope.exit();
}
}
/**
* Processes are run and bound in @ProcessRunScope.
*/
protected void configure() {
final SimpleScope processRunScope = new SimpleScope();
bindScope(ProcessRunScope.class, processRunScope);
bind(SimpleScope.class)
.annotatedWith(Names.named(PROCESS_RUN_SCOPE))
.to(processRunScope);
}
/**
* This wraps Processes bound via MapBinder to a ProcessFactory
*/
@Provides @Singleton
ProcessFactory createProcessFactory(final Map<String, Provider<Process>> processFactories) {
log.info("Instantiating process factory", "known-processes", processFactories.keySet());
return new ProcessFactory() {
@Override
public Process create(final String name) {
return processFactories.get(name).get();
}
};
}
/**
* ProcessRunner does not know about Guice
*/
@Provides @Singleton
ProcessRunner createProcessRunner(
final ProcessScope processScope,
final ProcessFactory processFactory) {
return new ProcessRunner(processScope, processFactory);
}
/**
* Convienience: bind a @ProcessName extracted from the @ProcessParameters
*/
@Provides @ProcessName @ProcessRunScope
String bindProcessName(final @ProcessParameters Map<String, Object> params) {
return params.get(ProcessRunner.PROCESS_NAME_KEY).toString();
}
/**
* Convienience: bind a ProcessConfig wrapping the @ProcessParameters
*/
@Provides @ProcessRunScope
ProcessConfig createParamHelper(final @ProcessParameters Map<String, Object> params) {
return new ProcessConfig(params);
}
}
After looking at your designs, both your first and third iterations appear to be more elegant designs. However, you mention that you're a student and your professor gave you some feedback. Without knowing exactly what your assignment or the purpose of the class is or more information about what your professor suggested, I would take anything I say below with a grain of salt.
In your first design, you declare your RuleInterface
to be an interface that defines how to handle each player's turn, how to determine if the game is over, and how to determine a winner after the game ends. It seems like that's a valid interface to a family of games that experiences variation. However, depending on the games, you might have duplicated code. I'd agree that the flexibility to change the rules of one game is a good thing, but I'd also argue that code duplication is terrible for defects. If you copy/paste defective code between implementations and one has a bug in it, you now have multiple bugs that need to be fixed in different locations. If you rewrite the implementations at different times, you could introduce defects in different locations. Neither of those is desirable.
Your second design seems rather complex, with a deep inheritance tree. At least, it's deeper than I would expect for solving this type of problem. You're also starting to break up implementation details into other classes. Ultimately, you are modeling and implementing a game. This might be an interesting approach if you were required mix-and-match your rules for determining the results of a move, the end of the game, and a winner, that doesn't seem to be in the requirements that you've mentioned. Your games are well defined sets of rules, and I'd try to encapsulate the games as much as I can into separate entities.
Your third design is one that I like the best. My only concern is that it's not at the right level of abstraction. Right now, you appear to be modeling a turn. I would recommend considering designing the game. Consider that you have players who are making moves on some kind of board, using stones. Your game requires these actors to be present. From there, your algorithm is not doTurn()
but playGame()
, which goes from the initial move to the final move, after which it terminates. After every player's move, it adjusts the state of the game, determines if the game is in a final state, and if it is, determines the winner.
I would recommend taking closer looks at your first and third designs and working with them. It might also help to think in terms of prototypes. What would the clients that use these interfaces look like? Does one design approach make more sense for implementing a client that's actually going to instantiate a game and play the game? You need to realize what it's interacting with. In your particular case, it's the Game
class, and any other associated elements - you can't design in isolation.
Since you mention you're a student, I'd like to share a few things from a time when I was the TA for a software design course:
- Patterns are simply a way of capturing things that have worked in the past, but abstracting them to a point where they can be used in other designs. Each catalog of design patterns gives a name to a pattern, explains its intentions and where it can be used, and situations where it would ultimately constrain your design.
- Design comes with experience. The best way to get good at design isn't to simply focus on the modeling aspects, but realize what goes into the implementation of that model. The most elegant design is useful if it can't easily be implemented or it doesn't fit into the larger design of the system or with other systems.
- Very few designs are "right" or "wrong". As long as the design fulfills the requirements of the system, it can't be wrong. Once there's a mapping from each requirement into some representation of how the system is going to meet that requirement, the design can't be wrong. It's only a qualitative at this point about concepts such as flexibility or reusability or testability or maintainability.
Best Answer
The strategy pattern, simply put, is providing the ability to specify a concrete behavior for something so that its consumer can ignore what the concrete behavior is. An example is something like a logging strategy. The thing doing the logging doesn't care where the log messages go.
Dependency Injection is the idea that things are given their dependencies rather than seeking them out.
There's still scenarios where code would know how to build the strategy they want, or otherwise directly depend on the strategy implementations. They're uncommon and best avoided, but hopefully that helps distinguish the orthogonal concepts for you.