Interfaces and abstract methods in Enum (Java)

Enum is widely used in Java to define fixed set of values. It provides more type-safety and increases code’s readability in many cases. Most people are also aware that it is a special kind of class that cannot extend any class because it already extends java.lang.Enum abstract class. But only a few know that Enum can implement interfaces and define abstract methods.

What is the purpose of these features? Can it really be useful? Let’s analyse an example where it could be applied.

Examples

Imagine a simple 2D game where the game’s world consists of two kinds of objects:

  • Characters with health points and magic points. They can move and attack their enemies to decrease their health points. Attacks can be performed using a sword or magic (which requires and consumes magic points).
  • Items – health potions and magic potions which can be collected by characters in order to refill their health/magic points to maximum. They are placed in some places and cannot be moved.

The purpose of this game is to defeat all enemies by decreasing their health points to zero. Each character is controlled by AI so we need some algorithms to decide what they do in order to win depending on the current game’s state.

Firstly, let’s define the base class for all game objects.

public class GameObject {

  private double x;
  private double y;

  public GameObject(double x, double y) {
    this.x = x;
    this.y = y;
  }
  
  // getters, setters and other helper methods
}

This way we know where each object is currently placed in the world. Then let’s define items.

public class Item extends GameObject {

  private final ItemType type;
  private boolean collected;

  public Item(ItemType type, double x, double y) {
    super(x, y);
    this.type = type;
  }

  // getters, setters and other helper methods
}

The class includes the collection status and type of an in-game item. The collection status indicates if the item has been consumed by any character in the game and the type is specified using an enumerated data type (Enum).

Item types – Enum with an abstract method
public enum ItemType {

  HEALTH_POTION {
    @Override
    public void applyTo(Character character) {
      character.setHealthPoints(Character.MAX_HEALTH);
    }
  },
  MAGIC_POTION {
    @Override
    public void applyTo(Character character) {
      character.setMagicPoints(Character.MAX_MAGIC);
    }
  };

  public abstract void applyTo(Character character);
}

This example demonstrates the use of an abstract method in Enum. This feature allows for concise code and the utilization of Enum’s advantages. What is the alternative way?

interface ApplicableItem {
  void applyTo(Character character);
}

class HealthPotion implements ApplicableItem {
  @Override
  public void applyTo(Character character) {
    character.setHealthPoints(Character.MAX_HEALTH);
  }
}

class MagicPotion implements ApplicableItem {
  @Override
  public void applyTo(Character character) {
    character.setMagicPoints(Character.MAX_MAGIC);
  }
}

But this approach has several drawbacks, e.g.:

  • Utilizes more classes than the previous one. An interface and separate class for each item type are used instead of a single Enum. In this example, there is 1 interface and 2 classes, as opposed to 1 enum class and 2 enum values.
    However, if additional item types were to be added, the number of classes would increase. Using an Enum would still only require adding more values to the single Enum.
  • Doesn’t have a fixed set of values like Enums do, allowing for the potential addition of new item types. However, this limitation can be partially addressed by making ApplicableItem interface sealed, only allowing HealthPotion and MagicPotion to implement it.
    But this feature is only available in Java 15 and later versions as a preview feature, making it not easily implementable in earlier versions.
  • Lacks the built-in functionalities of Enums such as values() method, == comparison, implementation of Serializable and Comparable interfaces etc. While these features can be replicated with additional code, it increases complexity in design and implementation and requires more effort – especially when trying to implement counterpart of values() method.
Character AI game strategies – Enum implementing interface

To make the game functional, characters need to be able to move, collect items and attack. Each character may have its own strategy for the game. One solution is to establish a set of fixed strategies and assign them to characters, allowing for reusability. This can be accomplished by defining an Enum with an abstract method, similar to the approach used for the ItemType.

public enum GameStrategy {

  ONLY_ATTACK_ENEMIES {
    @Override
    public void performAction(Game game, Character character) {
      Character enemy = game.getFirstAliveCharacterOtherThan(character);
      character.attackOrMoveTowardEnemy(enemy);
    }
  },
  COLLECT_ITEMS_THEN_ATTACK {
    @Override
    public void performAction(Game game, Character character) {
      if (game.hasAnyItemsNotCollected()) {
        Item item = game.getFirstNotCollectedItem();
        character.collectOrMoveTowardItem(item);
      } else {
        Character enemy = game.getFirstAliveCharacterOtherThan(character);
        character.attackOrMoveTowardEnemy(enemy);
      }
    }
  };
  
  public abstract void performAction(Game game, Character character);
}

While this approach may work, it lacks the ability to introduce more modularity in the game or allow others to develop their own game strategies while preserving the existing basic strategies.
Using interfaces is a better way to go in this case. We can start by defining an interface for various game strategies. This makes it more flexible and easier to add new things in the future.

public interface GameStrategy { // in core module

  void performAction(Game game, Character character);
}

With this approach, basic implementations of the interface can be provided as separate classes but it is also possible to accomplish this using a single Enum.

public enum BasicGameStrategies implements GameStrategy { // in core module

  ONLY_ATTACK_ENEMIES {
    @Override
    public void performAction(Game game, Character character) { // ... }
  },
  COLLECT_ITEMS_THEN_ATTACK {
    @Override
    public void performAction(Game game, Character character) { // ... }
  }
}

By using this method, it becomes easy for other modules to define additional strategies.

public enum AdditionalGameStrategies implements GameStrategy { // in additional/external module
  COLLECT_HEALTH_POTION_IF_LOW_HP {
    @Override
    public void performAction(Game game, Character character) { // ... }
  },
  COLLECT_MAGIC_POTION_IF_LOW_MANA {
    @Override
    public void performAction(Game game, Character character) { // ... }
  }
}

Some might prefer to use separate classes for each game strategy, but one of the reasons that convince me to use Enum implementing interfaces, in this case, is the ability to group strategies. Each Enum can define a single group, so if we have many strategies, we can categorize them, for example, as offensive, defensive and balanced.

Drawbacks

Readability and maintainability

The approach presented here can be useful in reducing code complexity, but it may also have an impact on readability and maintainability. The risk of negative effects increases with more code in the Enum class, for example, when:

  • there are many values in the Enum – even if the method implementations are simple, there will be many lines of code, making it harder to maintain the code,
  • method implementations are long and consist of many lines of code.
Dependencies

Enums do not easily allow for dependency injection. This can lead to difficulties in reusing code implemented in other classes and may result in a less reusable codebase. While it is technically possible to use static methods and fields to work around this limitation, it is not recommended. It is best to use Enum methods for simple cases and consider alternative approaches for more complex logic.

Summary

The ability to define abstract methods in Enums and implement interfaces within them can be a valuable tool in certain situations. While alternative approaches exist, the approaches described here can provide improved code in terms of readability and complexity in some situations. It is important to keep all options in mind when deciding on the design of the code and weigh the pros and cons before making a decision.

Full code

Full game’s code (there is much more than presented in this article) can be found in my GitHub’s repository.

Leave a Reply

Your email address will not be published. Required fields are marked *