Reputation: 678
I decided to an solid rpg-like game structure with Java to practice design patterns.
Basically there are different types of characters in my game, which are all considered to be "game objects", having some common features:
public abstract class Character extends GameObject {
Status status;
//fields, methods, etc.
}
public abstract class Monster extends Character{
//fields, methods, etc
}
public class Hero extends Character {
//fields, methods, etc
}
Status here is an enumeration:
public enum Status {
NORMAL,
BURNT,
POISONED,
HEALED,
FROZEN
}
I would like to make my code flexible, easy-to-modify, and I would like to follow the SOLID principles using the necessary design patterns effectively.
Let's suppose I would like to customize my characters, allowing to create custom Character extensions allowing to have only certain status changes. For example I would create a Monster called
public class FireGolem extends Monster{...}
, which is unable to get damaged by heat (hence is unable to get burnt).
I have 2 ideas to do this:
1) create a Set for the class Character, in which I would specify what kind of status changes can a Character have
2) create different interfaces (Burnable, Freezable, ...) and implement them when necessary.
What do you think? Which is better and why? Is there any better and cleaner option at all?
Thank you in advance.
Upvotes: 5
Views: 297
Reputation: 49646
FireGolem
might simply override the method setStatus
and throw, let's say, an IllegalArgumentException
when the given status can't be applied to its instances.
class FireGolem extends Monster {
@Override
public void setStatus(Status status) {
if (Status.BURNT.equals(status)) {
throw new IllegalArgumentException("FireGolem can't be burnt!");
}
super.setStatus(status);
}
}
As @Vince Emigh pointed out, it's not a pure SOLID example: preconditions shouldn't be strengthened in subclasses.
Upvotes: 2
Reputation: 1498
I think you might want to consider going the other way around.
Generally speaking, most characters will be burnable, freezable, etc.
So instead of creating a set for all kinds of status a character can have, create one for the characters Immunities.
This will allow you to handle immunities in the parent class (Character), so that when creating a new monster, all you have to do is add an immunity to it in its constructor and all will be well without having to override any methods.
Let's see how that would work in your example.
Oh but before that, short warning: I will call your status BURNING and not BURNT, just because I assume the character with that status is in fact still burning ;)
public abstract class Character extends GameObject {
Status status;
ArrayList<Status> immunities = new ArrayList<>();
//fields, methods, etc.
public void addImmunity(Status immunity) {
immunities.add(immunity);
}
// return false if the status couldn't be set in case you want to do something
// like show an "Immune!" message or something like that
public boolean setStatus(Status status) {
if (immunities.contains(status)) {
return false;
}
this.status = status;
return true;
}
}
class FireGolem extends Monster {
public FireGolem() {
addImmunity(Status.BURNING);
}
}
The great thing about this approach is that you will save quite a bit of memory in the long run. And you don't have to overengineer anything. Now... whether or not you're using an ArrayList or something else is of course up for debate, this is just a simple example.
Also, the setStatus method is returning a boolean as a result here. The reason I'm not throwing an exception is because I simply don't consider it one. Why wouldn't a player try to set the Fire Golem on fire? Sure, it shouldn't work, but it's still one of the expected cases. Then again, different people use different approaches and there's certainly nothing completely wrong with throwing an exception here, it's just that for me personally, it doesn't feel right. If you want more information than a simple true or false for visualization purposes, you could return more complicated objects instead, but I wanted to leave the example as simple as possible.
One more thing to add: Maybe you should also consider giving the character a status list instead of a single status because while freeze and burn might cancel each other out, I do think it's possible to be burning and poisoned at once, but that's just a matter of opinion. There are a lot of games out there that only allow a single status at once.
Upvotes: 1
Reputation: 691
I assume your scope is something more than a toy project, so follow SOLID principe here is a need, not only an execise. Otherwise you can do with your approach with no problem at all.
My pick is not to use inheritance, but instead encapsulation, because you have better modularity in your code and is more maintanable. For reference on digging in this discussion, here see here
So in your case avoid to have a GameObject, extend with Character then extend with Monster and hero, because every little change in GameObject will have repercussions in every entity of your game.
You could use another approach: Entity Component System (reference here and here.
So in your case you will:
For another reference on experience using it see here
Note: Ecs are used by big names too, like Unity
Upvotes: 1
Reputation: 3062
In general you should not extend in order to limit capabilities / functionalities. That violates Liskov substitution principle which is part of SOLID which you said you want to use.
In your particular case you first say every Character
can have one of those statuses and then you try to introduce a character that can not have given status.
My first thought (I can't claim a solution without knowing all the details) about this particular case is to follow the Interface segregation principle and introduce interfaces that provide isBurnt
, isFrozen
, ... respectively. I may then aggregate those into say Fragile
interface if most Character
s should implement all. It is also possible to have say FragileCharacter
abstract class with the common logic.
Upvotes: 1
Reputation: 4733
Why not take a look at the State Pattern?
Basically, each state would be a class, and they all have the same base class. Then you'll have a context (e.g. your character), where it holds the current state and uses it.
You can, of course, control if a state X can turn into state Y and so on, since each state holds a reference to its context.
Upvotes: 1