What you're talking about is programming to abstractions, which does not imply the Strategy pattern.
Programming to abstractions is always a good practice. There's virtually no additional effort involved in providing an interface or abstract base class for a concrete implementation, and many refactoring tools can now do this automatically for you.
How abstract to make it is a different question.
In your first example, you simply have an abstract type. You could program everything to use a generic Serializer
or ISerializer
type and then wire up a single default implementation in an IoC container like Castle or Spring. Simple. Done.
In the second instance, though, you're designing abstract functionality. You're trying to predict how this abstract type might be used in the future, and you are probably going to be wrong. The YAGNI principle applies here; unless you have some reason to believe that your application actually needs a fully-generic "transform" interface which can turn anything into anything else, and unless you've actually scoped out the requirements for it, then you're wasting your time planning for it. Maybe you should just use one of the many existing XML transformation languages if you need that kind of flexibility.
None of this really has anything to do with the strategy pattern. The strategy pattern means that not only are you coding against an abstract type, but that you are choosing which concrete type to use based on information only known at runtime. This is neither explicit nor implicit in your question.
If you only have one implementation, then implementing a strategy pattern is absolutely overkill, unless you know for sure that you will need to support additional strategies almost immediately afterward.
But that does not preclude you from using abstract types. This is generally how most OO applications are designed now, using dependency injection for code and an IoC container for configuration. I'd go so far as to say this is more productive than coding to concrete types, because (a) it's much easier to test and mock, and (b) quickly coding an interface allows you to continue your current task without much distraction, as opposed to going off and writing a whole new class because you can't continue without it.
So definitely make use of abstract types, but don't overthink things and spend a lot of time fussing about how to generalize them. It's easier to refactor later than it is to work with a bloated interface.
I think you may be getting a little bogged down in the English meaning of state, when compared to the State Pattern (or Finite-State Machine, which is really a diagrammatic representation of a State Pattern). Both are appropriate here, but they shouldn't be confused.
The State Pattern is something which should, given various common stimuli, operate on the game state differently at different times in the game. So, as you've rightly concluded, the lobby stage (which I'm assuming to be while people are joining a game) is a State in that context.
The game state is a list of players, pieces, cards, dice, whatever else, and all the information required to simulate them. You might want to call that the context.
Each State should receive a UserClicked message from the application which receives the context and coordinates or area clicked and should operate on those accordingly. The information that you are trying to give ownership of to the State object does not belong there, it belongs in the game itself, alongside the State.
The state may also have a UserPressedKey method, a TimerTicked method or any other kind of stimulus on which it should act. But each of these should act on the game state rather than being the game state.
One important method of the State will be ScreenRefresh, which will draw the context for the user.
Here is a very rough example:
abstract class ApplicationState {
void Begin(GameContext context);
void UserClicked(GameContext context, int x, int y);
void UserPressedKey(GameContext context, char key);
void TimerTicked(GameContext context);
void ScreenRefresh(GameContext context);
protected void OnStateChanged(ApplicationState newState) {
// inform interested parties that state has changed,
// using the observer pattern
}
}
class LobbyState : ApplicationState
{
void Begin(GameContext context) {
context.Players = new User[context.NoOfPlayers];
}
void UserClicked(GameContext context, int x, int y) {
// Find a displayed player box which contains click coords
int index = -1;
for (i=0; i < context.Players.Length; i++) {
if (PlayerBox[i].Contains(x, y)) {
index = i;
}
}
if (index > -1) {
if (context.Player[index] == null) {
context.Player[index] = context.ActivePlayer;
} else if (context.Player[index] == context.ActivePlayer()) {
context.Player[index] = null;
} else {
ErrorSound.Play();
}
} else if (CloseIcon.Contains(x, y)) {
OnStateChanged(new ExittingState());
} else {
ErrorSound.Play();
}
}
void UserPressedKey(GameContext context, char key) {
if (key = 'J' or key = 'j') {
if (! context.AddPlayerRandomly(context.ActiveUser)) {
ErrorSound.Play();
}
if (context.GameIsNowFull()) {
OnStateChanged(new InitializingState());
}
} else if key = Esc {
if (!context.RemovePlayer(context.ActiveUser)) {
ErrorSound.Play();
}
} else {
ErrorSound.Play();
}
}
void TimerTicked(GameContext context) {
// there is no use for a game timer while
// we're trying to fill the game, but you
// want one to tick anyway, because the
// application doesn't know which state
// it is in.
}
void ScreenRefresh(GameContext context) {
DrawPlayerBoxes(context.Screen, context.Players);
DrawExitIcon(context.Screen);
}
}
class InitializingState : ApplicationState {
private double waitSpinnerAngle = 0;
void Begin(GameContext context) {
context.RandomizePlayOrder();
context.ShuffleCards();
// all other game initialization rules here
OnContextChanged(new PlayerUpState(context.Players[0]));
}
void UserClicked(GameContext context, int x, int y) {
// Not responding to user input for a moment
ErrorSound.Play();
}
void UserPressedKey(GameContext context, char key) {
// Not responding to user input for a moment
ErrorSound.Play();
}
void TimerTicked(GameContext context) {
waitSpinnerAngle += 0.05;
if (waitSpinnerAngle > 1) waitSpinnerAngle = 0;
}
void ScreenRefresh(GameContext context) {
DrawWaitSpinner(waitSpinnerAngle);
}
}
See where I'm going with this? The Application itself is then as simple as setting the initial State, hooking a listener into the OnStateChanged event and calling Begin. When the listener hears that the event is called, unhook your old State, hook in the new one, and call Begin again.
Everything else is triggered by events from the mouse, keyboard or timer, passed directly to the current State without knowledge of which State the game is currently in. If you are running this game across a network then you will also need an event for changes of state received from other players.
Everything in your State and Context is now very unit-testable and separation of concerns are observed. Although, you may want to refactor that LobbyState#UserClicked method a bit, among other things.
Remember, this is just a free example and you get what you paid for it. Don't try to apply it directly to your game. Just use it to understand how the State Pattern should work in the context of a game.
Best Answer
Passing the reference
Yes it would still work because you are only passing the reference and therefore it doesn't alter the referenced object (nor create a copy)
Responsibility for opening/closing
There are 3 possible cases:
Only one command uses the serial port. The serial port is completely encapsulated in the command and is opened at the beginning of the command and closed at the end.
At least 2 commands use the serial port and performance isn't a problem (you don't have timings to ensure). Each command has an instance of the serial port (pointing to the same COM port). Each command is responsible for opening/closing. Since the commands are executed sequentially (
DequeuedItem
) there's no conflict on the serial port.At least 2 commands use the serial port but you can't afford to open/close the serial port for each command. The serial port has to be instancied a level higher and given to every commands that need it. The commands don't decide to open/close the serial port, they assume they receive an already open port and they shouldn't alter its state. The serial port could be opened at the moment a command requiring the serial port is added to the queue. The port could be closed when the queue finishes to execute the last command requiring the serial port.
Within these 3 cases, I think there is no bad solution. Each case is appropriate (or not) depending on the usage you make (sparsly, intensively, accurately, ...). The way you use the serial port seems fine to me.