Ответ 1
Сначала, если вы хотите следовать "Композиции над наследованием", более половины ваших решений не подходят, поскольку вы все еще используете наследование в них.
Фактически реализуя его с помощью "Композиции над наследованием", существует несколько разных способов, возможно, каждый из них имеет свои преимущества и недостатки. Сначала один способ, который возможен, но не в С# в настоящее время. По крайней мере, не с некоторым расширением, которое переписывает код IL. Одна идея, как правило, использовать mixins. Таким образом, у вас есть интерфейсы и класс Mixin. Миксин в основном содержит только методы, которые "вводятся" в класс. Они не вытекают из этого. Таким образом, у вас может быть класс вроде этого (весь код находится в псевдокоде)
class RobotDog
implements interface IEat, IBark
implements mixin MEat, MBark
IEat
и IBark
предоставляют интерфейсы, тогда как MEat
и MBark
будут mixins с некоторой реализацией по умолчанию, которую вы могли бы ввести. Такой дизайн возможен в JavaScript, но не в настоящее время на С#. Это имеет то преимущество, что в конце вы имеете класс RobotDog
, который имеет все методы IEat
и IBark
с общей реализацией. И это также является недостатком в то же время, потому что вы создаете большие классы с большим количеством методов. Вдобавок к этому могут быть конфликты методов. Например, если вы хотите ввести два разных интерфейса с тем же именем/подписями. Как хороший подход выглядит в первую очередь, я думаю, что недостатки большие, и я бы не поощрял такой дизайн.
Поскольку С# не поддерживает Mixins напрямую, вы можете использовать методы расширения, чтобы каким-то образом перестроить проект выше. Таким образом, у вас все еще есть интерфейсы IEat
и IBark
. И вы предоставляете методы расширения для интерфейсов. Но он имеет те же недостатки, что и реализация mixin. На объекте появляются все методы, проблемы с столкновением имен методов. Кроме того, идея композиции также заключается в том, что вы можете обеспечить различные реализации. У вас также могут быть разные Mixins для одного и того же интерфейса. И вдобавок к этому, mixins просто существуют для какой-то реализации по умолчанию, идея по-прежнему заключается в том, что вы можете перезаписать или изменить метод.
Выполнение подобных действий с помощью метода расширений возможно, но я бы не использовал такой дизайн. Теоретически вы можете создать несколько разных пространств имен, поэтому в зависимости от того, какое пространство имен вы загружаете, вы получаете другой метод расширения с другой реализацией. Но такой дизайн кажется мне более неудобным. Поэтому я бы не использовал такой дизайн.
Типичный способ решения этой проблемы - ожидание полей для каждого поведения, которое вы хотите. Итак, ваш RobotDog выглядит так
class RobotDog(ieat, ibark)
IEat Eat = ieat
IBark Bark = ibark
Итак, это означает. У вас есть класс, который содержит два свойства Eat
и Bark
. Эти свойства имеют тип IEat
и IBark
. Если вы хотите создать экземпляр RobotDog
, вам необходимо передать конкретную реализацию IEat
и IBark
, которую вы хотите использовать.
let eat = new CatEat()
let bark = new DogBark()
let robotdog = new RobotDog(eat, bark)
Теперь RobotDog будет есть как кошка, а Bark - как Собака. Вы просто можете назвать то, что должен сделать ваш RobotDog.
robotdog.Eat.Fruit()
robotdof.Eat.Drink()
robotdog.Bark.Loud()
Теперь поведение вашего RobotDog полностью зависит от введенных объектов, которые вы предоставляете при конструировании вашего объекта. Вы также можете переключать поведение во время выполнения с другим классом. Если ваш RobotDog находится в игре, и Barking получает обновление, вы просто можете заменить Bark во время выполнения другим объектом и поведением, которое вы хотите
robotdog.Bark <- new DeadlyScreamBarking()
В любом случае, изменив его или создав новый объект. Вы можете использовать изменяемый или неизменный дизайн, это зависит от вас. Итак, у вас есть совместное использование кода. По крайней мере, мне нравится стиль намного больше, потому что вместо того, чтобы иметь объект с сотнями методов, у вас в основном есть первый слой с разными объектами, каждый из которых имеет четкую разницу. Если вы, например, добавляете Moving в свой класс RobotDog, вы просто можете добавить свойство "IMovable", и этот интерфейс может содержать несколько методов, таких как MoveTo
, CalculatePath
, Forward
, SetSpeed
и т.д. Они были бы легко доступны под robotdog.Move.XYZ
. У вас также нет проблем с сталкивающимися методами. Например, могут быть методы с одинаковым именем в каждом классе без каких-либо проблем. И сверху. Вы также можете иметь несколько типов поведения одного и того же типа! Например, Health and Shield может использовать один и тот же тип. Например, простой тип "MinMax", который содержит мин/макс и текущее значение и методы для их работы. Здравоохранение /Shield в основном имеют такое же поведение, и вы можете легко использовать два из них в одном классе с таким подходом, потому что не существует метода/свойства или события.
robotdog.Health.Increase(10)
robotdog.Shield.Increase(10)
Предыдущая конструкция может быть немного изменена, но я не думаю, что она делает ее лучше. Но многие люди бездумно принимают каждый образец дизайна или закон, надеясь, что он автоматически сделает все лучше. То, что я хочу назвать здесь, - это часто называемый Law-of-Demeter
, который я считаю ужасным, особенно в этом примере. На самом деле существует много дискуссий о том, хорошо это или нет. Я думаю, что это нехорошее правило следовать, и в этом случае это также становится очевидным. Если вы следуете за ним, вы должны реализовать метод для каждого объекта, который у вас есть. Поэтому вместо
robotdog.Eat.Fruit()
robotdog.Eat.Drink()
вы реализуете методы RobotDog, которые вызывают некоторый метод в поле Eat, так что с чем вы закончили?
robotdog.EatFruit()
robotdog.EatDrink()
Вам также нужно еще раз решить конфликты, такие как
robotdog.IncreaseHealt(10)
robotdog.IncreaseShield(10)
На самом деле вы просто пишете множество методов, которые просто делегируют некоторые другие методы в поле. Но что ты выиграл? В принципе ничего нет. Вы просто следовали безмозглым правилу. Теоретически можно сказать. Но EatFruit()
может сделать что-то другое или сделать что-то еще до вызова Eat.Fruit()
. Вейл да, что может быть. Но если вы хотите другое поведение Eat, тогда вы просто создаете еще один класс, который реализует IEat
, и вы назначаете этот класс роботологу при его создании.
В этом смысле Закон Деметры - это не точечное упражнение.
http://haacked.com/archive/2009/07/14/law-of-demeter-dot-counting.aspx/
Как вывод. Если вы будете следовать этому дизайну, я бы рассмотрел возможность использования третьей версии. Используйте "Свойства", которые содержат ваши объекты "Поведение", и вы можете напрямую использовать эти поведения.