Когда подкласс, а не дифференцировать поведение - PullRequest
9 голосов
/ 27 августа 2009

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

Например, скажем, у меня класс базовый автомобиль . В моей программе я буду иметь дело с тремя разными типами автомобилей. Гоночные машины , Автобусы и Семейные модели . У каждого будет своя реализация передач, как они поворачиваются и как расставляются сидения. Должен ли я разделить свой автомобиль на три разные модели или создать переменную типа и сделать общие механизмы поворота и сидения, чтобы они действовали по-разному в зависимости от выбранного типа автомобиля?

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

Ответы [ 5 ]

7 голосов
/ 31 августа 2009

Все кредиты AtmaWeapon из http://www.xtremevbtalk.com, ответ в этой теме

Ядром обеих ситуаций является то, что я считаю фундаментальным правилом объектно-ориентированного проектирования: принципом единой ответственности. Два способа выразить это:

"A class should have one, and only one, reason to change."
"A class should have one, and only one, responsibility."

SRP - это идеал, который не всегда может быть достигнут, и следование этому принципу hard . Я склонен стрелять за «У класса должно быть как можно меньше обязанностей». Наш мозг очень хорошо убеждает нас, что очень сложный отдельный класс менее сложен, чем несколько очень простых классов. В последнее время я начал делать все возможное, чтобы писать меньшие классы, и в моем коде значительно сократилось количество ошибок. Дайте ему шанс для нескольких проектов, прежде чем отклонить его.

Сначала я предлагаю вместо того, чтобы начинать дизайн с создания базового класса карты и трех дочерних классов, начинать с дизайна, который разделяет уникальное поведение каждой карты на вторичный класс, который представляет общее «поведение карты». Этот пост посвящен доказательству того, что этот подход является лучшим. Мне трудно быть конкретным, не имея достаточно глубоких знаний о вашем коде, но я буду использовать очень простое понятие карты:

Public Class Map
    Public ReadOnly Property MapType As MapType

    Public Sub Load(mapType)
    Public Sub Start()
End Class

MapType указывает, какой из трех типов карт представляет карта. Когда вы хотите изменить тип карты, вы вызываете Load() с типом карты, который вы хотите использовать; это делает все, что нужно для очистки текущего состояния карты, сброса фона и т. д. После загрузки карты вызывается Start () . Если на карте есть такие поведения, как «spawn monster x каждые y секунд», Start () отвечает за настройку этих действий.

Это то, что у вас есть сейчас, и вам стоит подумать, что это плохая идея. Поскольку я упомянул SRP, давайте посчитаем обязанности Map .

  • Он должен управлять информацией о состоянии для всех трех типов карт. (3+ обязанности *)
  • Load() должен понимать, как очистить состояние для всех трех типов карт и как установить начальное состояние для всех трех типов карт (6 обязанностей)
  • Start() должен знать, что делать для каждого типа карты. (3 обязанности)

** Технически каждая переменная является ответственностью, но я упростил ее. *

В итоге, если вы добавите четвертый тип карты? Вы должны добавить больше переменных состояния (1+ обязанностей), обновить Load(), чтобы иметь возможность очистить и инициализировать состояние (2 обязанности), и обновить Start() для обработки нового поведения (1 ответственность) , Итак:

Количество Map обязанностей: 12 +

Количество изменений, необходимых для новой карты: 4 +

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

Это тоже нелегко проверить. Предположим, вы хотите написать тест для сценария «Когда я создаю карту« монстров возрождения », карта должна порождать одного нового монстра каждые пять секунд». Легко обсудить, как вы можете это проверить: создайте карту, установите ее тип, запустите ее, подождите немного дольше пяти секунд и проверьте количество врагов. Однако наш интерфейс в настоящее время не имеет свойства «счетчик врагов». Мы могли бы добавить это, но что, если это единственная карта с количеством врагов? Если мы добавим свойство, у нас будет свойство, которое недопустимо в 2/3 случаев. Также не очень ясно, что мы тестируем карту «spawn monsters» без чтения кода теста, так как все тесты будут тестировать класс Map.

Вы, безусловно, могли бы сделать Map абстрактный базовый класс Start() MustOverride и получить по одному новому типу для каждого типа карты. Теперь ответственность за Load() лежит где-то еще, потому что объект не может заменить себя другим экземпляром. Вы можете также сделать фабричный класс для этого:

Class MapCreator
    Public Function GetMap(mapType) As Map
End Class

Теперь наша иерархия карт может выглядеть примерно так (для простоты была определена только одна производная карта):

Public MustInherit Class Map
    Public MustOverride Sub Start()
End Class

Public Class RentalMap
    Inherits Map

    Public Overrides Sub Start()
End Class

Load() больше не требуется по причинам, которые уже обсуждались. MapType излишен на карте, потому что вы можете проверить тип объекта, чтобы увидеть, что это такое (если у вас нет нескольких типов RentalMap, тогда он снова станет полезным.) Start() переопределяется в каждом производном классе, так что вы переместили обязанности государственного управления на отдельные занятия. Давайте сделаем еще одну проверку SRP:

Карта базового класса 0 обязанностей

Карта производного класса - Должен управлять государством (1) - Должен выполнять какую-то специфическую работу (1)

Всего: 2 обязанности

Добавление новой карты (То же, что и выше) 2 обязанности

Общее количество обязанностей в классе: 2

Стоимость добавления нового класса карты: 2

