Проблема проектирования, связанная с разделением типов по множеству подклассов - PullRequest
3 голосов
/ 16 сентября 2008

Основная проблема, с которой я сталкиваюсь довольно часто, но когда-либо находил ясное решение, это та, в которой вы хотите кодировать поведение для взаимодействия между различными объектами общего базового класса или интерфейса. Чтобы сделать это немного конкретнее, я приведу пример;

Боб работал над стратегической игрой, которая поддерживает «классные географические эффекты». Они округляются до простых ограничений, таких как, если войска ходят в воде, они замедляются на 25%. Если они идут по траве, они замедляются на 5%, а если они идут по асфальту, они замедляются на 0%.

Теперь руководство заявило Бобу, что им нужны новые виды войск. Там будут джипы, лодки, а также суда на воздушной подушке. Кроме того, они хотели, чтобы джипы получали урон, если их водили в воду, а суда на воздушной подушке игнорировали все три типа местности. Ходят слухи, что они могут добавить еще один тип местности с еще большим количеством функций, чем замедление юнитов и получение урона.

Ниже приведен пример очень грубого псевдокода:

public interface ITerrain
{
    void AffectUnit(IUnit unit);
}

public class Water : ITerrain
{
    public void AffectUnit(IUnit unit)
    {
        if (unit is HoverCraft)
        {
            // Don't affect it anyhow
        }
        if (unit is FootSoldier)
        {
            unit.SpeedMultiplier = 0.75f;
        }
        if (unit is Jeep)
        {
            unit.SpeedMultiplier = 0.70f;
            unit.Health -= 5.0f;
        }
        if (unit is Boat)
        {
            // Don't affect it anyhow
        }
        /*
         * List grows larger each day...
         */
    }
}
public class Grass : ITerrain
{
    public void AffectUnit(IUnit unit)
    {
        if (unit is HoverCraft)
        {
            // Don't affect it anyhow
        }
        if (unit is FootSoldier)
        {
            unit.SpeedMultiplier = 0.95f;
        }
        if (unit is Jeep)
        {
            unit.SpeedMultiplier = 0.85f;
        }
        if (unit is Boat)
        {
            unit.SpeedMultiplier = 0.0f;
            unit.Health = 0.0f;
            Boat boat = unit as Boat;
            boat.DamagePropeller();
            // Perhaps throw in an explosion aswell?
        }
        /*
         * List grows larger each day...
         */
    }
}

Как вы можете видеть, все было бы лучше, если бы у Боба был твердый проектный документ с самого начала. По мере роста количества юнитов и типов местности растет и сложность кода. Бобу не только нужно выяснить, какие элементы могут быть добавлены в интерфейс модуля, но он также должен повторить много кода. Весьма вероятно, что новые типы местности требуют дополнительной информации из того, что можно получить из базового интерфейса IUnit.

Каждый раз, когда мы добавляем в игру еще одну единицу, каждая местность должна обновляться, чтобы обрабатывать новую единицу. Понятно, что это требует много повторений, не говоря уже об уродливой проверке во время выполнения, которая определяет тип рассматриваемого юнита. Я отключил вызовы для определенных подтипов в этом примере, но такие вызовы не нужны. Примером может быть то, что когда лодка падает на сушу, ее винт должен быть поврежден. Не все агрегаты имеют пропеллеры.

Я не уверен, как называется такая проблема, но это зависимость многих от многих, которую мне трудно отделить. Мне не нравятся сотни перегрузок для каждого подкласса IUnit на ITerrain, так как я хотел бы получить чистую связь.

Любой свет по этой проблеме очень популярен. Возможно, я думаю о выходе из орбиты все вместе?

Ответы [ 5 ]

1 голос
/ 16 сентября 2008

Здесь определенно три объекта:

1) Рельеф
2) Terrain Effects
3) Единицы

Я бы не советовал создавать карту с парой местности / единицы в качестве ключа для поиска действия. Это затруднит вам проверку всех комбинаций по мере роста списков юнитов и ландшафтов.

На самом деле, похоже, что каждая комбинация ландшафта с юнитами обладает уникальным эффектом ландшафта, поэтому сомнительно, что вы получите выгоду от общего списка эффектов ландшафта вообще.

Вместо этого, я хотел бы, чтобы каждый отряд поддерживал свою собственную карту рельефа местности. Затем местность может просто вызвать Unit-> AffectUnit (myTerrainType), и юнит может посмотреть, какой эффект будет оказывать местность на себя.

