Как максимизировать повторное использование кода в этом интерфейсе vs Inheritance С# Example

Вдохновленный отличным видео на тему "Одобрить композицию объекта над наследованием", в которой использовались примеры JavaScript; Я хотел попробовать это на С#, чтобы проверить свое понимание концепции, но это не так, как я надеялся.

/// PREMISE
// Animal base class, Animal can eat
public class Animal
{
    public void Eat() { }
}
// Dog inherits from Animal and can eat and bark
public class Dog : Animal
{
    public void Bark() { Console.WriteLine("Bark"); }
}
// Cat inherits from Animal and can eat and meow
public class Cat : Animal
{
    public void Meow() { Console.WriteLine("Meow"); }
}
// Robot base class, Robot can drive
public class Robot
{
    public void Drive() { }
}

Проблема заключается в том, что я хочу добавить класс RobotDog, который может использовать Bark and Drive, но не есть.


Первое решение - создать RobotDog в качестве подкласса Robot,

public class RobotDog : Robot
{
    public void Bark() { Console.WriteLine("Bark"); }
}

но чтобы дать ему функцию Bark, мы должны были скопировать и вставить функцию Dog Bark, так что теперь у нас есть повторяющийся код.


Второе решение состояло в том, чтобы создать общий суперкласс с методом Bark, который затем и классы Animal и Robot, унаследованные от

public class WorldObject
{
    public void Bark() { Console.WriteLine("Bark"); }
}
public class Animal : WorldObject { ... }
public class Robot : WorldObject { ... }

Но теперь КАЖДОЕ животное и КАЖДЫЙ робот будут иметь метод Барка, который большинству из них не нужен. Продолжая этот шаблон, подклассы будут нагружены методами, которые им не нужны.


Третьим решением было создание интерфейса IBarkable для классов, которые могут Bark

public interface IBarkable
{
    void Bark();
}

И реализуйте его в классах Dog и RobotDog

public class Dog : Animal, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}
public class RobotDog : Robot, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}

Но снова у нас есть повторяющийся код!


Четвертый метод состоял в том, чтобы снова использовать интерфейс IBarkable, но создать класс-помощник Bark, который затем выполняет каждая из реализаций интерфейса Dog и RobotDog.

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


Пятое предложенное решение (hacky?) заключалось в том, чтобы повесить метод расширения с пустого интерфейса IBarkable, так что если вы реализуете IBarkable, то вы можете Bark

public interface IBarker {    }
public static class ExtensionMethods
{
    public static void Bark(this IBarker barker) { 
        Console.WriteLine("Woof!"); 
    }
}

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


Каков наилучший объектно-ориентированный способ добавления класса RobotDog к этому примеру?

Ответ 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/


Как вывод. Если вы будете следовать этому дизайну, я бы рассмотрел возможность использования третьей версии. Используйте "Свойства", которые содержат ваши объекты "Поведение", и вы можете напрямую использовать эти поведения.

Ответ 2

Я думаю, что это скорее концептуальная дилемма, а не проблема композиции.

Когда вы говорите: И реализовать его в классах Dog и RobotDog

public class Dog : Animal, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}
public class RobotDog : Robot, IBarkable
{
    public void IBarkable.Bark() { Console.WriteLine("Bark"); }
}

Но снова у нас есть повторяющийся код!

Если Dog и RobotDog имеют одну и ту же реализацию Bark(), они должны наследоваться от класса Animal. Но если их реализации Bark() различны, имеет смысл получить от интерфейса IBarkable. В противном случае, где различие между Dog и RobotDog?