Устранение связи между классами, которые имеют сильные концептуальные связи друг с другом - PullRequest
6 голосов
/ 21 декабря 2008

У меня есть типы Rock, Paper и Scissors. Это компоненты или «руки» игры «Камень, ножницы, бумага». Учитывая руки двух игроков, игра должна решить, кто победит. Как мне решить проблему хранения этой цепочки графиков

Rock, Paper, Scissors chart

не соединяя различные руки друг с другом? Цель состоит в том, чтобы позволить добавить в игру новую руку (например, Джона Скита), не меняя другие.

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

Это своего рода проблема Design 101, но мне любопытно, какие решения люди могут предложить для этого. Очевидно, что эта проблема может легко масштабироваться до гораздо больших систем с большим количеством компонентов с произвольно сложными отношениями между ними. Вот почему я показываю очень простой и конкретный пример для решения. Любая используемая парадигма, ООП или иная, приветствуется.

Ответы [ 6 ]

4 голосов
/ 21 декабря 2008

Есть класс GameStrategy, который реализует метод Win. Метод win берет список рук и возвращает либо руку - если есть победитель - либо ноль, если игра была ничейной. Я думаю, что выигрышная стратегия на самом деле является не рукой, а игрой. Включите определение победителя пары рук в класс GameStrategy.

РЕДАКТИРОВАТЬ : потенциальная стратегия

public enum RPSEnum { Rock, Paper, Scissors }

private RPSEnum FirtRPS = RPSEnum.Rock;
private RPSEnum LastRPS = RPSEnum.Scissors;

public Hand Win( Hand firstPlayer, Hand secondPlayer )
{
    if (firstPlayer.Value == FirstRPS
        && secondPlayer.Value == LastRPS)
    {
       return firstPlayer;
    }
    else if (secondPlayer.Value == FirstRPS
             && firstPlayer.Value == LastRPS)
       return secondPlayer;
    }
    else
    {
       int compare = (int)firstPlayer.Value - (int)secondPlayer.Value;
       if (compare > 0)
       {
          return firstPlayer;
       }
       else if (compare < 0)
       {
          return secondPlayer;
       }
       else
       {
          return null;
       }       
    }
}

Чтобы добавить новое значение руки, просто добавьте значение в RPSEnum в правильной последовательности. Если это новая «самая низкая» рука, обновите FirstRPS. Если это новая «самая высокая» рука, обновите LastRPS. Вам вообще не нужно менять фактический алгоритм.

ПРИМЕЧАНИЕ: это более сложно, чем нужно для всего трех значений, но требовалось, чтобы дополнительные значения можно было добавлять без обновления большого количества кода.

2 голосов
/ 21 декабря 2008

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

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

1 голос
/ 21 декабря 2008

Я не думаю, что разные руки - это разные типы: это отдельные экземпляры одного типа. Этот тип имеет такие атрибуты, как имя, возможно изображение и т. Д.

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

0 голосов
/ 21 декабря 2008

Несколько других ответов предоставили очень гибкие и очень несвязные решения ... и они способ сложнее, чем они должны быть. Короткий ответ на развязку - двойная отправка , которую очень немногие языки поддерживают. Шаблон посетителя GoF существует для имитации двойной диспетчеризации на языках, которые поддерживают одиночную диспетчеризацию (это поддерживается любым языком OO, даже C с указателями на функции).

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

rock/paper
paper/scissors
paper/paper
...

... вы смотрите на эту серию:

GET/200
GET/304
POST/401
POST/200
...

Если в системе есть объекты HttpRequest и HttpResponse, простейшим способом отправки будет использование одной функции, которая перенаправляет все возможные варианты:

HttpRequest req;
HttpResponse resp;
switch ((req.method, resp.code)) {
    case (GET, 200): return handleGET_OK(req, resp);
    case (GET, 304); return handleGET_NotModified(req, resp);
    case (POST, 404): return handlePOST_NotFound(req, resp);
    ...
    default: print "Unhandled combination"; break;
}

При этом на ваши базовые объекты не влияет добавление новых типов. Тем не менее, ваш метод классификации по-прежнему очень прост и, следовательно, поддерживается другими разработчиками. Если вы хотите (или если ваш язык упрощает), вы можете превратить этот оператор switch в своего рода карту функций и зарегистрировать обработчики различных комбинаций методов запроса и кодов ответов при запуске.

