Подтвердить что ты не робот

С# имеет абстрактные классы и интерфейсы, должен ли он также иметь "mixins"?

Каждый раз так часто я сталкиваюсь с тем случаем, когда я хочу, чтобы в коллекции классов существовала аналогичная логика. Например, возможно, мне нужны как Bird, так и Airplane, чтобы иметь возможность Fly(). Если вы думаете "шаблон стратегии", я бы согласился, но даже со стратегией иногда невозможно избежать дублирования кода.

Например, скажем, применимо следующее (и это очень похоже на реальную ситуацию, с которой я недавно столкнулся):

  • Оба Bird и Airplane должны содержать экземпляр объекта, который реализует IFlyBehavior.
  • Оба Bird и Airplane должны запрашивать экземпляр IFlyBehavior Fly() при вызове OnReadyToFly().
  • Оба Bird и Airplane должны запрашивать экземпляр IFlyBehavior Land() при вызове OnReadyToLand().
  • OnReadyToFly() и OnReadyToLand() являются частными.
  • Bird наследует Animal и Airplane наследует PeopleMover.

Теперь предположим, что позже мы добавим Moth, HotAirBalloon и еще 16 объектов, и пусть все они следуют одному и тому же шаблону.

Теперь нам понадобится 20 копий следующего кода:

private IFlyBehavior _flyBehavior;

private void OnReadyToFly()
{
    _flyBehavior.Fly();
}

private void OnReadyToLand()
{
    _flyBehavior.Land();
}

Две вещи, которые мне не нравятся:

  • Это не очень DRY (одни и те же девять строк кода повторяются снова и снова). Если мы обнаружили ошибку или добавили BankRight() в IFlyBehavior, нам нужно было бы распространить изменения на все 20 классов.

  • Нет никакого способа обеспечить, чтобы все 20 классов последовательно выполняли эту повторяющуюся внутреннюю логику. Мы не можем использовать интерфейс, потому что интерфейсы разрешают только публичные члены. Мы не можем использовать абстрактный базовый класс, потому что объекты уже наследуют базовые классы, а С# не допускает множественное наследование (и даже если классы еще не наследовали классы, мы могли бы позже добавить новое поведение, которое реализует, скажем, ICrashable, поэтому абстрактный базовый класс не всегда будет жизнеспособным решением).

Что делать, если...?

Что делать, если у С# была новая конструкция, например pattern или template или [заполните вашу идею здесь], которая работала как интерфейс, но разрешала вам вводить частные или защищенные модификаторы доступа для членов? Вам все равно необходимо обеспечить реализацию для каждого класса, но если ваш класс реализовал шаблон PFlyable, у вас есть хотя бы один способ обеспечить, чтобы каждый класс имел необходимый шаблонный код для вызова Fly() и Land(). И с помощью современной среды разработки, такой как Visual Studio, вы сможете автоматически генерировать код с помощью команды "Выполнять шаблон".

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

Вопросы:

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

Обновление

Из комментария AakashM, я считаю, что уже есть имя для функции, которую я запрашиваю: Mixin. Итак, я думаю, мой вопрос можно сократить до: "Если С# разрешить Mixins?"

4b9b3361

Ответ 1

Проблема, которую вы описываете, может быть решена с использованием шаблона посетителя (все можно решить с помощью шаблона посетителя, так что будьте осторожны!)

Шаблон посетителя позволяет переместить логику реализации в новый класс. Таким образом, вам не нужен базовый класс, и посетитель отлично работает над разными деревьями наследования.

Подводя итог:

  • Новые функции не нужно добавлять ко всем разным типам
  • Вызов посетителя может быть привязан к корню каждой иерархии классов.

Для справки см. Шаблон посетителя

Ответ 2

Visual Studio уже предлагает это в форме "бедных манов" с фрагментами кода. Кроме того, с помощью инструментов рефакторинга a la ReSharper (и, возможно, даже встроенной поддержки рефакторинга в Visual Studio) вы получите долгий путь в обеспечении согласованности.

[EDIT: я не думал о методах расширения, этот подход приносит вам еще больше (вам нужно сохранить _flyBehaviour как частную переменную). Это делает остальную часть моего ответа, вероятно, устаревшей...]

Тем не менее; только ради обсуждения: как это можно улучшить? Вот мое предложение.

Можно предположить, что следующая версия будет поддерживаться будущей версией компилятора С#:

// keyword 'pattern' marks the code as eligible for inclusion in other classes
pattern WithFlyBehaviour
{
   private IFlyBehavior_flyBehavior;

   private void OnReadyToFly()
   {
      _flyBehavior.Fly();
   }

   [patternmethod]
   private void OnReadyToLand()
   {
      _flyBehavior.Land();
   }     
}

Что вы могли бы использовать тогда что-то вроде:

 // probably the attribute syntax can not be reused here, but you get the point
 [UsePattern(FlyBehaviour)] 
 class FlyingAnimal
 {
   public void SetReadyToFly(bool ready)
   {
      _readyToFly = ready;
      if (ready) OnReadyToFly(); // OnReadyToFly() callable, although not explicitly present in FlyingAnimal
   }

 }

Будет ли это улучшение? Вероятно. Это действительно стоит? Может быть,...

Ответ 3

Нельзя использовать методы расширения для этого

    public static void OnReadyToFly(this IFlyBehavior flyBehavior)
    {
          _flyBehavior.Fly()
    }

Это подражает желаемой функции (или Mixins)

Ответ 4

Вы только что описали аспектно-ориентированное программирование.

Одна популярная реализация AOP для С# выглядит PostSharp (Основной сайт, кажется, работает/не работает для меня, хотя это является прямой страницей "О программе" ).


Чтобы следить за комментарием: я не уверен, поддерживает ли PostSharp его, но я думаю, что вы говорите эту часть АОП:

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

Ответ 5

Я буду использовать методы расширения для реализации поведения, как показывает код.

  • Пусть объекты Bird и Plane реализуют свойство для объекта IFlyBehavior для интерфейса IFlyer

    public interface IFlyer 
    {  
        public IFlyBehavior FlyBehavior 
    }
    
    public Bird : IFlyer
    {
        public IFlyBehaviour FlyBehavior {get;set;}
    }
    
    public Airplane : IFlyer
    {
        public IFlyBehaviour FlyBehavior {get;set;}
    }
    
  • Создайте класс расширения для IFlyer

    public IFlyerExtensions
    {
    
        public void OnReadyToFly(this IFlyer flyer) 
        {
            flyer.FlyBehavior.Fly(); 
        }
    
        public void OnReadyToLand(this IFlyer flyer) 
        {
            flyer.FlyBehavior.Land(); 
        }
    }
    

Ответ 6

Не могли бы вы получить такое поведение, используя новый ExpandoObject в .NET 4.0?

Ответ 7

Scala traits были разработаны для решения такого сценария. Там также некоторые исследования включают черты в С#.

ОБНОВЛЕНИЕ: Я создал свой собственный эксперимент, чтобы роли в С#. Посмотрите.