Это намного лучше. Как насчет нашего тестового сценария? Мы в лучшей форме, но все еще не совсем правы. Мы можем избежать использования свойства «количество врагов» в нашем производном классе, потому что каждый класс является отдельным, и мы можем привести к определенным типам карт, если нам нужна конкретная информация. Тем не менее, что если у вас есть RentalMapSlow и RentalMapFast? Вы должны продублировать свои тесты для каждого из этих классов, так как у каждого своя логика. Итак, если у вас есть 4 теста и 12 разных карт, вы будете писать и немного дорабатывать 48 тестов. Как мы это исправим?

Что мы делали, когда создавали производные классы? Мы определили часть класса, которая менялась каждый раз, и поместили ее в подклассы. Что если вместо подклассов мы создадим отдельный класс MapBehavior, который мы можем менять по своему усмотрению? Давайте посмотрим, как это может выглядеть с одним производным поведением:

Public Class Map
    Public ReadOnly Property Behavior As MapBehavior

    Public Sub SetBehavior(behavior)
    Public Sub Start()
End Class

Public MustInherit Class MapBehavior
    Public MustOverride Sub Start()
End Class

Public Class PlayerSpawnBehavior
    Public Property EnemiesPerSpawn As Integer
    Public Property MaximumNumberOfEnemies As Integer
    Public ReadOnly Property NumberOfEnemies As Integer

    Public Sub SpawnEnemy()
    Public Sub Start()
End Class

Теперь использование карты включает в себя присвоение ей MapBehavior и вызов Start(), который делегирует поведению Start(). Вся информация о состоянии находится в объекте поведения, поэтому карта не должна ничего знать об этом. Тем не менее, что, если вам нужен конкретный тип карты, кажется неудобным создавать поведение, а затем создавать карту, верно? Итак, вы получили несколько классов:

Public Class PlayerSpawnMap
    Public Sub New()
        MyBase.New(New PlayerSpawnBehavior())
    End Sub
End Class

Вот и все, одна строка кода для нового класса. Хотите карту спавна хард-плеера?

Public Class HardPlayerSpawnMap
    Public Sub New()
        ' Base constructor must be first line so call a function that creates the behavior
        MyBase.New(CreateBehavior()) 
    End Sub

    Private Function CreateBehavior() As MapBehavior
        Dim myBehavior As New PlayerSpawnBehavior()
        myBehavior.EnemiesPerSpawn = 10
        myBehavior.MaximumNumberOfEnemies = 300
    End Function
End Class

Итак, чем это отличается от наличия свойств в производных классах? С поведенческой точки зрения мало что отличается. С точки зрения тестирования, это большой прорыв. PlayerSpawnBehavior имеет свой набор тестов. Но поскольку HardPlayerSpawnMap и PlayerSpawnMap оба используют PlayerSpawnBehavior, то, если я проверил PlayerSpawnBehavior, мне не нужно писать никаких тестов, связанных с поведением, для карты, которая использует поведение! Давайте сравним тестовые сценарии.

В случае «один класс с параметром типа», если есть 3 уровня сложности для 3 поведений, и у каждого поведения есть 10 тестов, вы будете писать 90 тестов (не включая тесты, чтобы увидеть, исходят ли из каждого поведения) в другой сценарий.) В сценарии «производные классы» у вас будет 9 классов, каждый из которых требует 10 тестов: 90 тестов. В сценарии «класс поведения» вы напишите 10 тестов для каждого поведения: 30 тестов.

Вот подсчет ответственности: Карта несет 1 ответственность: отслеживать поведение. Поведение имеет 2 обязанности: поддерживать состояние и выполнять действия.

Общее количество обязанностей в классе: 3

Стоимость добавления нового класса карты: 0 (повторное использование поведения) или 2 (новое поведение)

Итак, мое мнение таково, что сценарий "класса поведения" написать не сложнее, чем сценарий "производных классов", но он может значительно снизить нагрузку на тестирование. Я читал о таких методах и отвергал их как «слишком много проблем» в течение многих лет, и только недавно осознал их ценность. Вот почему я написал почти 10 000 символов, чтобы объяснить и обосновать это.

3 голосов
/ 27 августа 2009

Вы должны создавать подклассы там, где ваш дочерний тип является своего рода специализацией родительского типа. Другими словами, вы должны избегать наследования, если вам просто нужна функциональность. Как гласит Принцип замещения Лискова : «если S является подтипом T, то объекты типа T в программе могут быть заменены объектами типа S без изменения каких-либо желательных свойств этой программы»

1 голос
/ 27 августа 2009

Я не программирую на C #, но в Ruby on Rails, Xcode и Mootools (инфраструктура javascript OOP) можно задать тот же вопрос.

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

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

1 голос
/ 27 августа 2009

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

gutofb7 пригвоздил его к голове, когда вы хотите создать подкласс. Приведу более конкретный пример: будет ли иметь какое-либо значение в вашей программе какой-либо автомобиль в вашей программе, с каким типом автомобиля вы работали? Теперь, если вы вложите в подкласс Map, сколько кода вам нужно написать для работы с конкретными подклассами?

1 голос
/ 27 августа 2009

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

Учитывая ваш комментарий о том, что каждый тип карты должен быть концептуально различным, я бы предложил разделить на подклассы, так как это, похоже, соответствует принципу подстановки Лискова. Однако это не значит, что вы должны полностью отказаться от композиции. Для тех свойств, которые есть у каждого типа карты, но они могут иметь различное поведение / реализацию, вам следует рассмотреть возможность включения в базовый класс их компонентов. Таким образом, вы все равно можете смешивать и сопоставлять функциональность, если вам нужно, при этом используя наследование для разделения задач.

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