Как добиться гибкой композиции объекта? - PullRequest
7 голосов
/ 28 мая 2019

Мотивация за вопросом

Я учился делать Композицию объектов в Javascript, используя Конкатенационное наследование , и удивлялся, как я могу сделать что-то подобное в VBA (что не ' не имеет наследства).

Состав объекта : Я пытаюсь выяснить, как добиться отношения "имеет" против отношения "есть". Я хочу иметь возможность писать простые поведенческие классы, где их можно использовать, комбинируя их вместе для создания более сложных классов.

Я создал простой пример, чтобы продемонстрировать, чего я хотел бы достичь.


Пример использования

Модуль тестирования

Вот несколько примеров того, что может быть использовано. В этом вопросе я остановлюсь только на примере использования класса Fighter.

Метод Fight фактически вызывает метод Fight в классе CanFight. Отлаживает сообщение и снижает выносливость на 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

Этот класс имеет два открытых свойства Name и Stamina.

Этот класс также содержит FightAbility, который является экземпляром класса CanFight. Это моя попытка создать композицию.

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

Это класс, который можно использовать для других персонажей. Примером является Paladin класс, возможно, также должен иметь способность бороться .

Очевидная проблема с тем, как это изложено, состоит в том, что state является Object. Пользователь не будет знать, что ему нужно иметь свойство Stamina и Name, если он не смотрит на код.

Option Explicit

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

Резюмируя вопрос

Мой пример выглядит испорченным, поскольку нет структуры, необходимой для использования его свойств.

В то же время я хочу убедиться, что мои игровые персонажи могут быть гибкими, обладая собственными отличительными свойствами. Примеры сверху:

  • Fighter использует: canFight (выносливость)
  • Mage использует: canCast (мана)
  • Paladin использует оба значения: canFight (выносливость) и canCast (мана)

Если бы я создал интерфейсный класс ICharacter, я чувствую, что он будет заблокирован, чтобы иметь все свойства для всех типов символов.

У меня вопрос, как мне добиться структурированного, но гибкого Композиции , как это в VBA?

Ответы [ 2 ]

4 голосов
/ 28 мая 2019

@ Роберт, мне действительно нравится твой код.Однако я не уверен, что это квалифицируется как композиция.На самом деле, я думаю, что вы обнаружили шаблон «смешивания», вроде (или, возможно, даже шаблон посетителей), так что поздравляю с этим.Вот композиция, как я ее вижу.

Итак, с помощью трюка с членом по умолчанию мы поставляем свойство Base, которое разрешает доступ ко всем методам базовых классов (но не к закрытому состоянию, что, по-моему, хорошо).Но поскольку запись foo.Base.Bar в коде выглядит ужасно, мы пытаемся сделать свойство Base элементом по умолчанию, чтобы его можно было заменить парой скобок.Таким образом, композиция становится менее уродливой для просмотра, и нет необходимости в подклассе для репликации всех методов базового класса.

'* 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

Класс Fighter

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

Класс Paladin.clsкак экспортировано на диск и изменено, чтобы использовать трюк члена по умолчанию.

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
3 голосов
/ 02 июля 2019

Это очень сложный вопрос, на который полезно ответить ИМО, в основном потому, что модель сильно упрощена.

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

Если бы я сделал Варварского Воина , который сражался с массивным боевым молотом, "поразил врага!" будет звучать как забавное преуменьшение. Кто / что является врагом ? Я знаю, что все это теоретически и упрощенно (верно?), Но если мы говорим об игре, то враг должен умереть в один момент, не так ли?

Если мы посмотрим, как традиционная JRPG может пойти по этому поводу, метод Fight должен знать состояние как бойца, так и его цели (давайте пока оставим цель в единственном числе), поэтому для начала, он может пойти так:

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

По сути, роль метода Fight заключается в оценке / реализации изменений, которые должны произойти в targetState, на основе ряда факторов, включающих как fighterState, так и targetState. Таким образом, лучшим названием для этого может быть Attack, и мы можем предположить, что fighterState содержит информацию о том, какая часть оружия в настоящее время экипирована и является ли это оружие «рубящим», «пробивающим», «сокрушающим» или просто «поражает» цель. Точно так же можно предположить, что targetState содержит информацию о том, какие части доспехов экипированы на цель, и может ли это оборудование отклонять / сводить на нет или уменьшать количество полученного урона. С такой механикой у нас может даже быть PoisonBlade, сокращающий цель, чтобы нанести то, что было вычислено, урон 76 HP, плюс повторяющийся урон 8 HP яда каждый ход, если цель не потребляет (или не получает иное) предмет Antidote для вылечить их отравленное состояние.

Теперь, независимо от того, является ли боец ​​Fighter или Paladin, или BlackMage, не имеет значения: для механики игры нужны разные свойства и члены в каждом классе персонажей. На самом деле, игровая механика не заботится о классах персонажей, механика одинакова для всех, независимо от того, Fight - команда пользовательского интерфейса, такая же способность, как и у любого другого. Персонаж является BlackMage и не имеет вооружения? Отбейте - и нанесите 1 HP урона, если есть. Персонаж Paladin и может решить "драться" или"бросить"? Команды пользовательского интерфейса, а не дизайн класса персонажей.

То, как мы проектируем модули классов, не совсем то, что они делают в учебниках с Animal и Cat и Dog, где Dog идет "гав", а Cat - "мяу" и весь код did был вызывать Animal.Talk в обоих случаях и poof, блестящий полиморфизм через наследование!

Я понял, что реальный код не выполняет классы Cat и Dog, не более чем реальная игра JRPG будет определять различные типы для каждого возможного класса персонажей в игре. - Enum, может быть, и различные активы и ресурсы, безусловно; добавление нового класса персонажа в вашу игру должно добавлять данные , а не код. Но игровая механика не должна беспокоиться о том, насколько может отличаться Paladdin от BlackMage или RedWizard, потому что различные умения и способности Paladin по сравнению с Fighter или BlackBelt - это , где композиция должна войти в игру.

Видите, они не разные методы , они разные объекты .

A Fighter не имеет «никакого понятия маны», это PlayableCharacter экземпляр, который может быть составлен из CharacterStats объекта, где и MP, и MaxMP свойства начинают игру в 0.

Таким образом, мы делаем шаг назад и смотрим на общую картину, и без написания единственной строки кода , мы визуализируем, как вещи должны сосуществовать и что должно нести ответственность за что, для того, чтобы игра, способная сделать Paladin удар в Dragon: когда мы разбиваем необходимые компоненты и выясняем, как они все связаны друг с другом, мы быстро понимаем, что нет необходимости форсировать композиция может произойти где угодно, это просто случается , по необходимости!

some quick, incomplete and roughly approximate class diagram

В языке, который поддерживает наследование классов, вы можете иметь CharacterAbility в качестве базового / абстрактного класса для таких вещей, как FightAbility, CastSpellAbility, UseItemAbility и других классов, каждый из которых имеет совершенно разные реализации для своих Execute метод. В VBA вы не можете этого сделать, поэтому вместо этого у вас может быть интерфейс ICharacterAbilityCommand и классы FightAbility, CastSpellAbility, UseItemAbility, которые его реализуют.

Теперь мы можем изобразить класс CombatController, который знает все о каждом актере: есть экземпляр KillableGameCharacter с именем Red Dragon, который дает 380 XP и 1200 золота, имеет BiteAbility, ClawAbility, WingSpikeAbility и, конечно, FireBreathAbility - его CharacterStats таковы, что его FireBreathAbility нанесет где-то между 600 и 800 уронами от стихийных огней нашему Паладину.

Ха! Заметил это? Просто излагая, как вещи взаимодействуют друг с другом, мы знаем, что ICharacterAbilityCommand.Execute должен взять CharacterStats исполняющего персонажа, чтобы вычислить, насколько яростен этот драконий огонь. Таким образом, мы можем позже использовать FireBreathAbility для более слабого монстра Wyvern. И поскольку мы берем объект CharacterStats, будь то характеристики Паладина , характеристики Черного Мага , характеристики Красного Дракон или Слизь , не имеет никакого значения.

И , что звучит очень точно так же, как проблема, которую вы пытались решить в первую очередь - чуть более абстрактно, так что вы не пишете код, который читается как Воин Дракона стенограмма боя; -)

Kain Attacks!

Имея CharacterEquipment, влияющую на CharacterStats персонажа на экипировку, и любые переходные навыки, влияющие на характеристики, выпекаемые в статистику, как только они приобретены / экипированы / активированы, мы убираем необходимость в ICharacterAbilityCommand.Execute для нужно что-нибудь кроме CharacterStats доблестного рыцаря / игрока и CharacterStats дракона / монстра.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...