Как добиться гибкой композиции объектов?

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

Я изучал, как сделать Состав объекта в 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

Истребитель Класс

Этот класс имеет два общедоступных свойства 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

Это класс, который можно повторно использовать для других персонажей. Примером может служить класс 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?

Попробуйте это exceldevelopmentplatform.blogspot.com/2018/09/…

S Meaden 28.05.2019 20:16

Очень похоже: stackoverflow.com/q/53873314/1188513

Mathieu Guindon 28.05.2019 20:17

@MathieuGuindon Я прочитал этот пост, прежде чем задать этот вопрос. Может быть, меня смущает то, что если бы у меня был класс ICharacter, разве мне не пришлось бы иметь все свойства, перечисленные в этом классе? Тогда у моего Fighter тоже будет Mana и все остальное, о чем я могу подумать заранее? Поправьте меня, если я ошибаюсь! Я действительно борюсь по какой-то причине в понимании интерфейсов.

Robert Todar 28.05.2019 20:29
Fighter наличие свойства Mana похоже на Cat наличие метода Purr, который интерфейс IAnimal не предоставляет: если остальная часть кода работает с ICharacter, приведение к какому-то классу Fighter приводит к тому, что LSP терпит поражение =) @SMeaden CanFightсорт, целью которого является поведение сочинять — немного похожее на то, как AIPlayer получает IGameStrategy в моей игре Морской бой.
Mathieu Guindon 28.05.2019 20:52

@MathieuGuindon =) будет ли ICharacter содержать только базовые свойства, которые я могу придумать для всех возможных персонажей (Name, Stamina, Health)? А какое уникальное свойство есть в конкретных классах? Если это так, то что, если бы у меня был класс CanCast, который истощает Mana. Это не сможет работать с ICharacter, поскольку оно не содержит Mana.

Robert Todar 28.05.2019 21:45

У меня есть дела прямо сейчас, но я обязательно свяжусь с вами (если не с ответом здесь, то я прокомментирую со ссылкой на новую статью в блоге) - но да, я бы ICharacter со всеми основные свойства персонажа.

Mathieu Guindon 28.05.2019 22:20

Я просто добавил пример с дополнительными классами в Гитхаб на случай, если это поможет получить лучшую картину.

Robert Todar 28.05.2019 22:25

Но ... вы можете сделать своего рода полиморфизм в VBA: vitoshacademy.com/vba-interfaces-in-vba-как-и-почему также, пожалуйста, объясните, что вы подразумеваете под «композицией объекта», было бы лучше объяснить вашу потребность, просто используя ссылку.

pdem 01.07.2019 17:15

@pdem Я внес больше ясности в то, что я имею в виду под композицией объектов. Я также добавил примечание о том, почему я могу нервничать, используя интерфейс, так как мне кажется, что он заставит моих персонажей делиться всеми теми же свойствами, включая те, которые я бы не хотел, чтобы они имели.

Robert Todar 01.07.2019 19:19

@MathieuGuindon просто смотрит, будет ли статья в блоге или ответ на столе когда-нибудь в будущем? Никакого давления, просто не получая лучшего понимания, которое я надеялся получить от этого вопроса.

Robert Todar 01.07.2019 19:46

Спасибо за напоминание - я начал работать над этим, отвлекся, и оно просто исчезло ... Сейчас иду с ответом.

Mathieu Guindon 01.07.2019 20:04
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
6
11
764
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Таким образом, с помощью трюка с членом по умолчанию мы отправляем свойство 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

Класс Истребитель

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

Не будет ли выставление Fighter в качестве базового класса в Paladin, а затем выставление всего Fighter? Кажется, что это тесно связывает их вместе. Это сделало бы Paladin все, что есть в боевике, и даже больше! На самом деле у Fighter, вероятно, есть свои преимущества, такие как больше атак и различные свойства и методы. На самом деле все, что я хотел бы показать, это метод Fight в этом примере.

Robert Todar 28.05.2019 21:24

хорошо, я начинаю лучше понимать, что вы хотите. Возможно, именно поэтому вы отметили вопрос «Внедрение зависимостей». Недавно я посетил это в своем блоге exceldevelopmentplatform.blogspot.com/2019/05/…

S Meaden 28.05.2019 22:47

@SMeaden Я так рад, что нашел ваш блог. Любить это!

ArcherBird 27.06.2019 17:03
Ответ принят как подходящий

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

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 урона ХП, если есть. Персонаж является Paladin и может решить «сразиться» с или «кастом»? Команды пользовательского интерфейса, а не дизайн классов персонажей.

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

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

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

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 дракона/монстра.

пуф, сверкающий полиморфизм через наследование +
QHarr 02.07.2019 09:29

Я не могу перестать смеяться, как вы описали Красного Дракона со всеми его методами, я изобразил массивного зверя, а не дракона на картинке LOL

Jose Cortez 24.06.2020 01:59

Другие вопросы по теме