Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to achieve flexible Object Composition?

Motivation behind the question

I've been learning how to do Object Composition in Javascript using Concatenative Inheritance and wondered how I could accomplish something similar in VBA (which doesn't have inheritance).

Object Composition: I'm trying to figure out how to accomplish a "has a" relationship vs. a "is a" relationship. I want to be able to write simple behavior classes where they can be used by combining them together to make more complex classes.

I've created a simple example to demonstrate what I'd like to accomplish.


Example use case

Testing Module

Here are some examples of what might be used. For this question though, I'll just focus on the example use of the Fighter class.

The Fight method is actually calling the Fight method in the CanFight class. It debugs a message and reduces stamina by 1.

'MOST EXCITING GAME OF ALL TIME! =)
Private Sub StartGame()

    Dim Slasher As Fighter
    Set Slasher = New Fighter
    Slasher.Name = "Slasher"

    Slasher.Fight '-> Slasher slashes at the foe!
    Debug.Print Slasher.Stamina '-> 99

    'MAGES CAN ONLY CAST (ONLY HAS MANA)
    Dim Scorcher As Mage
    Set Scorcher = New Mage
    Scorcher.Name = "Scorcher"
    Scorcher.Cast "fireball" '->Scorcher casts fireball!
    Debug.Print Scorcher.Mana '-> 99

    'CAN BOTH FIGHT & CAST (HAS BOTH STAMINA & MANA)
    Dim Roland As Paladin
    Set Roland = New Paladin
    Roland.Name = "Roland"
    Roland.Fight '-> Roland slashes at the foe!
    Roland.Cast "Holy Light" '-> Roland casts Holy Light!

End Sub

Fighter Class

This class has two public properties Name and Stamina.

This class also contains FightAbility which is an instance of the CanFight class. This is my attempt at trying to accomplish composition.

Option Explicit

Private FightAbility As CanFight
Private pName As String
Private pStamina As Long

Private Sub Class_Initialize()
    pStamina = 100
    Set FightAbility = New CanFight
End Sub

Public Property Get Name() As String
    Name = pName
End Property

Public Property Let Name(ByVal Value As String)
    pName = Value
End Property

Public Property Get Stamina() As String
    Stamina = pStamina
End Property

Public Property Let Stamina(ByVal Value As String)
    pStamina = Value
End Property

'This is the function that uses the ability to fight.
'It passes a reference to itself to the `CanFight` class
'giving it access to its public properties.
'This is my attempt at composition.
Public Sub Fight()
    FightAbility.Fight Me
End Sub

CanFight Class

This is the class that can be reused for other characters. An Example is a Paladin class might need to also have the ability to fight.

The obvious issue with how this is laid out is that state is an Object. The user won't know it needs to have Stamina and a Name property unless they look at the code.

Option Explicit

Public Sub Fight(ByRef State As Object)
    Debug.Print State.Name & " slashes at the foe!"
    State.Stamina = State.Stamina - 1
End Sub

Summarizing the question

My example feels broken since there is no structure in place as far as what properties are needed in order to use it.

At the same time, I want to make sure my game characters can be flexible in having their own distinct properties. Examples from above:

  • Fighter uses: canFight (stamina)
  • Mage uses: canCast (mana)
  • Paladin uses both: canFight (stamina) and canCast (Mana)

If I created an ICharacter interface class then I feel like it would be locked into having all the properties for all types of Characters.

My question is how do I achieve structured but flexible Composition like this in VBA?

like image 961
Robert Todar Avatar asked May 28 '19 18:05

Robert Todar


2 Answers

This is a very hard question to usefully answer IMO, mostly because the model is vastly oversimplified.

Public Sub Fight(ByRef State As Object)
    Debug.Print State.Name & " slashes at the foe!"
    State.Stamina = State.Stamina - 1
End Sub

If I made a Barbarian Warrior that fought with a massive warhammer, "slashes at the foe!" would sound like some funny understatement. Who/what is the foe? I know this is all theoretical & simplified (right?), but if we're talking about a game, then the foe needs to actually die at one point, doesn't it?

