I have a large application in Java filled with independent classes which are unified in a PlayerCharacter
class. The class is intended to hold a character’s data for a game called the Burning Wheel, and as a result, is unusually complex. The data in the class needs to have computations controlled by the UI, but needs to do so in particular ways for each of the objects in the class.
There are 19 objects in the class PlayerCharacter
. I’ve thought about condensing it down, but I’m pretty sure this wouldn’t work anywhere in the application as it stands so far.
private String name = ""; private String concept = "";
private String race = ""; private int age = -1;
private ArrayList<CharacterLifepath> lifepaths = new ArrayList<>();
private StatSet stats = new StatSet();
private ArrayList<Affiliation> affiliations = new ArrayList<>();
private ArrayList<Contact> contacts = new ArrayList<>();
private ArrayList<Reputation> reputations = new ArrayList<>();
//10 more lines of declarations
I’ve been considering this problem for some time now, and have considered multiple
approaches. The problem arises primarily when data is deleted – for instance, since pretty much everything else (but not some parts!) depends upon Lifepaths, when a lifepath is deleted, nearly everything else must be recalculated. However, if a Skill is deleted, only a few things must be recalculated.
Additionally, the application must somewhere track certain values; skill points, trait points, etc. to ensure that the user is not unintentionally exceeding those values.
So my question is generally as follows: Where should everything go? What makes this easiest? There are a couple options:
- Place point total calculations in the PlayerCharacter class (but how does this generate a warning?)
- Handle all calculation outside of the PlayerCharacter class, and just use PlayerCharacter as a container for all the character’s information
- Place all calculations in the PlayerCharacter class; after each item is changed, recompute the entire character. Then, if there are issues arising from deletion, throw warnings back at the UI.
I’m slightly overwhelmed by the scope of this particular class – whereas everything I’ve done so far has been easily broken down into small manageable chunks, this beast seems to resist being tamed. If there’s a better approach to this, I’m all ears! But as of right now, I’m slightly confused, and progressing aimlessly probably won’t get me anywhere. Any advice is appreciated.
I apologize if this is unclear – I’m certain I lack the software vocabulary to properly communicate my ideas. I would love to improve this question – any help here is appreciated!
Separate interface from implementation. “PlayerCharacter” should be an interface with methods like “getStats”, “getContacts”, “getReputation” etc. This gives you freedom to play with the implementation without having to change the code which uses this interface.
The implementation can choose to cache values and recompute them if necessary. For example a method “getStats” would return the value of “stats” variable; and if the variable is empty, it would be recomputed first. Methods like “removeLifepath” could remove all cached lifepath-related values.
The methods for actual recomputation could be placed to yet another class, if needed. Thus you would have: A) the interface, B) one or more classes doing the calculation, and C) an implementation keeping the cached values, and calling recalculation when necessary.
Quick sketch:
interface PlayerCharacter {
List<Lifepath> getLifepaths();
void addLifepath(Lifepath lifepath);
void removeLifepath(Lifepath lifepath);
Skills getSkills();
}
class PlayerCharacterImpl {
List<Lifepath> lifepaths = new ArrayList<>();
Skills cachedSkills = null;
public List<Lifepath> getLifepaths() {
return Collections.unmodifiableList(lifepaths);
}
public void addLifepath(Lifepath lifepath) {
lifepaths.add(lifepath);
cachedSkills = null;
}
public void removeLifepath(Lifepath lifepath) {
lifepaths.remove(lifepath);
cachedSkills = null;
}
// thread-unsafe
public Skills getSkills() {
if (null == skills) {
skills = PlayerCharacterCalc.calculateSkills(lifepaths);
}
return skills
}
}
class PlayerCharacterCalc {
static Skills calculateSkills(List<Lifepath> lifepaths) {
Skills skills = new Skills();
for (Lifepath lifepath : lifepaths) {
skills.apply(lifepath);
}
return skills;
}
}
Isolate & Encapsulate Complexity
Pull calculations into their own “function” classes. Their structure and relationships is very highly dependent on your existing design.
Maybe turn affiliations
, contacts
, etc. into classes. This makes PlayerCharacter more usable by “function” classes.
With PlayerCharacter now a composite of other classes and individual properties as appropriate you can easily create “data” classes. These data classes are intended as customizations for the functional classes. The function class code will have the look and feel of dealing with a coherent class, not a jumble of PlayerCharacter and other classes.
Scrub your Design
You will need to re-analyze to make sure you get fundamental functionality into the right classes. For example each “data” class may have its own “delete” function that does very specific and minimal things. A “function” class, likewise, will orchistrate all the “data” classes in the overall delete process and and do “delete-y” things that are of an inter-class context.
Get a High-Level View of Design Patterns
Learn some basics about the various patterns. This will give you some insight as to how you might tackle the problem. Don’t sweat the class diagrams just get the ideas and motivation behind them. I’m not saying you should explicitly implement a particular pattern or any pattern at all. You’re just looking for inspiration.
Flyweight – Shared State.
Visitor – Performing the same operation on a collection of different types
Template Method – defines an algorithm in a base class using abstract operations that subclasses override to provide concrete behavior.
Adapter –
Convert the interface of a class into another interface clients expect.
Adapter lets classes work together, that could not otherwise because of incompatible interfaces.
Façade – Provide a unified interface to a set of interfaces in a subsystem. Façade defines a higher-level interface that makes the subsystem easier to use.
This will be a partial answer since I’m not a regular user of Java (more of a C++ guy myself).
Here’s a few ideas that could guide you :
1) If a attribute may be edited by multiple sources (a Stat calculated with a base value, bonus/malus from a skill and bonus from multiple lifepath), perhaps should you not save it as a single value, but as a association of value; and it could be used to limit the total value if you wand to be sure it will not leave some boundaries. For example, something like that :
public class StatModifier {
public String source; // the 'editing' source (ex : a lifepath)
public String target; // the edited Stat (ex : strength)
public int modifier;
}
public class Stat {
private int base;
private ArrayList<StatModifier> modifiers;
private int computedValue;
private int actualValue;
private bool computed;
public function addModifier(modifier) { // removeModifier() is basically the same
modifiers.add(modifier);
computed = false;
}
public function getValue() {
if (!computed) {
computedValue = base;
// TODO : here compute value (ex : sum modifiers)
actualValue = Math.min(computedValue, 100); // Max stat value : 100
computed = true;
}
return actualValue;
}
}
If you use this method, then the modifications of a Lifepath is a list of modifiers, and when you add/remove a lifepath, you just have to add/remove the associated modifiers. Those modifiers would logically be present within the Lifepath instance, and the PlayerCharacter class will only have to get this list and process it.
2) if you need to limit the total value of a group (say, a character cannot have more than 50 skill points across all skills), you could use a SkillSet class (for exemple) who would work as a list of skills and a validator. That way, the PlayerCharacter class only have to request the validate() method of all these class to be sure that the character is valid, and the actual validation rules will be managed set by set.
3) keep a list of the modification done since the last validation. That way, if you detect that the user is not valid, you can rollback them.
4) if you use ‘1)’, a validation step could also be to check that the sources of each modifier (still) exists.