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

Как бы вы реализовали шаблон дизайна "trait" в С#?

Я знаю, что функция не существует в С#, но PHP недавно добавил функцию под названием Traits, которую я считал немного глупым сначала, пока я не начал об этом думать.

Скажем, у меня есть базовый класс под названием Client. Client имеет одно свойство, называемое Name.

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

Теперь клиент А приходит и говорит, что ему также нужно отслеживать клиентский вес. Заказчику B не нужен вес, но он хочет отслеживать высоту. Клиент C хочет отслеживать как вес, так и высоту.

С чертами мы можем сделать черты характера Weight и Height:

class ClientA extends Client use TClientWeight
class ClientB extends Client use TClientHeight
class ClientC extends Client use TClientWeight, TClientHeight

Теперь я могу удовлетворить все потребности моих клиентов, не добавляя лишнего пуха в класс. Если мой клиент вернется позже и говорит: "О, мне очень нравится эта функция, могу ли я ее тоже?", Я просто обновляю определение класса, чтобы добавить дополнительный признак.

Как бы вы это сделали в С#?

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

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

4b9b3361

Ответ 1

Синтаксис можно получить с помощью интерфейсов маркеров и методов расширения.

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

public class Client {
  public double Weight { get; }

  public double Height { get; }
}

public interface TClientWeight {
  double Weight { get; }
}

public interface TClientHeight {
  double Height { get; }
}

public class ClientA: Client, TClientWeight { }

public class ClientB: Client, TClientHeight { }

public class ClientC: Client, TClientWeight, TClientHeight { }

public static class TClientWeightMethods {
  public static bool IsHeavierThan(this TClientWeight client, double weight) {
    return client.Weight > weight;
  }
  // add more methods as you see fit
}

public static class TClientHeightMethods {
  public static bool IsTallerThan(this TClientHeight client, double height) {
    return client.Height > height;
  }
  // add more methods as you see fit
}

Используйте это:

var ca = new ClientA();
ca.IsHeavierThan(10); // OK
ca.IsTallerThan(10); // compiler error

Изменить: Был задан вопрос о том, как можно сохранить дополнительные данные. Это также можно решить, сделав некоторое дополнительное кодирование:

public interface IDynamicObject {
  bool TryGetAttribute(string key, out object value);
  void SetAttribute(string key, object value);
  // void RemoveAttribute(string key)
}

public class DynamicObject: IDynamicObject {
  private readonly Dictionary<string, object> data = new Dictionary<string, object>(StringComparer.Ordinal);

  bool IDynamicObject.TryGetAttribute(string key, out object value) {
    return data.TryGet(key, out value);
  }

  void IDynamicObject.SetAttribute(string key, object value) {
    data[key] = value;
  }
}

И тогда методы-признаки могут добавлять и извлекать данные, если "интерфейс признаков" наследуется от IDynamicObject:

public class Client: DynamicObject { /* implementation see above */ }

public interface TClientWeight, IDynamicObject {
  double Weight { get; }
}

public class ClientA: Client, TClientWeight { }

public static class TClientWeightMethods {
  public static bool HasWeightChanged(this TClientWeight client) {
    object oldWeight;
    bool result = client.TryGetAttribute("oldWeight", out oldWeight) && client.Weight.Equals(oldWeight);
    client.SetAttribute("oldWeight", client.Weight);
    return result;
  }
  // add more methods as you see fit
}

Примечание: при реализации IDynamicMetaObjectProvider объект даже разрешил бы выставлять динамические данные через DLR, делая доступ к дополнительным свойствам прозрачным при использовании с ключевым словом dynamic.

Ответ 2

Язык С# (по крайней мере, до версии 5) не поддерживает Traits.

Однако Scala имеет черты и Scala работает на JVM (и CLR). Поэтому это не вопрос времени выполнения, а просто языка.

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

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

Счастливое кодирование.

Ответ 3

Существует академический проект, разработанный Стефаном Рейхартом из группы Software Composition в Бернском университете (Швейцария), который обеспечивает истинную реализацию черт языка С#.

Посмотрите документ (PDF) на CSharpT для полного описания того, что он сделал, на основе монокомпилятора.

Вот пример того, что можно записать:

trait TCircle
{
    public int Radius { get; set; }
    public int Surface { get { ... } }
}

