2010-04-11 2 views
6

Неплохая ли политика, чтобы иметь много «рабочих» классов (таких как классы стратегии), что только одно?Взрыв классов стратегии и «действия»

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

  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 
} 

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

Благодаря

ответ

2

Прогулка, запуск и плавание кажутся реализацией, а не интерфейсами. У вас может быть интерфейс 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 
    } 

} 

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

EDIT: добавлен материал о IMOVE и Iact

EDIT: после некоторых комментариев

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

void SomeFunction(IWalk alienMonster) {...} 

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

+1

Еще одна приятная вещь в этом заключается в том, что, когда вы хотите перейти от A к B, вы можете просто перебрать свой список интерфейсов локомоции и спросить каждую стоимость вместо жесткого кодирования «получить стоимость ходьбы, получить стоимость прогона получить стоимость плавания, получить стоимость вылета, получить телепортную стоимость, ... »и нести технический долг, чтобы сохранить этот список в соответствии с новыми типами локомотивов. –

+0

Не могли бы вы объяснить, почему Walk, Run и Swim вам больше нравятся реализации, чем интерфейсы? Должна ли вообще модель интерфейса что-то определенному классу «делать»? ISwim будет означать, что мой монстр (или танк, или какой бы класс, который я пытаюсь реализовать) не мог плавать. Тогда я мог бы реализовать несколько различных классов стратегий (например), таких как SwimInWater SwimInAcid и т. Д. Что мне здесь не хватает? –

+1

Позвольте мне перефразировать «Интерфейсы не описывают, что-то может сделать как таковое». Рекомендуется использовать интерфейсы логичным и последовательным образом. Использование ILocomotion to Growl было бы плохим выбором. Но ходить, плавать, бегать, летать, телепортироваться и т. Д. - все это просто формы движения. Абстрагируя их на интерфейс движения, вы можете значительно упростить свой код. Вы можете иметь 100 реализаций ILocomotion и быть лучше, чем 20 IWalk, 20 IRun, 20 ISwim, 20 ISkip, 2 ITeleport, 18 IJump-реализаций. Если что-то не соответствует интерфейсу ILocomotion, создайте новый интерфейс. – Felan

2

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

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

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

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

1

«Найдите то, что меняется и инкапсулирует его».

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

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

2

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

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

Смежные вопросы