Search code examples
javacastingabstract-class

Implementing subclass methods in abstract class or casting?


Suppose I have a superclass Zone, which serves the purpose of generalizing more specific types of Zone which are defined in their own respective subclasses. I wish to create a game board, which consists of a 4x4 Zone array. A player should interact with a zone differently depending on what type of Zone they are on.

Consider the following superclass:

public abstract class Zone{        
        Pawn pawn;
        private boolean movable;

        public abstract boolean isMovable();
}

This class is extended by two other classes, MountainZone:

public class MountainZone extends Zone{
        private int temperature;
        movable = true;

        public void FreezePawn(){
            super.pawn.freeze(); //suppose freeze() is implemented in Pawn class
        }

        public boolean isMovable(){
            return this.movable;
        }

        public int getTemperature(){
            return this.temperature;
        }
}

and LavaZone:

public class LavaZone extends Zone{
        private int sulfurLevels;
        private int lavaFlowSpeed;
        movable = false;
        
        public boolean isMovable(){
            return this.movable;
        }

        public int getSulfurLevels(){
            return this.sulfurLevels;
        }

        public void setLavaFlowSpeed(int speed){
            this.lavaFlowSpeed = speed;
        }
}

Lastly, consider the following Main class where the gameboard is created:

public final class Main{
    private static Zone[][] gameboard = new Zone[4][4];

    public static void main (String[] args){

        for (Zone z : gameboard){
            if (z instanceof MountainZone){
                System.out.println(((MountainZone) z).getTemperature());
            } else if (z instanceof LavaZone){
                System.out.println(((LavaZone) z).getSulfurLevels());
            }
        }
    }
}

Instead of writing each method in the superclass with null (or 0 for int) as the return value for the getters and overriding them in their respective subclasses, I would tend towards using casting as shown above. This applies for any other method in the subclasses of Zone as well.

I want to know, if the best way to go about interacting with the individual zones on the gameboard array for specific cases is to use casting, or to implement every single unique method from the subclasses to the superclass.


Solution

  • The short answer: A combinatino of Invert control, and command patterns.

    If you give all zone types the 'getSulphurLevels' method, that's potentially odd; it doesn't appear to apply to a non-lava zone. It can work, of course, but you need to change the mental model of how your app works: If ALL zones have a sulphur level, but it's just that on most zones it is 0, then this works fine, and you can just add:

    public int getSulphurLevels() {
        return 0;
    }
    

    to class Zone, and override that only in LavaZone. Still, that strategy doesn't neccessarily apply to all the weird things you might want to do to a specific zone kind. Thus:

    Inversion of control

    Let's look at that for (Zone z : gameboard) loop. What is it trying to do? It is trying to print some very basic information that is of particular importance to that specific zone. So, implement THAT:

    class Zone {
        public String renderImportantInformation() {
            return "Nothing special going on here";
        }
    }
    
    class LavaZone {
        public String renderImportantInformation() {
           return String.format("Sulphur is at %d density", getSulphurLevel());
        }
    }
    

    This usually serves, but one way or another, if you want non-universal logic, then you need to decide: Either the zones carry this info, or the board does. Usually the zone is the right place, but every so often you get a third layering on top: Code that doesn't really make sense to be in LavaZone itself, but is more a combinatorial property: On specific board kinds, for specific zones, say, special rules apply. Imagine you have a 'modification' engine where the game can be tweaked with alternate rules, and you have a class representing the alternate ruleset. How would that work?

    Then there is the command pattern: Create a map that maps the pertinent info (perhaps only the zone type, for a simple pattern) onto a block of code that knows what to do with it:

    private static final Map<Class<? extends Zone>, Function<Zone, String>> pertinentPropertiesPrinters = new HashMap<>();
    
    static {
    pertinentPropertiesPrinters.put(LavaZone.class, zone -> 
      String.format("Sulphur levels: %d", ((LavaZone) zone).getSulphurLevel());
    }
    

    It is slightly unfortunate that generics aren't powerful enough to let you avoid the cast here, though in practice you can paper over this by making a single method for adding a 'handler' for a given zone which takes care of the casting once.

    Command patterns are somewhat advanced - there's more to read before you use these: I thought I'd give you a general idea of what they are, but I'd try first with making methods that accurately describe the more general thing you are trying to do (such as: Print pertinent information, check for hazards, ask the player if they want to perform the special action that belongs with this zone, etc).