0 голосов
/ 21 декабря 2008

Почему развязка? Все эти элементы неразрывно связаны друг с другом, и добавление новой руки не изменит этого.

Просто имейте базовый класс рук, который расширен камнем, ножницами и бумагой. Присвойте базовому классу атрибут .beatenby, который принимает один из других классов типа Hand. Если вы столкнулись с ситуацией, когда руку можно побить несколькими другими руками, просто заставьте атрибут .beatenby принять массив.

0 голосов
/ 21 декабря 2008

В этом случае важно то, что если вы сравните два объекта , вы получите результат .

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

Теперь я также прокомментировал ваш вопрос, сказав, что «Иногда вопросы могут быть слишком обобщенными», и проблема здесь в том, что независимо от того, сколько я вам скажу, как сделать то, что вы спросите о , это не поможет вам ни о какой реальной проблеме.

Если вы не строите игру "Камень-ножницы-бумага-Х".

Лично я бы сделал следующее со своим собственным контейнером IoC.

ServiceContainer.Global.RegisterFactory<IHandType>()
    .FromDelegate(() => RandomRockScissorPaper());
ServiceContainer.Global.RegisterFactory<IHandComparison, DefaultHandComparison>();

(точнее, я бы настроил вышеописанное в файле app.config или аналогичном, чтобы его можно было изменить после сборки проекта).

Затем, если пользователю / клиенту / конечной точке необходимо переопределить, чтобы добавить другой тип, я бы увеличил количество регистраций следующим образом (помните, что приведенное выше находится в app.config, поэтому я заменил бы те, что указаны ниже). ):

ServiceContainer.Global.RegisterFactory<IHandType>()
    .FromDelegate(() => RandomRockScissorPaper())
    .ForPolicy("original");
ServiceContainer.Global.RegisterFactory<IHandType>()
    .FromDelegate((IHandType original) => RandomRockScissorPaperBlubb(original))
    .WithParameters(
        new Parameter<IHandType>("original").WithPolicy("original"))
    .ForPolicy("new")
    .AsDefaultPolicy();

Здесь я добавляю новый способ разрешения IHandType, не только сохраняя оригинальный путь, но и добавляя новый. Этот новый получит результат вызова старого, а затем должен будет внутренне решить, должен ли случайный случай вернуть четвертый тип или исходный тип (один из трех исходных).

Я бы тогда также переопределил исходное правило сравнения:

ServiceContainer.Global.RegisterFactory<IHandComparison, DefaultHandComparison>()
    .ForPolicy("original");
ServiceContainer.Global.RegisterFactory<IHandComparison, NewHandComparison>()
    .ForPolicy("new")
    .AsDefaultPolicy()
    .WithParameters(
        new Parameter<IHandType>("original"));

Вот как это будет использоваться:

IHandType hand1 = ServiceContainer.Global.Resolve<IHandType>();
IHandType hand2 = ServiceContainer.Global.Resolve<IHandType>();
IHandComparison comparison = ServiceContainer.Global.Resolve<IHandComparison>();
if (comparison.Compare(hand1, hand2) < 0)
    Console.Out.WriteLine("hand 1 wins");
else if (comparison.Compare(hand1, hand2) > 0)
    Console.Out.WriteLine("hand 1 wins");
else
    Console.Out.WriteLine("equal");

Вот как это сделать:

public interface IHandComparison
{
   Int32 Compare(IHandType hand1, IHandType hand2);
}
public class DefaultHandComparison : IHandComparison
{
    public Int32 Compare(IHandType hand1, IHandType hand2)
    {
        ... normal rules here
    }
}
public class NewHandComparison : IHandComparison
{
    private IHandComparison _Original;
    public NewHandComparison(IHandComparison original)
    {
        _Original = original;
    }
    public Int32 Compare(IHandType hand1, IHandType hand2)
    {
        if hand1 is blubb or hand2 is blubb then ...
        else
            return _Original.Compare(hand1, hand2);
    }
}

После написания всего этого я понимаю, что моя конфигурация app.config не сможет обрабатывать делегатов, поэтому потребуется фабричный объект, но то же самое применимо.

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

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