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.
This boils down to composition vs. inheritance.
Let's take a look at one pattern that you can do in both ways.
The adapter pattern
is, as the name implies, an adapter between two classes that are otherwise incompatible.
Say you write a 3D engine and you need a Point
class. You have your Point
all ready and it kind of works, but then you find another class OtherPoint
in some library that does everything your class should be doing in a nicer way, but its methods have different names. Your 3D engine expects your Point
class.
What can you do to make both classes compatible in order to use OtherPoint
class?
Point extends OtherPoint
inheritance for the win. Point
receives all the functionality it needs. The methods specific to your engine that Point
declares simply call the corresponding ones from OtherPoint
. This is the class pattern style of the adapter.
- On the other hand, you could do
Point(OtherPoint constructorParameter)
to receive an OtherPoint
object. The idea is similar: the Point
methods now don't delegate to the methods it received via inheritance but to the object it received as a parameter in the constructor. This is the object pattern style of the adapter. This is a comparable to the wrapper pattern.
How does that explain the GoF statement(s)?
"Class patterns [...] are static-fixed at compile-time.
By saying Point extends OtherPoint
you say that a Point
is a OtherPoint
. It's a plain old simple inheritance chain. You cannot change them at run time.
Object patterns [...] which can be changed at run-time and are more dynamic
By saying Point(OtherPoint constructorParameter)
you say that a Point
has a OtherPoint
. Maybe you find out that this library has an even fancier class, i.e. FancierOtherPoint extends OtherPoint
. You can pass that to Point
as well. In fact you can pass any sublcass of OtherPoint
to Point
, because of polymorphism.
Maybe FancierOtherPoint
only works on certain hardware and you have to check for that hardware at run time. With the object pattern style of the adapter, you can do exactly that: decide dynamically what class to use.
I've never heard of "object relationships" outside of this book.
"object relationships" is not special phrase. It means exactly what is says: relationships between objects. Design patterns are just names for common ways to define relationships between objects.
If I were to say "3.14159265359..." you'd probably stop me to ask if I mean "pi". Now if you were to tell me you "inherit from that one class to make things compatible and delegate methods and..." I would stop you to ask if you mean the adapter pattern.
It simplifies communication and everybody has a rough understanding of what the conversation is about without explaining everything in full detail.
Best Answer
In
Beverage
we have the ("template") methodprepare
which calls some abstract methods likeboilWater
,brew
etc. which are implemented in the subclasses. It is not essentially for the pattern to have the template method located in the base class, it could be anywhere else, assumed the abstract methods are public. But it is essentially that there are the abstract methods which must be overriden and will fill the gaps in the "algorithm template".The factory method is also about a class where abstract methods are overwritten, a core point is the caller of that method does not need to know the exact type of the created object. Note that in
PizzaStore
, the abstract methodcreatePizza
must be called somewhere from - lets say a methodcreateLotsOfPizza
.This makes the latter a template method, where the specific steps of the "algorithm to create pizzas" are the gaps to be filled. Now it is probably just an imprecise use of words by saying "the factory method pattern" is a special case of the "template method" pattern. Especially the "factory methods" are not "template methods", however they might be called from a template method. So to be more precise, we might say
and I guess that is what the authors of that book wanted to express.