In a complex system it is understandable that you may have to dynamically perform an action without statically knowing whether this object supports the action. In my experience, it is best to ask the object – not by casting, but by guarding access to the method. For example:
@FunctionalInterface
interface ReloadAction { void reload(); }
public abstract class Weapon {
public abstract void attack();
public ReloadAction getReloadAction() { return null; }
}
public ReloadableWeapon extends Weapon {
...
@Override
public ReloadAction getReloadAction() {
return () -> { this.ammo = this.maxAmmoCapacity; };
}
}
In client code:
void handleReloadEvent(Weapon w) {
ReloadAction reload = w.getReloadAction();
if (reload != null) reload.reload();
else showErrorMessage("this weapon can't be reloaded");
}
Ideally, the UI adapts itself to prevent impossible events.
To make the signature more explicit you could return a Optional<ReloadAction>
.
How does this relate to your suggestions?
This is similar in effect to a downcast but is a bit more typesafe. It is also far more extensible (in the sense of the Open/Closed Principle), since it is now the Weapon
object itself and not the client code which decides whether a weapon is reloadable. In particular, you can now have different kinds of weapons that are reloadable without having to inherit from Reloadable
. This composability also allows you to handle weapons that support a combination of multiple interfaces, without having to create a new class for each combination: the main Weapon
class is often sufficient if you supply the available actions through the constructors.
This solution is the exact opposite of using a Visitor. With the visitor you cannot add arbitrary new classes but can add more actions through visitors. This is because the visitor interface describes which classes are supported. But here we have added a new method to the Weapon
interface. So we cannot add arbitrary new actions, but we can create more subclasses that implement these actions. Whether a visitor or these method objects are more appropriate depends on how you expect to extend your code in the future: is it more likely to add new actions (then prefer a visitor) or more likely to add new implementations (then prefer method objects)?
Related concepts:
- Entity-Attribute-Value systems: describing a data model dynamically instead of using the type system of the host language.
- Object Adapter Pattern: these method objects behave like adapters from the Weapon class to the Action interface.
- Type Object Pattern: composition instead of inheritance, especially for complex behaviours that are common in games.
- Virtual Constructor idiom, especially in C++: use methods instead of downcasting. Related to the Template Method Pattern.
ReloadableWeapon
knows about reloading (since it usesReloadable
) and aboutattack()
(because is ISAWeapon
). SoReloadableWeapon
can simply overrideWeapon.attack()
to useReload()
.reload
method, you should most likely instantiate aReloadableWeapon
directly, if you don't care about thereload
method and just want to use a regular weapon, you probably will not instantiate aReloadableWeapon
in the first place.Reload()
fromattack()
, what if I want to reload my weapon when my player is not attacking or resting?Weapon
- it has to be withinReloadableWeapon
or in its subclasses (e.g.CrossBow
). Of course, you can have a general callbackWeapon.downtime()
that gets called regularly, and overrideReloadableWeapon.downtime()
so that it callsReload()
as well assuper.downtime()
.Weapon
can have aprepare()
method, guns can be loaded, a sword can be drawn. During player downtime I can callprepare()
provided the Weapon in in use. At least this way, it avoids having the declareReloadeableWeapon
separately and I can keep myWeapon
in a collection without having to downcast to use it.