Abstract Subclassing
Occasionally records will be classes that are abstract
This article will go over the concepts of Abstract Subclassing through the lens of looking at a specific example found in Skyrim Npc. Other records have similar but different concepts.
Consider Skyrim's NpcConfiguration records:
public interface INpcConfigurationGetter
{
// ...
short MagickaOffset { get; }
short StaminaOffset { get; }
// The field below is an abstract type
IANpcLevelGetter Level { get; }
// ...
}
Why is it Needed?
Why is the Level
field an odd abstract ANpcLevel
object?
The subclassing helps encapsulate some complexity while remaining consistent and type safe.
Consider that NPC_
's Level
field is an integer normally. But if you turn on the PC Level Mult
flag, it suddenly acts as a float. So how can Mutagen expose this in a type safe manner if the field can just change its type depending on a switch somewhere else?
Mutagen exposes this by using subclassing. ANpcLevel
has two implementing classes:
Implementation Class | Type |
---|---|
NpcLevel | integer |
PcLevelMult | float |
These two alternatives allow the same field to contain different types.
Setting an Abstract Subclass Member
You will notice Mutagen does not expose the Pc Level Mult
flag. Instead, you simultaneously control both the "mode" that the Level
is in, as well as Level
's value by choosing the appropriate subclass.
INpc n = ...
// Setting to straight level, with the Pc level Mult flag "off"
n.Configuration.Level = new NpcLevel()
{
Level = 10
};
INpc n = ...
// Setting to Pc Level Mult flag "on", now with float capabilities
n.Configuration.Level = new PcLevelMult()
{
Level = 2.1234f
};
Now, it's very clear when Level
is an integer, and when it is a float. The flag's value and Level
's type are "bundled" as one choice, depending on which subclass you make.
Reading an Abstract Subclass Member
Reading needs to respect/consider these subclasses in the same way. One easy way to do this is using a C# type switch:
INpcGetter n = ...
switch (n.Configuration.Level)
{
case INpcLevelGetter straightLevel:
System.Console.WriteLine($"Npc level was {straightLevel.Level}");
break;
case IPcLevelMultGetter mult:
System.Console.WriteLine($"Npc level multiplier was {mult.Level}");
break;
default:
System.Console.WriteLine("Huh?");
break;
}
Do Not Cast
It is not good practice to hard cast to the desired type
INpcGetter n = ...
// Bad code:
int level = ((INpcLevelGetter)n.Configuration.Level).Level;
PC Level Mult
flag on, as the subobject won't be of type INpcLevelGetter
.
Summary
Abstract Subclassing is used when a concept is complex enough to warrant the need for extra control. It can help with:
- Exposing one field as many types
- Bundling complex configurations into one atomic decision, so that there is no potential for invalid configurations.
In the above example, you will never accidentally deal with a Level
that is of type float
unless it is in Pc Level Mult
mode, and vice versa. That is not the biggest deal, but in many other situations the concepts/differences are more extreme.
Other Records with Abstract Subclassing
Skyrim Npc is not the only record type that uses these concepts. There are many other records that have the need for data structure to change depending on context, and they will use Abstract Subclassing to help expose that.
Some other examples include: Perk Effects
These show a more extreme example where the fields that a Perk Effect contains vary widely depending on the Perk type. The subclassing helps only expose the applicable fields for a given effect type.
An effect can reference many different types of records, where some effect types can point to records of type ABC, while another effect type can only point to records of type XYZ. The subclassing again helps expose only the correct typing depending on the effect type you're dealing with.
Documentation
Each subclassing situation is different and is trying to solve a different complexity specific to that record. As things mature, documentation outlining each specific structure will probably be written.
Additionally, you can investigate the subclassing alternatives yourself without documents:
- Utilize Intellisense, and follow references in the IDE to see the classes and what they contain.
-
Of importance: The interfaces of these abstract classes contain comments of what options are available:
This helps narrow down which types it can be so you know what to switch on and handle./// <summary> /// Implemented by: [NpcLevel, PcLevelMult] /// </summary> public partial interface IANpcLevel { // ... }
-
Also, you can sometimes refer to the xmls that define the records, like the ones linked above.