trait TColor { ... }

class MyCircle
{
    uses { TCircle; TColor }
}

Ответ 4

Я хотел бы указать на NRoles, эксперимент с ролями в С#, где роли похожи на черты.

NRoles использует посткомпилятор, чтобы переписать IL и внедрить методы в класс. Это позволяет вам писать такой код:

public class RSwitchable : Role
{
    private bool on = false;
    public void TurnOn() { on = true; }
    public void TurnOff() { on = false; }
    public bool IsOn { get { return on; } }
    public bool IsOff { get { return !on; } }
}

public class RTunable : Role
{
    public int Channel { get; private set; }
    public void Seek(int step) { Channel += step; }
}

public class Radio : Does<RSwitchable>, Does<RTunable> { }

где класс Radio реализует RSwitchable и RTunable. За кулисами Does<R> интерфейс без членов, поэтому в основном Radio компилируется в пустой класс. RSwitchable IL после компиляции внедряет методы RSwitchable и RTunable в Radio, которые затем можно использовать так, как будто они действительно получены из двух ролей (из другой сборки):

var radio = new Radio();
radio.TurnOn();
radio.Seek(42);

Чтобы использовать radio непосредственно перед перезаписью (то есть в той же сборке, в которой объявлен тип Radio), вы должны прибегнуть к методам расширений As<R>():

radio.As<RSwitchable>().TurnOn();
radio.As<RTunable>().Seek(42);

поскольку компилятор не позволяет вызывать TurnOn или Seek непосредственно в классе Radio.

Ответ 5

Это действительно рекомендуемое расширение для ответа Lucero, где все хранилище находилось в базовом классе.

Как насчет использования свойств зависимостей для этого?

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

using System.Windows;

public class Client : DependencyObject
{
    public string Name { get; set; }

    public Client(string name)
    {
        Name = name;
    }

    //add to descendant to use
    //public double Weight
    //{
    //    get { return (double)GetValue(WeightProperty); }
    //    set { SetValue(WeightProperty, value); }
    //}

    public static readonly DependencyProperty WeightProperty =
        DependencyProperty.Register("Weight", typeof(double), typeof(Client), new PropertyMetadata());


    //add to descendant to use
    //public double Height
    //{
    //    get { return (double)GetValue(HeightProperty); }
    //    set { SetValue(HeightProperty, value); }
    //}

    public static readonly DependencyProperty HeightProperty =
        DependencyProperty.Register("Height", typeof(double), typeof(Client), new PropertyMetadata());
}

public interface IWeight
{
    double Weight { get; set; }
}

public interface IHeight
{
    double Height { get; set; }
}

public class ClientA : Client, IWeight
{
    public double Weight
    {
        get { return (double)GetValue(WeightProperty); }
        set { SetValue(WeightProperty, value); }
    }

    public ClientA(string name, double weight)
        : base(name)
    {
        Weight = weight;
    }
}

public class ClientB : Client, IHeight
{
    public double Height
    {
        get { return (double)GetValue(HeightProperty); }
        set { SetValue(HeightProperty, value); }
    }

    public ClientB(string name, double height)
        : base(name)
    {
        Height = height;
    }
}

public class ClientC : Client, IHeight, IWeight
{
    public double Height
    {
        get { return (double)GetValue(HeightProperty); }
        set { SetValue(HeightProperty, value); }
    }

    public double Weight
    {
        get { return (double)GetValue(WeightProperty); }
        set { SetValue(WeightProperty, value); }
    }

    public ClientC(string name, double weight, double height)
        : base(name)
    {
        Weight = weight;
        Height = height;
    }

}

public static class ClientExt
{
    public static double HeightInches(this IHeight client)
    {
        return client.Height * 39.3700787;
    }

    public static double WeightPounds(this IWeight client)
    {
        return client.Weight * 2.20462262;
    }
}

Ответ 6

Черты могут быть реализованы в С# 8 с использованием методов интерфейса по умолчанию. По этой причине в Java 8 также появились методы интерфейса по умолчанию.

Используя С# 8, вы можете написать почти точно то, что вы предложили в вопросе. Черты реализуются интерфейсами IClientWeight, IClientHeight, которые обеспечивают реализацию по умолчанию для их методов. В этом случае они просто возвращают 0:

