kibernetics писал(а):Т.е., например,
IAnimals
IDogs
затем, в классе Dogs, я могу реализовать сразу два интерфейса?
class Dogs
implements IAnimals
implements IDogs
Почему у тебя классы (да и интерфейсы) в своём названии имеют множественное число? У тебя, что, один экземпляр класса Dogs будет олицетворять не одну собачку, а сразу несколько собачек? Какое-то неопределённое число собачек? Отсюда возникает предположение, а понимаешь ли ты вообще концепцию и фишку «классов» в ООП, или для тебя классы — это просто способ «тематически» раскидать функции по каким-то контейнерам-для-кода с более-менее осмысленными именами, примерно как файлы раскидывают по папкам чисто для поддержания порядока?
В ином случае любой человек назвал бы класс так, чтобы в его имени предмет был в единственном числе, потому что имя класса становится новым типом для переменных, а где ты видел, чтобы в названиях типа было множественое число?
У тебя же
- Код: Выделить всё
Dim FileName As String
Dim X As Long
Dim Y As Long
а не
- Код: Выделить всё
Dim FileName As Strings
Dim X As Longs
Dim Y As Longs
Единственный случай, где классу было бы логично иметь название именно во множественном числе, это когда предполагается, что что класс, точнее экземпляры класса должны олицетворять не отдельных собачек, а коллекции собак. Но если так, то я не вижу смысла для класса, олицетворяющего коллекцию собак, поддерживать интерфейсы
IAnimals и
IDogs. Какие-такие методы нужны классу «коллекция собак», которые при том ещё и предполагается, что должны уметь поддерживать ещё какие-то другие коллекции (и что это будут за коллекции?). Коллекциям всего, чего угодно нужно, наоборот, быть максимально полиморфными между собой — в лучшем случае они все должны поддерживать какой-нибудь интерфейс для унифицированного перечисления содержащихся в них объектов.
Ладно, предположим, это просто опечатка и ты правильно понимаешь суть классов.
Тогда у нас интерфейсы
IAnimal и
IDog и класс
CDog. Ну, да, пример релевантен вопросу об использовании интерфейсов, но не очень-то жизненный по целому ряду вещей.
Во-первых, что это за программа, где приходитсяи иметь такие интерфейсы и классы? Это какая-то экспертная система биологических особей? Или это игра/симулятор виртуальной реальности?
Предположим, что это игра/симулятор для дальнейших примеров.
Первая проблема: чисто логически становится очевидно, что интерфейс
IDog должен наследоваться от
IAnimal, потому что собаки являются частным случаем животных, и всё, что умеют делать животные, собаки тоже должны делать. Например, у интерфейса IAnimal явно должен быть метод
Die, который олицетворяет смерть животного и реализация которого должна делать все вещи, соответствующие переводу животного из живого состояния в мёртвое (начиная от проигрывания соответствующих звуков и порождения/отрисовки соответствующих спецэффектов, заканчивая изменением внутренних переменных и остановки в объекте каких-то внутренних процессов, например процесса поиска врагов от которых могло бы иметь смысл убегать, или жертв, которые бы имело смысл преследовать). Но тогда и у интерфейса
IDog должен быть метод
Die, ведь собаки тоже умирают. И тут мы встречаемся с тем фактом, что в VB
нельзя объявить интерфейс, который был унаследован от другого интерфейса — из нашего же проекта или из внешнего мира.
Не получится в VB создать
IDog и каким-то образом указать, что он унаследован от
IAnimal.
Справедливости ради, такие интерфейсы (с наследованием одного от другого) можно легко написать на MIDL-е, скомпилировать TLB и её подключить в VB-проект:
- Код: Выделить всё
[uuid(09AFBE23-7BCA-4491-998E-45DFEE734111)]
library Example
{
[uuid(A7EBE723-B0C4-4403-9502-FFBF2A983235)]
interface IAnimal : IUnknown
{
HRESULT Die();
HRESULT GetTemperature([out, retval] float *temp);
}
[uuid(61AFB4FF-FD2F-4786-8D36-30C779C59744)]
interface IDog : IAnimal
{
HRESULT Bark();
}
}
Батник для компиляции:
- Код: Выделить всё
midl example.idl /win32 /out .
pause
- interfaces-from-tlb.png (7.96 Кб) Просмотров: 1080
Проблема только в том, что VB всё равно не даст реализовать такую пару интерфейсов в классе CDog:
- no-way-to-implement-inherited-ifaces.png (9.99 Кб) Просмотров: 1080
Причина такого запрета понятна на первый взгляд, но непонятна при более глубоком рассмотрении — если бы у меня был доступ к исходникам VB, я бы этот запрет устранил
В таком случае много кто попытался бы выкрутиться: интерфейс IDog сделал бы не как унаследованный от
IAnimal, а полностью самостоятельный, продублировав в него все методы из
IAnimal. Такое можно и в IDL/TLB-подходе сделать, и в подходе при создании интерфейсов прямо в VB-проекте.
Но это крайне некрасивое решение. Потому что в таком случае у нас получается, что у
IAnimal есть свой метод
Die, а у
IDog есть свой метод
Die. С точки зрения VB, COM, и с точки зрения здравомыслящих людей — коль скоро это два интерфейса, у каждого есть метод Die() и интерфейсы не унаследованы друг от друга, это два совершенно разных метода Die().
И тогда наш класс, который собирается одновременно реализовать и IAnimal, и IDog, должен реализовать два метода Die: общеживотническую смерть и собачью смерть. А это очень глупо. Конечно и из этого можно выкрутиться, реализовав оба метода как-то вот так:
- Код: Выделить всё
Private Sub IAnimal_Die()
RealDie
End Sub
Private Sub IDog_Die()
RealDie
End Sub
Private Sub RealDie()
' здесь весь фарш
' здесь весь фарш
' здесь весь фарш
' здесь весь фарш
' здесь весь фарш
' здесь весь фарш
' здесь весь фарш
' здесь весь фарш
' здесь весь фарш
' здесь весь фарш
' здесь весь фарш
End Sub
Но это очень грязн и некрасиво.
Вторая проблема в том, что я не вижу смысла иметь интерфейс
IDog и класс
CDog, если классом
CDog всё ещё и окончится. Кто ещё, то есть какой ещё другой класс будет имплементировать интерфейс
IDog в самом скором будущем или в гипотетическом будущем? Если никто, то никакого проку от такого отдельного интерфейса не будет, особенно если учитывать, что в VB при создании модуля класса сразу же появляется и одноимённый класс, и одноимённый интерфейс. И так — для любоого VB-класса.
Единственным исключением был бы случай, когда мы делаем проект типа ActiveX DLL/ActiveX EXE с классом типа PublicNotCreatable — тогда иметь отдельный интерфейс IDog был бы хоть какой-то смысл, интерфейс бы в таком случае играл роль «контракта» для других программистов, которые захотят реализовать у себя собачье поведение, совместимое с собачьим поведением нашего объекта, да причём так, что какая-то третья сторона сможет воспользоваться совершенно разными объектами от разных авторов, но одинаковым и унифицированным образом предоставляющим своё собачье поведение этой третьей стороне.
Вот если бы в твоём примере был бы интерфейс
IDog, а классы
СBullterrier,
CSpaniel,
CPoodle,
CBoxer, реализующие этот интерфейс — вот тогда бы я сказал, что это хороший пример.
Это как в той поговорке, что плох тот замок, которые открывается любым ключом, но хорош тот ключ, который открывает любой замок. В каком-то смысле интерфейс — это ключ к получении доступа к свойствами и методам объектов, и хороший прок от использования интерфейсов состоит в том, чтобы, когда есть разнородные объекты, имеющие какие-то общие черты, получать доступ к общим чертам этих совершенно разных объектов
единым спобом. Не зная, не запариваясь, не задумываясь о том, какой именно объект стоит за интерфейсной ссылкой.
Причём, не обязательно хорошим примером будет только тот, где один интерфейс наследуется многими классами. Другой хороший пример — это когда у нас есть интерфейс
IHuman с общечеловеческим поведением,
IDog с общесобачьим поведением (метод «Гавкнуть», метод «ЛовитьБлох»), и класс
CSharikoff, имплементирующий оба этих интерфейса
.
Третий момент состоит в том, что не всегда (особенно в VB) выгодно иметь интерфейсы, которые соответствуют понятиям предметной
области проекта. А иногда продуктивнее подумать об интерфейсах, которые олицетворяют служебные и/или характеристические аспекты объектов.
Вот продолжим пример с игрой: пусть у нас речь идёт об игре, где существует множества разных существ — кошечек, собачек, птичек, монстров, зомби, людей —, а также неживые объекты, такие как бочки, коробки, летящие ракеты, огненные шары, аптечки и т.п.
Вместо того, чтобы иметь в таком проекте интерфейсы типа
IAnimal и
IDog, нужно посмотреть на это с совершенно иной стороны. Не нужно интерфейсами пытаться копировать предметную область, которая и так представлена классами. Тем более дублировать интерфейсами иерархию объектов, особенно с учётом того, что в VB с этим не напряжёнка.
Гораздо лучше будет подумать вот о таких интерфейсах:
- Интерфейс IDestructable с членами:
- Методом TakeDamage(ByVal Amount As Single) — вызывается при нанесении игровому объекту урона.
- Свойством Health — показателем текущего здоровья/степени разрушенности.
- Свойством или функцией KeepDead, показывающим, нужно ли по мнению объекта оставлять его в наборе игрововых объектов после того, как он умер/уничтожен, или его надо удалить из набора игровых объектов.
Этот интерфейс должны будут поддерживать все объекты, которые могут быть уничтожены. Это и «главный герой», и всевозможные его враги, и какие-нибудь взрываемые бочки на уровне или разрушаемые деревянные ящики. Все классы, олицетворяющие подобные объекты, будут иметь Implements IDestructable и каким-то образом реализовать его методы (а также другие члены, типа свойств). Скорее всего примерно одинаковым образом, но вовсе не обязательно. И в то же время на игровом уровне могут существовать какие-нибудь объекты типа каменных статуй, уничтожение которых вообще не предусмотрено. Логично, что классы, олицетворющие такие объекты, вообще не будут имплементировать IDestructable. Также какая-нибудь висящая в небе луна или облака — в них тоже, сколько не стреляй, их не уничтожить, так что эти объекты не будут, по задумке, поддерживать этот интерфейс. Или какой-нибудь противник на уровне, который появляется если мы приближаемся не туда, куда нужно, и который по нашему замыслу бессмертен (но воевать с ним не нужно, нужно просто покинуть нежелательную зону).
- Интерфейс IPhysicalObject с такими методами как:
- GetMass — возвращающий массу объекта, что нужно для физических расчётов.
- TakeImpulse — вызывается, когда в результате физических взаимодействий объекту сообщается механический импульс.
Любой объект в игре, которые должен падать, сталкиваться и подчиняться другим законам физике, должен поддерживать этот интерфейс. И объект, олицетворяющий игрока, и противники, и всякие бочки и вспомогательные предметы — будут его поддерживать. А вот солнце и облачка на небе, на том уровне абстрагирования от реальности, который действует в игре, не будут являться физическими объектами и не будут поддерживать этот интерфейс. На них не будет воздействовать гравитация и другие воздействия. В таком подходе код, обеспечивающий действие гравитации на все объекты, будет выглядеть так:
- Код: Выделить всё
For Each obj In World
' ...
' Любые другие действия со всеми объектами
' ...
' Просчёт гравитации:
If TypeOf obj Is IPhysicalObject
Dim PhysObj As IPhysicalObject
Set PhysObj = obj
PhysObj.TakeImpulse 9.8 * PhysObject.GetMass() * DeltaTime ' Сила m*g за время dt сообщает телу импульс m*g*dt
Set PhysObj = Nothing
End If
' ....
' Любые другие действия со всеми объектами'
' ...
Next Obj
- Интерфейс IPawn или IActor, олицетворяющий любые присутствущие в игровом мире объекты: в нём будут методы для определения/получения/задания координат (X, Y, Z), поступательной скорости, вращательной скорости, проверки видимости на экране и всё такое прочее. Тут и игрок, и собачка, и птичка, и облака на небе, и солнце/луна — все будут поддерживать IPawn/IActor. А вот объект, олицетворяющий фоновую музыку, играющую на уровне, очевидно, не будет, потому что у него нет никаких координат в игровом мире, нет никакой скорости.
- Интерфейс ILoadSaveGame, который будет использоваться при сохранении игры в файл или загрузки сохранённой игры из файла. Он будет иметь метода LoadObject и SaveObject: при этом каждый класс (точнее его автор), реализуя эти методы, сам будет решать, какие именно данные и параметры объекта и его состояния в текущий момент должны быть сохранены в файл сохранения игры и как при загрузке они должны быть интерпретированы, чтобы в точности воссоздать состояние объекта на момент сохранения тогда, когда будет осуществлена загрузка сохранённой игры из файла. Если для какой-нибудь тихо стоящей на уровне декоративной бочки (COilBarrel) нужно будет сохранить только её координаты (ну и может быть Health, если она разрушаемая), то какому-нибудь «противнику» нужно будет сохранить и свои координаты, и скорость, и здоровье и список целей, которые он намерен атаковать, и текущее состояние его анимации, и ещё возможно много чего. При этом и класс CLevelMusic, олицетворяющий фоновую музыку уровня, который до этого не поддерживал ни IPhysicalObject, ни IActor, ни IDestructable, тоже будет имплементировать ILoadSaveGame, потому что мы хотим, чтобы, если мы сохранили игру, то после загрузки музыка продолжила играться ровно с того же места, а не самого начала.
При этом процедура сохранения игры будет выглядеть очень лаконично — просто как цикл обхода всех подряд объектов и попытки их сохранить
- Код: Выделить всё
Public Sub SaveGame(ByVal SavedGameDataConteiner As ...)
Dim Obj As IUnknown
For Each Obj In World
If TypeOf Obj Is ILoadSaveGame ' проверяем, поддерживает ли объект из коллекции всех объектов загрузку/сохранение
SavedGameDataConteiner.WriteObjectClass TypeName(Obj)
CastAs_ILoadSaveGame(Obj).SaveTo SavedGameDataConteiner
End If
Next Obj
End Sub
Public Function CastAs_ILoadSaveGame(ByVal o as IUnknown) As ILoadSaveGame
Set CastAs_ILoadSaveGame = o
End Function[/code]
а загрузка — как обратный цикл.
При этом у нас может быть объект, олицетворяющий дырку от пули в стене. У него есть координаты, так что он будет поддерживать IActor. Но он не будет поддерживать IDestructable или IPhysicalObject, потому что дырку от пули нельзя ни разрушить, ни гравитация и другие силы на неё не действуют. А вот будет ли объект класса «дырка от пули» поддерживать ILoadSaveGame — вопрос. Если мы хотим сохранять информацию о всех дырках от пуль и восстанавливать уровень вплоть до такой точности после загрузки, то будет. А если мы хотим сэкономить на объёме файла с сохранённой игрой, то мы просто не будет классу «дырка от пули» делать поддержку интерфейса ILoadSaveGame — и тогда после сохранения и загрузки уровень загрузится со всеми объектами на своих местах, за исключением дырок от пуль и прочих следов взрывов.
Тогда взаимоотношения между классами нашего проекта и интерфейсами нашего проекта можно представить вот этой картинкой:
- game-classes-and-interfaces.png (89.68 Кб) Просмотров: 1080
В строках — классы, в столбцах — существующие интрерфейсы. Галочка на пересечении означает, что данный класс подддерживает (импементирует) соответствующий интерфейс.
Это пример того, как набор всех классов и набор всех интерфейсов может быть двумя ортогональными плоскостями, в противовес тому, что у тебя в примере был класс-собачка и интерфейс-животное (хотя само по себе существование класса-собачки и интерфейса-животного очень даже вписывается в идею классов и интерфейсов).
Тут ещё на картинке появился интерфейс
IAiControllable, про который я выше не говорил. И дело вот в чём. Допустим, у нас в игре будет 100 разновидностей противников. И каждый противник должен уметь атаковать нашего игрока, бороться с ним, убегать и прятаться, когда это тактически необходимо. В общих чертах все противники делают это одинаково, но каждый тип противника имеет свои особенности: кто-то более трусливый, кто-то атакует «в лобовую», не думая о последствиях. Кто-то хитрый и коварный: предпочитает в основном прятаться и избегать наших атак, но сам наносит удар с точно выверенный момент.
При этом у нас в VB6 нет наследования реализаций между классами. В каком-то другом языке мы бы создали класс CEnemy, внутри которого реализовали бы весь искуственный интеллект и тактику действий противника, а от этого базового класса CEnemy наследовалось бы потом 100 классов для каждого конкретного противника. Но как быть в VB? Неужели прав был человек, статья которого обсуждается в соседнем топике, который сказал, что в VB
полностью отсутствовал полноценный ООП? И неужели нам придётся 100 раз копипастом переносить логику искуственного интеллекта из класса в класс для каждого противника?
Нет! Наследования реализации между классами нет, но это не значит, что задачу нельзя решать красиво. Она просто решается другим подходом. Мы просто введём ещё один интерфейс —
IAiControllable — который должны будут поддерживать все классы, олицетворяющие любой из игровых объектов, который может управляться искуственным интеллектом. Это и дружелюбные к нам существа, и наши враги, и нейтральные существа.
У этого интерфейса будут методы, отвечающие за управление телом силой разума. То есть мы логически отделяем разум от тела, и интерфейс
IAiControolable представляет для нас какое угодно тело, управляемое каким-то абстрактным разумом.
Какие именно методы могут быть у такого интерфейса? Что-то вроде вот таких:
- Присядь
- Ляжь
- Встань
- Подпрыгни
- ПовернисьНалево
- ПовернисьНаПраво
- СтреляйТуда
- БегиСюда
- и т.п.
Какие-нибудь
CFastZombie и
CCowNPC оба будут поддерживать
IAiControllable и по сути и коровий и зомбячьий класс будут предоставлять внешнему миру просто объект-марионетку. Сам по себе экземпляр что
CFastZombie, что
CCowNPC будет работать как пассивное существо, которое может быть по своей воле пошатывается, крутит головой, смотрит то туда сюда, но абсолютно не имеет инициативы. Ни куда не бежит, и ни от кого не бежит. Не атакует и вообще ничего не делает. Чисто марионетка с базовыми функциями.
Откуда же появляется инициативность и деятельность?
А мы делаем ещё один класс, точнее семейство классов: классы для AI-контроллеров. Каждый объект (экзкемляр) такого класса будет олицетворять как бы разум некотого существа. Мозги, искуственный интеллект, волю.
Например, мы можем сделать
CFriendlyCreatureController — для управления действиями дружелюбных к нам существ
- CEnemyContrller — для управления действиями наших врагов.
- CNpcController — для управления действиями нейтральных персонажей, каких-нибудь коров и птичек, присутствующих на уровне для красоты.
А дальше мы должны подружить игровой объект и его AI-контролер.
У вышеприведённых классов будет метод, который позволяет контроллеру задать контролируемый объект. Контроллер при этом не будет, чем является контролируемый объект, потому что с точки зрения контроллера ему подсовывают абстрактное нечто, но это неизвестное нечто поддерживает
IAiControllable, а значит объект-контроллер может отдавать подконтрольному разнообразные приказы: двигайся туда-то, присядь, притворись мёртвым. Ему (контроллеру) глубоко плевать, какими именно путями подконтрольный будет выполнять отданные приказы.
Очевидно, что в реализации
IAiControllable::MoveTo у разных классов может содержаться значительно различающийся код: у птички релаизация этого метода будет предполагать махание крыльями, у зомби — передвижение на четвереньках, у рыбки — движение плавниками.
Внутри объектов классов
CFastZombie,
CCowNpc,
CFinalBoss будет ссылка на объект-контроллер, а внутри объекта-контроллера будет ссылка на подконтрольный объект, поддерживающий интерфейс
IAiControllable. Очевидно, что
CFastZombie и
CFinalBoss будут использовать объект-контроллер одного и того же типа (грубо говоря — «разум врага»), а корова — контроллер совершенно другого типа.
Таким образом мы между двумя классами делим ответственность за осуществление разных задач:
В классах типа «коровка», «зомби», «птичка» мы реализуем конкретику того, как именно двигается персонаж, какими путями он решает простейшие задачи. Код в этих классах отвечает за то,
как это делать, но совершенно не решает
что именно нужно делать. С другой стороны, у нас есть классы-контроллеры (вроде класса для дружелюбного существа, враждебно настроенного существа, нейтрально настроенного существа), код в которых отвечает только за то, что именно нужно делать в каждой конкретной ситуации, при этом в этом коде нет ни намёка на то, каким именно способом будет двигать ручками/лапками/ножками подконтрольное существо.
Кстати, очевидно, что какой-нибудь там
CEnemyController из всех вышеперечисленных интерфейсов не будет поддерживать ни
IActor, ни
IPhysicalObject, ни
IDestructable, но точно будет поддерживать
ILoadSaveGame, ведь мы не хотим, чтобы при последовательном сохранении/загрузки игры все существа забыли, что они хотели делать, куда бежали, кого атаковали.
Таким образом, все живые существа с собственной воли в игре у нас будут представляться спаркой из двух объектов: объект-марионетка (внутри которого реализована конкретика функционирования существа, но не логика его поведения) и объект-контроллер (который управляет объектом-марионеткой, не задумываясь при этом о том, как именно она выполняет высокоуровневый приказы).
При создании нового экземпляра код класса CFastZombie сам будет создавать для себя контроллер и присоединять себя к нему:
- Код: Выделить всё
Private m_MyAiController as IAiController
Private Sub Class_Initialize()
Set m_MyAiControlelr = New CEnemyController
m_MyAiController.StartControl Me
End Sub
Если представить себе, что в нашей игре должно быть что-то вроде того, что какое-то изначально дружелюбное к нам существо вдруг должно перевоплотиться и стать враждебным к нам, например в каких-то добрых персонажей может вселяться демон, полностью меняя их поведение, то это может быть очень элегантно воплощено в коде:
- Код: Выделить всё
Public Sub BecomeDemoniacal(ByVal bMode As Boolean)
Static OldController As IAiController
If bMode Xor OldController Is Nothing then
If bMode Then
Set OldController = m_MyAiController
OldController.StartControl Nothing
Set m_MyAiController = CEnemyController
m_MyAiController.StartControl Me
Else
m_MyAiController.StartControl Nothing
Set m_MyAiController = OldController
OldController.StartControl Me
Set OldController = Nothing
End If
End If
End Sub
Разумеется, объект-существо в игровом мире и вовсе сможет существовать без присоединённого к нему объекта-контроллера. Просто это будет пассивное существо.
Зато, когда нам понадобится внедрить в игру ещё 10 новых типов противников, мы создадим 10 новых классов, в которых будут уникальные особенности этих протвников. Для того, чтобы они вели себя как противники, все 10 классов будут порождать вспомогательный объект-контроллер класса CEnemyController, внутри которого раеализована вся логика враждебности.
Но при таком подходе абсолютно все противники в игре получаются одинаковыми: они могут по разному выглядеть, по разному двигаться, умирать с разными спецэффектами (а некоторые из них могут быть бессмертными), но вести бой они будут совершенно идентичными образом.
Чтобы такого не было, есть множество разных решений.
Во-первых,
IAiControllable может иметь специальный метод — что-то вроде
GetCharacteristics — посредством которого контроллер узнаёт у подконтрольного о «личностных характеристиках» подконтрольного: набор показателей и коэффициентов, которые определяют минимальное и максимально время между попытками поменять тактику, коэффициенты вероятности принятия решения на атаку/отступление и т.п., словом, комплекс числовых параметров, влияющих на то, как именно ведёт себя искусственный интеллект. Агрессивность, напористость, осторожность — все эти коэффициенты будут фигурировать в логике принятия решений на осуществлений конкретных действий.
Имея подход, при котором контроллер спрашивает у подконтрольного о его характеристиках, мы можем сделать так, что, к примеру, один тип протовника (тот же самый
CFastZombie) по мере нанесения ему урона становится менее агрессивным, но более трусливым. Другой тип противника может, наоборот, с приближением своей смерти становится совсем уж отчаянными — терять-то всё равно нечего. Третий тип противников будет иметь постоянную тактику в бою вне зависимости от того, каков показатель здоровья.
А всё это будет достигаться просто тем, что
- Код: Выделить всё
Private Sub IAiControllable_GetCharacteristics(...)
...
End Sub
во всех трёх классов будет иметь разную начинку.
Во-вторых, если какому-то типу противника нужна совсем уж никальная тактика ведения боя, мы можем для него написать эксклюзивный класс-контроллер.
А в третьих, можно подумать о том, что между экземплярами классов-контроллеров можно сделать возможность выстраивать иерархии. То есть в простейшем случае у нас есть объект-марионетка и объект-контроллер, который действуют в паре. Но в более сложном случае у объекта-контроллера могуть быть родительские/дочерние объекты контроллеры.
Такая иерархичность даст нам возможность наслаивать более сложные формы поведения поверх более простых, строя дерево из контроллеров для каждого из подконтрольных объектов, вместо присоединения одного единственного. Стимулы (например сигнал о взорвавшейся рядом гранате) будут итеративно прогоняться по цепочке контроллеров, начиная от самого высокоуровневого контроллера и так вниз до самого низкоуровневого, пока один из них не примет какое-то решение. При этом, на каждом уровне контроллер сможет не только принять решение что делать, но и выбрать между тем, чтобы передать стимул дальше вниз по иерархии или прекратить его обработку на своём уровне.
Это в сущности очень похоже на то, как оконные процедуры оконных классов обрабатывают оконные сообщения: от top-level-оконного класса сообщение передаётся родительской оконной процедуре и так вплоть до
DefWindowProc. На каждом уровне можно и отреагировать как-то на оконное сообщение, и принять решение о том, передавать ли его дальше по иерархии.
Точно так же устроена обработка IRP-объектов в ядре Windows драйверами устройств, промежуточными драйверами-фильтрами.
В общем, наслаивая друг на друга объекты таким образом, мы можем достичь в принципе того же, чего бы добились путём наследования классов в среде/языке, где наследование классов есть.
Отсюда вывод, что для получения красивого ООП и стройной архитектуры не обязательно иметь в языке наследование реализаций у классов. Нужно иметь голову.
И вообще, с играми, зверушками и существами пример достаточно далёк от жизни. Не каждый пишет игры с таким богатым разнообразием игровых объектов и такой стройной объектной архитектурой.
Я предлагаю
пример попроще.
Есть у меня в одном проекте процедура, реализующая ну очень запутанный алгоритм. Там очень много кода. При этом по ходу своей работы алгоритм должен вываливать какие-то данные куда-то.
По началу я использовал
Debug.Print ....
Но здесь есть проблема: если в какой-то момент придётся отказаться от вывода информации и делать всё молча, придётся либо править код, вырезая все упоминания
Debug.Print, либо всё равно править код, обрамляя каждое упоминание
Debug.Print в
If-блок.
Что придётся делать, если нужно будет тот же вывод печатать не в
Immediate Pane, а в файл? Тогда придётся опять же переписывать весь код, меняя
Debug.Print xxxxxxxxx на
Print #filenumber, xxxxxxxx. Придётся также писать код для открытия и закрытия файла средствами VB.
Но что делать, если нужно выводить уже не в файл, а в ListBox, отображающий лог какого-то затяжного процесса? Опять придётся переписывать весь код, меняя
Debug.Print на
ListBoxObj.Additem, причём здесь потребуется уже нетривиальная замена. Это в случае с файлом
Debug.Print и
Print использовали почти идентичную форму синтаксиса.
- Код: Выделить всё
Debug.Print "AAA";
Debug.Print " | ";
Debug.Print Tab(2); 123 ; foo ; Spc(10); bar
Выведёт всё в одну строчку за счёт использования «точки с запятой». Это нельзя вслепую автозаменой заменить на три вызова
ListBox.AddItem, а конструкции
Tab(2) и
Spc(10) вообще не могут появляться нигде в коде, кроме как в Print-стейтменте.
В общем, меняя то, куда мы намерены выводить данные, и намерены ли выводить вообще, мы вынуждены постоянно переписывать код, выводящий эти данные. А что если код очень большой и его переписывание грозит большими проблемами. Не дай бог что-то не то тронуть — и всё сломается.
Как бы нам иметь унифицированный способ вывода данных, не заботясь о том, куда именно пойдут данные и пойдут ли вообще? Чтобы нам вообще не нужно было трогать, если нам понадобится менять пункт назначения данных или в принципе отключать вывод данных?
А это можно сделать с применением интерфейсов. И я это сделал у себя в разных проектах.
Я ввёл интерфейс
ILogTarget, у которого два метода:
AddLine и
AddText. Второй метод отличается от первого просто тем, что добавляет текст к последней добавленной строке, а не начинает новую строку: это сделанно для портирования с формы
Debug.Print xxx;, которая не ставит символ переноса строки в конце вывода.
В свою супер-замудрённую процедуру я передаю ссылку на объект, поддерживающий интерфейс
ILogTarget. Или, если это метод класса, то у класса есть глобальная переменная/свойство со ссылкой на такой объект.
В итоге супер-замудрённая процедура, желающая выводить какой-то отладочный текст или лог, выглядит так:
- Код: Выделить всё
Public Sub VeryComplexRoutine(..........., ByVal Logger As ILogTarget)
' ....
' .... тысячи строк кода
' ....
Logger.AddLine "Мы обработали все важные этапы, а именно: "
For i = 0 To Stages.Count - 1
Logger.AddLine Stages.Item(i).Name
Logger.AddLine IIf(i + 1 = Stages.Count, ".", ", ")
Next I
' ....
' .... тысячи строк кода
' ....
End Sub
Здесь важно то, что супер-сложная функция выводит лог или отладочные данные
ВООБЩЕ НЕ ЗАДУМЫВАЯСЬ о том, куда этот текст пойдёт и какими средствами он будет выводиться. И пойдёт ли куда-то. Мне не придётся в тысячи местах менять один способ вывода на другой, если я вдруг решу поменять то, куда мне надо выводить вывод — я просто в одном месте поменяю то, что я передаю на вход супер-запутанной процедуре.
И соответственно, у меня есть разный набор классов, поддерживающих
ILogTarget, реализующие разные способы вывод данных.
Например класс
CDebugLogger:
- Код: Выделить всё
Implements ILogTarget
Private m_fHasOutput As Boolean
Private Sub ILogTarget_AddLine(ByVal s As String)
If m_fHasOutput Then Debug.Print ""
Debug.Print s;
m_fHasOutput = True
End Sub
Private Sub ILogTarget_AddText(ByVal s As String)
m_fHasOutput = True
Debug.Print s;
End Sub
Он выводит всё в Immediate Pane, как если бы мы просто использовали Debug.Print.
У меня есть отдельный класс
CNullLogTarget, который вообще никуда ничего не выводит, а просто выбрасывает текст, который в него передают.
- Код: Выделить всё
Implements ILogTarget
Private Sub ILogTarget_AddLine(ByVal s As String)
End Sub
Private Sub ILogTarget_AddText(ByVal s As String)
End Sub
Когда у меня была процедура, которая выводила каждое своё мало-мальски значимое действие, и мне это стало не нужно, я просто стал передавтаь ей экземпляр
CNullLogTarget — мне не потребовалось вырезать 1000 мест вывода текста из процедуры, я просто изменил то, как процедура вызывается.
У меня может быть класс
CFileLogger, который всё, что ему скармливают, пишет в файл:
- Код: Выделить всё
Implements ILogTarget
Private m_fHasOutput As Boolean
Private m_iFilenum As Long
Public Sub OpenLogFile(ByVal sFileName As String)
Dim iFnum As Long
iFnum = FreeFile()
Open sFilename For Output As #iFnum
If m_iFilenum Then Close #m_iFilenum
m_iFilenum = iFnum
End Sub
Private Sub Class_Terminate
If m_iFilenum Then
Print #m_iFilenum, ""
Close #m_iFilenum
End If
End Sub
Private Sub ILogTarget_AddLine(ByVal s As String)
If m_fHasOutput Then Debug.Print ""
Print #m_iFilenum, s;
m_fHasOutput = True
End Sub
Private Sub ILogTarget_AddText(ByVal s As String)
m_fHasOutput = True
Print #m_iFilenum, s;
End Sub
И если мне нужно будет, чтобы моя супер-пупер процедура выводила всё в файл, я подкину ей на вход экземпляр этого класса:
- Код: Выделить всё
Dim xxx As New CFileLogger
xxx.OpenLogFile "test.log"
MySuperDuperProc 1, 2, 3, xxx
У меня есть
форма FSimpleLogWnd, которая импементирует
ILogTarget — да-да, а кто сказал, что форма не может поддерживать интерфейс, ведь она является частным случаем класса. На этой форме есть
ListBox, и все выводимые строки добавляются в него и он служит эдаким контейнером-для логов:
- Код: Выделить всё
Option Explicit
Implements ILogTarget
Private Sub ILogTarget_AddLine(ByVal s As String)
Dim nLines As Integer
nLines = lboxLines.ListCount
CleanupLogString s
If nLines = 32767 Then
lboxLines.RemoveItem 0
nLines = nLines - 1
End If
InternalAddLine s, nLines
End Sub
Private Sub ILogTarget_AddText(ByVal s As String)
Dim iLastLine As Integer
iLastLine = lboxLines.ListCount - 1
CleanupLogString s
If iLastLine < 0 Then
InternalAddLine s, 0
Else
lboxLines.List(iLastLine) = lboxLines.List(iLastLine) + s
End If
End Sub
Private Sub ILogTarget_Clear()
lboxLines.Clear
End Sub
Private Sub InternalAddLine(ByVal s As String, ByVal nLinesBeforeAdd As Integer)
lboxLines.AddItem s
lboxLines.ListIndex = nLinesBeforeAdd
Me.Caption = "Lines: " + CStr(nLinesBeforeAdd + 1)
End Sub
Private Sub CleanupLogString(ByRef s As String)
s = Replace(s, vbNullChar, " ")
s = Replace(s, vbTab, " ")
End Sub
' Код неполный, конечно же, масштабирование ListBox-а в ответ на изменение
' размеров окна, функционал контектсного меню и т.п. я вырезал
Причём, если я вызываю свою супер-замудрённую процедуру три раза, я могу все 3 раза выводить в одно и то же окно:
- Код: Выделить всё
' Вспомогательная функция
Public Function MakeLogWindow() As ILogTarget
Dim NewLogWnd As FSimpleLogWnd
Set NewLogWnd = New FSimpleLogWnd
Load NewLogWnd
NewLogWnd.Sow
Set MakeLogWindow = NewLogWnd
End Function
- Код: Выделить всё
Dim CommonLog As ILogTarget: Set CommonLog = MakeLogWindow()
CommonLog.AddLine "Результат первого вызова:"
MySuperDuperProc 1, 2, 3, CommonLog
CommonLog.AddLine "Результат второго вызова:"
MySuperDuperProc 4, 5, 6, CommonLog
CommonLog.AddLine "Результат третьего вызова:"
MySuperDuperProc 7, 8, 9, CommonLog
А могу результат каждого вызова вывести в своё отдельное окошечко:
- Код: Выделить всё
MySuperDuperProc 1, 2, 3, MakeLogWindow()
MySuperDuperProc 4, 5, 6, MakeLogWindow()
MySuperDuperProc 7, 8, 9, MakeLogWindow()
В реальности форму
FSimpleLogWnd я использовал только очень непродолжительное время: ListBox-контрол не может содержать больше 32767 строчек, да и с теми становится тормознутым по мере приближения числа добавленных строк к предельному.
Я сделал форму
FSimpleLogWnd2, на которой нет ни единого контрола (вру — кроме скроллбара), но которая способна легко переварить сотни тысяч или миллионы строк, которые можно в неё добавить, а отображает она и путём отрисовки текста на себе. Она позволяет выделять строки, делать поиск по логу, копировать выделенные строки, скроллиться по всему этому блоку из сотен тысяч строк без малейших задержек. Реализация занимается 1400 строк, так что я тут не буду её листинг приводить.
Почти 9 лет назад я показал
способ без всяких извращений сделать на VB консольное приложение, применив FSO и недокументированный ключ LinkSwitches. Можно сделать класс-обёртку, которая позволяет выводить лог уже не файл или окошечко, а в консоль. Точнее в stdout или stderr, откуда вывод попадёт потом в консоль, файл, на вход другому процессу в stdin или в устройство NUL. Но в простейшем случае — в консоль. Задача враппера просто транслировать обращения к метода
ILogTarget в вызовы соответствующих методов
ITextStream (это уже FSO-шный интерфейс). Это будет совершенно тривиальный враппер
CLogToFsoStream:
- Код: Выделить всё
Implements ILogTarget
Public FsoSteam As ITextStream
Public Sub AddLine(ByVal s As String)
FsoStream.WriteLine s
End Sub
Public Sub AddText(ByVal s As String)
FsoStream.Write s
End Sub
С использованием такого враппера можно передать нашей супер-замудрёной процедуре ссылку на объект-враппер и всё, что она будет выводить, используя интерфейс
ILogTarget объекта-враппера, будет в конечном итоге попадать в консоль (или в файл, ведь FSO позволяет писать и в файлы — таким образом получится альтернатива вышеприведённому классу
CFileLogger)
Можно сделать враппер не над каким-то конкретным механизмом вывода текста, а над другим абстрактным получателем текстового вывода: например сделать лог-враппер, который префиксирует выводимые строки каким-нибудь текстовым префиксом, что может понадобиться, например, для оганизации отступов или выравнивания внутри логов, для цитирования в большом логе как бы вложенного в него маленького лога:
Вот например класс, который, сам поддерживая
ILogTarget, олицетворяет собой обёртку над каким-то другим объектом, поддерживающим
ILogTarget, заранее даже неизвестно каким (и в этом прелесть), добавляя в начале каждой новой строки заданный префикс
CPrefixingLogWrapper:- Код: Выделить всё
Option Explicit
Implements ILogTarget
Private m_ParentLogTarget As ILogTarget
Private m_sPadding As String
Private m_HadOutputs As Boolean
Private Sub ILogTarget_AddLine(ByVal s As String)
m_ParentLogTarget.AddLine m_sPadding & s
m_HadOutputs = True
End Sub
Private Sub ILogTarget_AddText(ByVal s As String)
If m_HadOutputs Then
m_ParentLogTarget.AddText s
Else
m_ParentLogTarget.AddLine m_sPadding & s
m_HadOutputs = True
End If
End Sub
Public Sub SetWrapping(ByVal ParentTarget As ILogTarget, ByVal sPrefix As String)
If ParentTarget Is m_ParentLogTarget Then Exit Sub
If ParentTarget Is Me Then Error 5 ' Не даём становиться обёрткой над самим собой
Set m_ParentLogTarget = ParentTarget
m_HadOutputs = False
m_sPadding = sPrefix
End Sub
При помощи вспомогательной функции мы можем любой заранее-не-известной-какой приёмник лог-информации обёрывать своим объектом, который будет добавлять нужный текст в начале строк:
- Код: Выделить всё
Public Function MkPrefixingWrapperOnLogTarget(ByVal LogTarget As ILogTarget, byval sPrefix As String) As ILogTarget
Dim Wrp As New CPrefixingLogWrapper
Wrp.SetWrapping LogTarget, sPrefix
Set MkPrefixingWrapperOnLogTarget= Wrp
End Function
Что это нам даёт?
Допустим, у нас есть функция, которыя принимает на вход имя файла и ссылку на ILogTarget, и в предоставленный лог-таргет скидывает хекс-дамп файла:
- Код: Выделить всё
Public Sub PrintHexDumpOfFile(file, ByVal Target As ILogTarget)
Дамп при этом выглядит как-то так:
- Код: Выделить всё
+0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
+0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
Теперь у меня есть большая процедура, которая обаратывает какие-то данные, которая тоже имеет переданный ей лог-таргет, в который она должна выводить результаты своей работы, и эта процедура в числе очень многих вещей хочет выводить хекс-дампы файлов в свой вывод.
Но нужно выводить хекс-дампы файлов так, чтобы они как-то псевдографически выделялись на фоне всего остального текста. Например с использованием
> в качестве префикса.
Тогда мы первоначально вот такой код:
- Код: Выделить всё
Sub DoBigJob(ByVal lt As ILogTarget)
lt.AddLine "Я делаю большую работу"
lt.AddLine "очень много всевозможной работы"
lt.AddLine "0010h = 0008h + 0008h, вы знали об этом?"
lt.AddLine "А вот, кстати, вам контент файла <foo.txt>:"
PrintHexDumpOfFile "foo.txt", lt
lt.AddLine "+10 градусов лучше, чем -40"
lt.AddLine "Что бы вам ещё такое рассказать?"
lt.AddLine "Ах да, вот содержимое файла <another.txt>:"
PrintHexDumpOfFile "another.txt", lt
lt.AddLine "Но у меня ещё остаётся много работы..."
lt.AddLine "Ещё очень и очень много работы..."
End Sub
дающий вот такой выхлоп:
- Код: Выделить всё
Я делаю большую работу
очень много всевозможной работы
0010h = 0008h + 0008h, вы знали об этом?
А вот, кстати, вам контент файла <foo.txt>:
+0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
+0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
+10 градусов лучше, чем -40
Что бы вам ещё такое рассказать?
Ах да, вот содержимое файла <another.txt>:
+0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
+0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
Но у меня ещё остаётся много работы...
Ещё очень и очень много работы...
преобразуем вот так:
- Код: Выделить всё
Sub DoBigJob(ByVal lt As ILogTarget)
lt.AddLine "Я делаю большую работу"
lt.AddLine "очень много всевозможной работы"
lt.AddLine "0010h = 0008h + 0008h, вы знали об этом?"
lt.AddLine "А вот, кстати, вам контент файла <foo.txt>:"
PrintHexDumpOfFile "foo.txt", MkPrefixingWrapperOnLogTarget(lt, " > ")
lt.AddLine "+10 градусов лучше, чем -40"
lt.AddLine "Что бы вам ещё такое рассказать?"
lt.AddLine "Ах да, вот содержимое файла <another.txt>:"
PrintHexDumpOfFile "another.txt", MkPrefixingWrapperOnLogTarget(lt, " > ")
lt.AddLine "Но у меня ещё остаётся много работы..."
lt.AddLine "Ещё очень и очень много работы..."
End Sub
Что даст гораздо лучше отформатированный текст:
- Код: Выделить всё
Я делаю большую работу
очень много всевозможной работы
0010h = 0008h + 0008h, вы знали об этом?
А вот, кстати, вам контент файла <foo.txt>:
> +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
> +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
+10 градусов лучше, чем -40
Что бы вам ещё такое рассказать?
Ах да, вот содержимое файла <another.txt>:
> +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
> +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
Но у меня ещё остаётся много работы...
Ещё очень и очень много работы...
Но самое главное здесь не просто очень незначительное улучшение форматирование, а сама идея о том, что мы вызываем какюу-то процедуру, скармивая ей обёртку над log-target'ом, и при этом с одной стороны нас совершенно не волнует, что именно будет выводит вызываемая функция, мы гарантированно получим правильное формативание, а с другой стороны вызываемую функцию совершенно не волнует, годится ли её вывод в первозданном виде или вызывающей стороне или нет: она выводит как если бы она была одна во вселенной, и её не беспокоит, куда это пойдёт, а вызывающую сторону не беспокоит, что именно выводят, но она получает форматирование путём вставка отступа и углвой скобки. При этом за конечным log-target'ом можно стоять вообще всё, что угодно: и консоль, и листбокс, и какой-то юзерконтрол для логгирования, и отдельное лог-окошечко, и файл, да хоть TTS-движок, который всё это будет голосом озвучивать.
Возможность каскадирования обёрток друг над другом позволяет использовать обёртывание в рекурсиях. Например, пусть у нас есть не шибко умно написанная фунция, которая перебирает содержимое каталога и возвращает содержащиеся в нём файлы и подкаталоги в виде массива:
- Код: Выделить всё
Public Function GetDirSubelements(ByVal sPath As String) As String()
Dim sResult As String
Dim sCombined As String
sResult = Dir(sPath, vbDirectory)
Do Until sResult = ""
If (sResult <> ".") And (sResult <> "..") Then
sCombined = sCombined & "|" & sResult
End If
sResult = Dir()
Loop
GetDirSubelements = Split(Mid$(sCombined, 2), "|")
End Function
Дальше мы делаем другую функцию, которая с использованием первой выводит список содержимого каталога в лог-таргет:
- Код: Выделить всё
Public Sub ListDirectory(ByVal sPath As String, ByVal lt As ILogTarget)
Dim i As Long, Subelements() As String
Subelements = GetDirSubelements(sPath)
For i = LBound(Subelements) To UBound(Subelements)
lt.AddLine Subelements(i)
Next
End Sub
Вызываем её как
ListDirectory "c:\test\", New CDebugLogger, и она вываливает нам вот это:
- Код: Выделить всё
Новая папка
Новая папка 2
foo.txt
bar.txt
Примеры
Теперь, сделав очень маленькое изменение, можно сделать эту функцию рекурсивной: пусть, встречая директорию, она для обработки этой директории вызывает саму себя — возникает рекурсия, а функция таким образом выводит не только непосредственное содержимое каталога, но и всех его подкаталогов:
- Код: Выделить всё
Public Sub ListDirectory(ByVal sPath As String, ByVal lt As ILogTarget)
Dim i As Long, Subelements() As String
Subelements = GetDirSubelements(sPath)
For i = LBound(Subelements) To UBound(Subelements)
lt.AddLine "+--" & Subelements(i)
If GetAttr(sPath & Subelements(i)) And vbDirectory Then
ListDirectory sPath & Subelements(i) & "\", lt
End If
Next
End Sub
Что мы увидим на выходе?
Мешанину! Содержимое подпапок смешается содержимым корневой папки и будет непонятно, где что.
- Код: Выделить всё
Новая папка
Текстовый документ.txt
Новая папка 2
pic01.jpg
pic02.jpg
foo.txt
bar.txt
Примеры
Хорошие примеры
Новая папка
Новая папка
Новая папка
Пример 1.txt
Пример 2.txt
Плохие примеры
Пример 1.txt.
Пример 2.txt
Пример 2 — разбор
Объяснение 1.txt
Объяснение 2.txt
Пример 3.txt
Просто пример 1.txt
Просто пример 2.txt
Как можно выкрутиться из этого положения? Можно
lt.AddLine Subelements(i) заменить на
lt.AddLine sPath & Subelements(i). Тогда будут выводиться не просто имена файлов и подкаталогов, а пути относительно первоначальной папки. А что если мы хотим выводить не полные пути (или частично полные), а только имена файлов и каталогов, но так, чтобы прослеживалась иерархия?
Мы это легко можем сделать с помощью объекта-враппера:
- Код: Выделить всё
Public Sub ListDirectory(ByVal sPath As String, ByVal lt As ILogTarget)
Dim i As Long, Subelements() As String
Subelements = GetDirSubelements(sPath)
For i = LBound(Subelements) To UBound(Subelements)
lt.AddLine Subelements(i)
If GetAttr(sPath & Subelements(i)) And vbDirectory Then
ListDirectory sPath & Subelements(i) & "\", _
MkPrefixingWrapperOnLogTarget(lt, " ")
End If
Next
End Sub
И вот в мешанине начинает прослеживаться структура каталога:
- Код: Выделить всё
Новая папка
Текстовый документ.txt
Новая папка 2
pic01.jpg
pic02.jpg
foo.txt
bar.txt
Примеры
Хорошие примеры
Новая папка
Новая папка
Новая папка
Пример 1.txt
Пример 2.txt
Плохие примеры
Пример 1.txt.
Пример 2.txt
Пример 2 — разбор
Объяснение 1.txt
Объяснение 2.txt
Пример 3.txt
Просто пример 1.txt
Просто пример 2.txt
Ещё небольшое усложнение кода, и выводе иерархия полноценно изображена псевдографикой:
- Код: Выделить всё
Public Sub ListDirectory(ByVal sPath As String, ByVal lt As ILogTarget)
Dim i As Long, Subelements() As String
Subelements = GetDirSubelements(sPath)
For i = LBound(Subelements) To UBound(Subelements)
Dim LevChar$: LevChar$ = IIf(i < UBound(Subelements), "|", " ")
lt.AddLine "+--" & Subelements(i)
If GetAttr(sPath & Subelements(i)) And vbDirectory Then
ListDirectory sPath & Subelements(i) & "\", _
MkPrefixingWrapperOnLogTarget(lt, LevChar$ & " ")
End If
Next
End Sub
- Код: Выделить всё
+--Новая папка
| +--Текстовый документ.txt
+--Новая папка 2
| +--pic01.jpg
| +--pic02.jpg
+--foo.txt
+--bar.txt
+--Примеры
+--Хорошие примеры
| +--Новая папка
| +--Новая папка
| +--Новая папка
| +--Пример 1.txt
| +--Пример 2.txt
+--Плохие примеры
| +--Пример 1.txt.
| +--Пример 2.txt
| +--Пример 2 — разбор
| | +--Объяснение 1.txt
| | +--Объяснение 2.txt
| +--Пример 3.txt
+--Просто пример 1.txt
+--Просто пример 2.txt
Можно было бы сказать, что использование объекта-враппера здесь совсем не обязательно: можно было бы просто в рекурсивной функции добавить ещё один аргумент: прификс-строку, и при выводе текущего элемента предварительно выводить её.
Да, можно. Но во-первых, тогда бы рекурсивную функцию пришлосб бы дробить на два: для внешнего пользователя — без лишнего аргумента, и для внутреннего пользования — с ним.
Во-вторых, что если мы нам внезапно приспичело не просто выводить файл, содердащийся в подпапке, но и выводить его содержимое или хекс-дамп уже знакомой нам функцией
PrintHexDumpOfFile? Она-то ничего не знает, что то, что она собирается выводить, должно быть аккуратно вписано в псевдографическую иерархию дерева каталогов?
Пришлось бы переделывать
PrintHexDumpOfFile, добавляя и ей дополнительный аргумент с префиксирующей строкой. Но если она уже используется в сотнях мест программы, мы не смогли бы, скорее всего, просто так добавить ей новый аргумент, ничего не поломав.
Зато с использованием враппер-объектом над лог-таргетом подружить эту готовую функцию с нашим кодом очень легко:
- Код: Выделить всё
Public Sub ListDirectory(ByVal sPath As String, ByVal lt As ILogTarget)
Dim i As Long, Subelements() As String
Subelements = GetDirSubelements(sPath)
For i = LBound(Subelements) To UBound(Subelements)
Dim LevChar$: LevChar$ = IIf(i < UBound(Subelements), "|", " ")
lt.AddLine "+--" & Subelements(i)
If GetAttr(sPath & Subelements(i)) And vbDirectory Then
ListDirectory sPath & Subelements(i) & "\", _
MkPrefixingWrapperOnLogTarget(lt, LevChar$ & " ")
Else
PrintHexDumpOfFile sPath & Subelements(i), _
MkPrefixingWrapperOnLogTarget(lt, LevChar$ & " > ")
End If
Next
End Sub
В итоге мы получим примерно вот такой вывод:
- Код: Выделить всё
+--Новая папка
| +--Текстовый документ.txt
| > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
+--Новая папка 2
| +--pic01.jpg
| | > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| | > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
| +--pic02.jpg
| > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
+--foo.txt
| > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
+--bar.txt
| > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
+--Примеры
+--Хорошие примеры
| +--Новая папка
| +--Новая папка
| +--Новая папка
| +--Пример 1.txt
| | > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| | > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
| +--Пример 2.txt
| > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
+--Плохие примеры
| +--Пример 1.txt
| | > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| | > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
| +--Пример 2.txt
| | > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| | > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
| +--Пример 2 — разбор
| | +--Объяснение 1.txt
| | | > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| | | > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
| | +--Объяснение 2.txt
| | > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| | > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
| +--Пример 3.txt
| > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
+--Просто пример 1.txt
| > +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
| > +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
+--Просто пример 2.txt
> +0000h | 41 42 43 44 45 46 47 48 49 4A 4B 4C 4D 4E 4F 50 | ABCDEFGHIJKLMNOP
> +0010h | 51 52 53 54 55 56 57 58 59 5A 5B 5C 5D 5E 5F 60 | QRSTUVWXYZ[\]^_`
Что общего между двумя этими примерами: с игрой и с логгером?
То, что интерфейсы позволяют разным компонентам программы (или разным программ) взаимодействовать между собой, не зная ничего друг о друге, точнее зная только тот необходимый объём, который требуется для взаимодействия. В первом случае у нас AI-контроллер управлял неизвестно-какими объектами-марионетками: ему было плевать, какие конкретно объекты (классы) стоят за марионетками — покуда они поддерживают
IAiControllable, он знал и мог ими управлять. А конкретно взятым классам объектов-марионеток было плевать, какой именно контроллер (злой, дружелюбный, нейтральный или контроллер сумасшедшего существа) ими управляет. Механизму сохранения и загрузки было не важно, какого типа объект он сейчас сохраняет в файл сохранения игры, коль скоро все они имели унифицированный способ попросить их скинуть в файл своё внутреннее состояние и прочие внутренние данные, достаточные для последующего воссоздания при загрузке игры. Во втором случае у нас функция, выводящая дамп файла совершенно не переживала, в каком контексте её вывод будет использоваться, куда именно и какими методами он будет выведен. А вызывающая её функция рекурсивного обхода дерева каталогов плевать хотела на то, что там внутри себя выводит функция дампинга файлов. И вообще, любая функция, использующая ILogTarget для вывода, может плевать на то, какая конкретно реализация стоит за ILogTarget и что она делает с текстом. Не нужно по всей программе что-то менять, если мы хотим принципиально поменять то, куда мы делаем логгинг.
Когда один кусок программы не привязан к специфике другого, можно в любой момент тот «другой» подменить на третий или четвёртый, не меняя ничего в первом и не боясь негативных последствий со стороны первого и программы целиком.
Один хорошо написанный и хорошо отлаженный компонент (в широком толковании этого слова) можно многократно использовать, заставляя его взаимодействовать с совершенно разными другими компонентами. Например, единожды написанный хороший механизм сортировки можно использовать для сортировки чего угодно, если его взаимодействие с сортируемыми объектами построено на том, что сортируемые объекты должны поддерживать
IComparable. Можно стыковать, совместно использовать или натравливать друг на друга объекты, механизмы, алгоритмы, если они предполагают использования заранее оговорённых и одних и тех же интерфейсов.
Почти так же, как в любой PCI-Express-слот можно вставить любую видеокарту, а одну и ту же видеокарту можно установить в множество совершенно разных материнских плат — потому что интерфейс из взаимодействия оговорен одной из стороны (либо производителем видеокарт, либо производителем материнских плат), либо вообще какой-то третьей стороной.
Если нам в программе нужно хранилище объектов или данных, сопоставляющее ключ какому-то значению (хранилище пар ключ→значение), имеет смысл придумать (или взять уже кем-то придуманный готовый и подходящий нам) интерфейс, олицетворяющий абстрактное хранилище подобных пар. Затем можно побыстрому накидать класс, имплементирующий этот интерфейс, который под капотом будет хранить все эти пары тупо в массиве. Это самый неэффективный, тупой, но максимально простой в плане реализации метод. Затем можно написать сотни строк, тысячи строк, десятки тысяч строк кода, использующий подобное это хранилище через интерфейс.
Затем, когда станет очевидно, что неоптимальная реализация хранилища не годится, можно написать другой класс: такого же хранилища, имплементируя тот же интерфейс, но на этот раз используя для поиска не перебор элементом массива от начала до конца, а бинарный поиск. Или использующий для хранения не массив, а AVL-дерево, или красно-чёрное дерево, или индексное дерево.
Для смены типа используемого хранилища достаточно будет сделать изменение в одном месте — том, где хранилище создаётся, изменив, как правило,
New CStupidStorage на
New CWellEngineeredStorage, и не нужно будет делать изменения в тысячи мест, где происходит использование этого хранилища в виде осуществления операций добавления, удаления, поиска, сортировки.