1 голос
/ 16 сентября 2008

отделить правила взаимодействия от классов Unit и Terrain; правила взаимодействия являются более общими, чем это. Например, можно использовать хеш-таблицу, где ключом является пара взаимодействующих типов, а значением является метод «эффектор», работающий с объектами этих типов.

когда два объекта должны взаимодействовать, найдите ВСЕ правила взаимодействия в хэш-таблице и выполните их

это устраняет межклассовые зависимости, не говоря уже о операторах отвратительного переключателя в исходном примере

если производительность становится проблемой, а правила взаимодействия не изменяются во время выполнения, кэшируйте наборы правил для пар типов по мере их появления и запускайте новый метод MSIL для их одновременного запуска

1 голос
/ 16 сентября 2008

Terrain имеет атрибут Terrain

Атрибуты местности являются многомерными.

Единицы тока-1 г.

Привод совместим с Атрибутами Ландшафта.

Подразделения передвигаются при посещении местности с аргументом в пользу движения. Это получает делегирование в г.

Подразделения могут быть затронуты местностью как часть посещения.

Код устройства ничего не знает о силовой установке. Типы ландшафта могут меняться без изменения чего-либо, кроме атрибутов ландшафта и движения. Конструкторы Propuslion защищают существующие подразделения от новых методов передвижения.

1 голос
/ 16 сентября 2008

Здесь вы сталкиваетесь с ограничением на то, что в C #, в отличие от некоторых других языков ООП, отсутствует многократная диспетчеризация .

Другими словами, учитывая эти базовые классы:

public class Base
{
    public virtual void Go() { Console.WriteLine("in Base"); }
}

public class Derived : Base
{
    public virtual void Go() { Console.WriteLine("in Derived"); }
}

Эта функция:

public void Test()
{
    Base obj = new Derived();
    obj.Go();
}

будет правильно выводить "in Derived", даже если ссылка "obj" имеет тип Base. Это связано с тем, что во время выполнения C # правильно найдет наиболее производный метод Go () для вызова.

Однако, поскольку C # является единым языком диспетчеризации, он делает это только для «первого параметра», который неявно «this» в языке ООП. Следующий код не работает, как указано выше:

public class TestClass
{
    public void Go(Base b)
    {
        Console.WriteLine("Base arg");
    }

    public void Go(Derived d)
    {
        Console.WriteLine("Derived arg");
    }

    public void Test()
    {
        Base obj = new Derived();
        Go(obj);
    }
}

Это выведет «Base arg», потому что кроме «this» все остальные параметры статически отправляются, что означает, что они связаны с вызываемым методом во время компиляции. Во время компиляции единственное, что знает компилятор, это объявленный тип передаваемого аргумента («Base obj»), а не его фактический тип, поэтому вызов метода привязан к Go (Base b).

Тогда решение вашей проблемы заключается в том, чтобы вручную написать небольшому диспетчеру методов:

public class Dispatcher
{
    public void Dispatch(IUnit unit, ITerrain terrain)
    {
        Type unitType = unit.GetType();
        Type terrainType = terrain.GetType();

        // go through the list and find the action that corresponds to the
        // most-derived IUnit and ITerrain types that are in the ancestor
        // chain for unitType and terrainType.
        Action<IUnit, ITerrain> action = /* left as exercise for reader ;) */

        action(unit, terrain);
    }

    // add functions to this
    public List<Action<IUnit, ITerrain>> Actions = new List<Action<IUnit, ITerrain>>();
}

Вы можете использовать рефлексию для проверки общих параметров каждого переданного действия, а затем выбрать наиболее производный, соответствующий заданной единице и рельефу, а затем вызвать эту функцию. Функции, добавленные в Действия, могут быть где угодно, даже распределены по нескольким сборкам.

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

0 голосов
/ 16 сентября 2008

Старая идея:

Сделай класс iTerrain и другой класс iUnit, который принимает аргумент который является типом местности, включая метод воздействия на каждый тип единицы

пример:

 boat = new
iUnit("watercraft") field = new
iTerrain("grass")
field.effects(boat)

ок, забудь все, что у меня есть идея получше:

Сделать эффекты каждой местности свойством каждого юнита

Пример:


public class hovercraft : unit {
    #You make a base class for defaults and redefine as necessary
    speed_multiplier.water = 1
}

public class boat : unit {
    speed_multiplier.land = 0
}
...