public interface IClientWeight
{
    int getWeight()=>0;
}

public interface IClientHeight
{
    int getHeight()=>0;
}

public class Client
{
    public String Name {get;set;}
}

ClientA и ClientB имеют черты, но не реализуют их. ClientC реализует только IClientHeight и возвращает другое число, в данном случае 16:

class ClientA : Client, IClientWeight{}
class ClientB : Client, IClientHeight{}
class ClientC : Client, IClientWeight, IClientHeight
{
    public int getHeight()=>16;
}

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

ClientC реализует интерфейс IClientHeight, поэтому вызывается его собственный метод. Метод доступен через сам класс.

public class C {
    public void M() {        
        //Accessed through the interface
        IClientHeight clientB = new ClientB();        
        clientB.getHeight();

        //Accessed directly or through the class
        var clientC = new ClientC();        
        clientC.getHeight();
    }
}

Этот пример SharpLab.io показывает код, полученный из этого примера

Многие функции признаков, описанные в обзоре PHP, могут быть легко реализованы с помощью методов интерфейса по умолчанию. Черты (интерфейсы) могут быть объединены. Также возможно определить абстрактные методы, чтобы заставить классы выполнять определенные требования.

Допустим, мы хотим, чтобы у наших признаков были sayHeight() и sayWeight() которые возвращают строку с ростом или весом. Им нужен какой-то способ заставить выставляемые классы (термин украден из руководства по PHP) реализовать метод, который возвращает рост и вес:

public interface IClientWeight
{
    abstract int getWeight();
    String sayWeight()=>getWeight().ToString();
}

public interface IClientHeight
{
    abstract int getHeight();
    String sayHeight()=>getHeight().ToString();
}

//Combines both traits
public interface IClientBoth:IClientHeight,IClientWeight{}

Теперь клиенты должны реализовать getHeight() или getWeight() но им не нужно ничего знать о методах say.

Это предлагает более чистый способ украшения

Ссылка SharpLab.io для этого образца.

Ответ 7

Основываясь на что предложил Лусеро, я придумал следующее:

internal class Program
{
    private static void Main(string[] args)
    {
        var a = new ClientA("Adam", 68);
        var b = new ClientB("Bob", 1.75);
        var c = new ClientC("Cheryl", 54.4, 1.65);

        Console.WriteLine("{0} is {1:0.0} lbs.", a.Name, a.WeightPounds());
        Console.WriteLine("{0} is {1:0.0} inches tall.", b.Name, b.HeightInches());
        Console.WriteLine("{0} is {1:0.0} lbs and {2:0.0} inches.", c.Name, c.WeightPounds(), c.HeightInches());
        Console.ReadLine();
    }
}

public class Client
{
    public string Name { get; set; }

    public Client(string name)
    {
        Name = name;
    }
}

public interface IWeight
{
    double Weight { get; set; }
}

public interface IHeight
{
    double Height { get; set; }
}

public class ClientA : Client, IWeight
{
    public double Weight { get; set; }
    public ClientA(string name, double weight) : base(name)
    {
        Weight = weight;
    }
}

public class ClientB : Client, IHeight
{
    public double Height { get; set; }
    public ClientB(string name, double height) : base(name)
    {
        Height = height;
    }
}

public class ClientC : Client, IWeight, IHeight
{
    public double Weight { get; set; }
    public double Height { get; set; }
    public ClientC(string name, double weight, double height) : base(name)
    {
        Weight = weight;
        Height = height;
    }
}

public static class ClientExt
{
    public static double HeightInches(this IHeight client)
    {
        return client.Height * 39.3700787;
    }

    public static double WeightPounds(this IWeight client)
    {
        return client.Weight * 2.20462262;
    }
}

Вывод:

Adam is 149.9 lbs.
Bob is 68.9 inches tall.
Cheryl is 119.9 lbs and 65.0 inches.

Это не так хорошо, как хотелось бы, но это тоже не так.

Ответ 8

Это похоже на PHP-версию Aspect Oriented Programming. В некоторых случаях есть инструменты, помогающие, например, PostSharp или MS Unity. Если вы хотите использовать свой собственный код, инъекция кода с использованием атрибутов С# - это один из подходов или как предлагаемые методы расширения для ограниченных случаев.

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