If we look at how a traditional JRPG might go about this, a Fight method would need to know the state of both the fighter and its target (let's keep the target singular for now), so for a start, it might go like this:

Public Sub Fight(ByVal fighterState As Object, ByVal targetState As Object)
    '...
End Sub

Basically the role of a Fight method would be to assess/implement the changes that need to happen on targetState based on a number of factors involving both fighterState and targetState. As such, a better name for it might be Attack, and we can assume that the fighterState contains information about what piece of weaponry is currently equipped and whether that weapon "slashes", "pierces", "crushes", or simply "hits" the target. Similarly, the targetState can be assumed to contain information about what pieces of armor are equipped on the target, and whether and how this equipment is able to deflect/nullify or reduce the amount of damage received. With such mechanics, we can even have a PoisonBlade slashing the target to deal what was calculated a 76 HP damage, plus a recurring 8 HP poison damage every turn unless the target consumes (or is otherwise given) an Antidote item to cure their poison state.

Now, whether the fighter is a Fighter or a Paladin, or a BlackMage, makes no difference: what the game mechanics needs isn't different properties and members in each character class. In fact, game mechanics couldn't care less what the character classes are, mechanics are the same for everyone regardless: Fight is a UI command, an ability like any other. The character is a BlackMage and has no weapon equipped? Fight off - and deal 1 HP damage, if any. The character is a Paladin and can decide to "fight" or "cast"? UI commands, not character class design.

How we design class modules is not quite like they do in textbooks with Animal and Cat and Dog where the Dog goes "woof" and the Cat goes "meow" and all the code did was invoke Animal.Talk in both cases and poof, glittering polymorphism-through-inheritance!

What I'm getting at is, real-world code doesn't do Cat and Dog classes, not any more than a real-world JRPG game would define different types for each possible character class in the game - an Enum, maybe, and different assets and resources, definitely; adding a new character class to your game should be adding data, not code. But the game mechanics don't need to be bothered with how different a Paladdin can be to a BlackMage or a RedWizard, because the different skills and abilities of a Paladin vs those of a Fighter or a BlackBelt are where composition should come into play.

See they're not different methods, they're different objects.

A Fighter doesn't have "no concept of mana", it's a PlayableCharacter instance that might be composed of a CharacterStats object where both the MP and the MaxMP properties begin the game at 0.

So we take a step back and look at the big picture, and without writing a single line of code, we visualize how things need to coexist and what needs to be responsible for what, in order for the game to be able to make a Paladin slash at a Dragon: as we break down the required components and work out how they all relate to each others, we quickly realize that there's no need to force composition to happen anywhere, it just happens, out of necessity!

some quick, incomplete and roughly approximate class diagram

In a language that supported class inheritance, you might have CharacterAbility as the base/abstract class for things like FightAbility, CastSpellAbility, UseItemAbility, and other classes, each with wildly different implementations for their Execute method. In VBA you can't do that, so instead you might have an ICharacterAbilityCommand interface, and FightAbility, CastSpellAbility, UseItemAbility classes that implement it.

Now we can picture a CombatController class that knows everything about every actor: there's an instance of a KillableGameCharacter named Red Dragon that yields 380 XP and 1200 gold, has a BiteAbility, a ClawAbility, a WingSpikeAbility, and of course a FireBreathAbility - its CharacterStats are such that its FireBreathAbility will deal somewhere between 600 and 800 fire-elemental damage to our Paladin.

Ha! Noticed that? Just by uttering how things interact with each other, we know that ICharacterAbilityCommand.Execute needs to take the CharacterStats of the executing character in order to be able compute just how fierce that dragonfire is. That way we can later reuse the FireBreathAbility for a weaker Wyvern monster. And since we're taking in a CharacterStats object, whether they're the stats of a Paladin, the stats of a Black Mage, the stats of a Red Dragon or those of a Slime, makes no difference whatsoever.

And that sounds very much exactly like the problem you were trying to tackle in the first place - just slightly more abstracted, such that you don't write code that reads like a Dragon Warrior battle transcript ;-)

Kain Attacks!

By having the CharacterEquipment affect the character's CharacterStats on equip, and any stats-affecting transient skills baked into the stats as soon as they're acquired/equipped/activated, we remove the need for ICharacterAbilityCommand.Execute to need anything other than the CharacterStats of the valiant knight/player, and the CharacterStats of the dragon/monster.

like image 59
Mathieu Guindon Avatar answered Sep 27 '22 23:09

Mathieu Guindon


@Robert, I actually like your code. However, I'm not sure it qualifies as composition. Actually, I think you've discovered a 'mixin' pattern, sort of, (or maybe even the visitor pattern) so congratulations on that. Here is composition as I see it.

So with the default member trick, we ship a Base property that allows access to all the base's classes methods (but not private state, which is a good thing IMHO). But because writing foo.Base.Bar in code is ugly, we pull a trick to make the Base property the default member so that it can be replaced with just a pair of brackets. Thus, the composition becomes less ugly to look at and no need for a subclass to replicate all the base class's methods.

'* Test Module
Private Sub StartGame2()

    Dim oPaladin As Paladin
    Set oPaladin = New Paladin
    oPaladin().Name = "Pal"

    oPaladin().Fight '-> Pal slashes at the foe!
    Debug.Print oPaladin().Stamina '-> 99
    Debug.Print oPaladin.Mana
End Sub

The Fighter class

Option Explicit

Private pName As String
Private pStamina As Long

Private Sub Class_Initialize()
    pStamina = 100
End Sub

Public Property Get Name() As String
    Name = pName
End Property

Public Property Let Name(ByVal Value As String)
    pName = Value
End Property

Public Property Get Stamina() As String
    Stamina = pStamina
End Property

Public Property Let Stamina(ByVal Value As String)
    pStamina = Value
End Property

'* This is the function that uses the ability to fight.
'* It passes a reference to itself to the `CanFight` class
'* giving it access to its public properties.
'* This is my attempt at composition.
' Public Sub Fight()
'     FightAbility.Fight Me
'End Sub

Public Sub Fight()
    Debug.Print Me.Name & " slashes at the foe!"
    Me.Stamina = Me.Stamina - 1
End Sub

The Paladin.cls class as exported to disk and amended to pull the default member trick.

VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "Paladin"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

Private moBase As Fighter

'* To do the default member trick
'* 1) Export this module to disk;
'* 2) load into text editor;
'* 3) uncomment line with text Attribute Item.VB_UserMemId = 0 ;
'* 4) save the file back to disk
'* 5) remove or rename original file from VBA project to make room
'* 6) Re-import saved file

Private Sub Class_Initialize()
    Set moBase = New Fighter
End Sub

Public Function Base() As Fighter
    Attribute Item.VB_UserMemId = 0
    Set Base = moBase
End Function


Public Function Mana() As String
    Mana = "I don't know what Mana even means"
End Function
like image 40
S Meaden Avatar answered Sep 27 '22 23:09

S Meaden