Схема стратегии и «действие» классов взрыва - PullRequest
6 голосов
/ 12 апреля 2010

Разве это плохая политика - иметь много «рабочих» классов (таких как классы Стратегии), которые делают только одно?

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

  1. Запечатай класс, если захочу. Позже другие пользователи могут просто создать новый класс и все еще иметь полиморфизм с помощью интерфейсов, которые я определил. Мне не нужно беспокоиться о том, как люди (или я) могут захотеть изменить / добавить функции в базовый класс в будущем. Все классы наследуются от Object и реализуют наследование через интерфейсы, а не от родительских классов.
  2. Повторно используйте стратегии, которые я использую с этим монстром, для других участников моего игрового мира.

Con: Эта модель жесткая. Иногда нам хотелось бы определить что-то, чего нелегко достичь, просто пытаясь собрать воедино эти «строительные блоки».

public class AlienMonster : IWalk, IRun, ISwim, IGrowl {
    IWalkStrategy _walkStrategy;
    IRunStrategy _runStrategy;
    ISwimStrategy _swimStrategy;
    IGrowlStrategy _growlStrategy;

    public Monster() {
        _walkStrategy = new FourFootWalkStrategy();
       ...etc
    }

    public void Walk() { _walkStrategy.Walk(); }
    ...etc
}

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

Каково ваше мнение по этому вопросу? Это правильное рассуждение? Это сверхинжиниринг?

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

interface IWalk {
    void Walk();
}

или

interface IWalk {
    IWalkStrategy WalkStrategy { get; set; } //or something that ressembles this
}

поскольку для этого мне не нужно было бы определять методы самого Monster, у меня просто были бы общедоступные методы получения для IWalkStrategy (похоже, это противоречит идее, что вы должны инкапсулировать все как можно больше!) Почему?

Спасибо

Ответы [ 4 ]

2 голосов
/ 12 апреля 2010

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

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

2 голосов
/ 12 апреля 2010

Walk, Run и Swim кажутся реализациями, а не интерфейсами. Вы можете иметь интерфейс ILocomotion и позволить своему классу принять список реализаций ILocomotion.

Growl может быть реализацией чего-то вроде интерфейса IAbility. И конкретный монстр может иметь коллекцию реализаций IAbility.

Затем создайте пару интерфейсов, которые определяют, какую способность или локомоцию использовать: IMove, IAct, например.

public class AlienMonster : IMove, IAct
{
  private List<IAbility> _abilities;
  private List<ILocomotion> _locomotions;

  public AlienMonster()
  {
    _abilities = new List<IAbility>() {new Growl()};
    _locomotion = new List<ILocomotion>() {new Walk(), new Run(), new Swim()}
  }

  public void Move()
  {
    // implementation for the IMove interface
  }

  public void Act()
  {
    // implementation for the IAct interface
  }

}

Составляя свой класс таким образом, вы избежите некоторой жесткости.

РЕДАКТИРОВАТЬ: добавил материал об IMove и IAct

РЕДАКТИРОВАТЬ: после некоторых комментариев

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

void SomeFunction(IWalk alienMonster) {...}

Приведенная выше функция будет принимать все, что реализует IWalk, но если есть варианты SomeFunction для IRun, ISwim и т. Д., Вам нужно написать совершенно новую функцию для каждого из них или передать объект AlienMonster целиком. Если вы передаете объект, то эта функция может вызывать любой и все интерфейсы на нем. Это также означает, что эта функция должна запросить объект AlienMonster, чтобы увидеть его возможности, а затем решить, какие из них использовать. Все это приводит к тому, что внешние функции становятся более функциональными, и их следует хранить внутри класса. Поскольку вы все это экстернализуете, и между IWalk, IRun, ISwim нет общего, поэтому некоторые функции могут невинно вызывать все три интерфейса, и ваш монстр может одновременно бегать-ходить-плыть. Кроме того, поскольку вы захотите иметь возможность вызывать IWalk, IRun, ISwim для некоторых классов, все классы должны будут в основном реализовывать все три интерфейса, и в итоге вы создадите стратегию, подобную CannotSwim, для удовлетворения требований интерфейса для ISwim, когда монстр не умею плавать. В противном случае вы можете попытаться вызвать интерфейс, который не реализован на монстре. В конце вы фактически ухудшаете код для дополнительных интерфейсов, IMO.

2 голосов
/ 12 апреля 2010

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

В C #, являющемся языком с единым наследованием, я не вижу другого выхода, кроме создания интерфейсов для них. Если между классами используется много общего кода, тогда ваш IWalkStrategy подход будет хорошо работать для уменьшения избыточного кода. В случае вашего примера вы также можете объединить несколько связанных действий (таких как ходьба, плавание и бег) в один класс.

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

Итак, вкратце: я бы сделал это так же, как вы предложили, используя дополнительный I...Strategy подход, когда он сокращает код, копируемый между классами.

1 голос
/ 12 апреля 2010

«Найди то, что меняется, и запиши это».

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

В целом: нет, определенно не слишком сложно инкапсулировать различные понятия в свои собственные классы. Также нет ничего плохого в создании конкретных классов sealed или final. Это заставляет людей сознательно нарушать инкапсуляцию, прежде чем наследовать от чего-то, что, вероятно, должно не быть унаследованным.

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