2012-05-23 2 views
36

Я знаю, что функция не существует в C#, но недавно PHP добавила функцию, называемую Traits, которую я считал сначала немного глупой, пока не начал думать об этом.Как бы вы реализовали «шаблонный» дизайн-шаблон в C#?

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

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

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

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

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

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

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

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

(«Заказчик», я имею в виду буквального человека, который использовал меня в качестве разработчика, тогда как «клиент» я имею в виду класс программирования, у каждого из моих клиентов есть клиенты, которым они хотят записать информацию)

+2

Ну, вы вполне можете имитировать черты в C# с помощью интерфейсов маркеров и методов расширения. – Lucero

+1

@ Lucero Это не черты и не имеют возможности добавлять новых членов (между прочим). Тем не менее, методы расширения являются отличными. –

+2

@Lucero: Это будет работать для добавления дополнительных методов, но что делать, если я хочу хранить дополнительные данные на объекте клиента? – mpen

ответ

41

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

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

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 

Edit: Был поднят вопрос о том, как дополнительные данные могут быть сохранены. Это также можно решить, выполнив некоторые дополнительные кодирования:

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.

+5

Итак, вы говорите, что помещаете все данные в базовый класс и все реализации метода в методах расширения у которых есть крючки на интерфейсах? Это любопытное решение, но возможно работоспособное. Моя единственная говядина - это то, что вы делаете классы клиентов нести много «мертвого веса» (неиспользуемые члены). С некоторой фантастической сериализацией ее не нужно будет сохранять на диск, но она все еще потребляет память. – mpen

+0

"Вид". Я уверен, что не могу думать ни о чем лучше на языке C#, поэтому +1. Тем не менее, я не придаю этому то же самое, что и «Черта». (Ограничение ограничения ограничено Mark.) –

+0

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

8

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

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

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

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

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

+0

Я не совсем уверен, что это ответит. Вы предлагаете мне взломать что-то, чтобы предварительно обработать мой код на C#? – mpen

+0

@Mark Нет. Я был 1) Предлагая C#, язык не может его поддерживать (хотя, возможно, с динамическими прокси-серверами? Этот уровень магии превыше меня.) 2) Эти черты не влияют на MRO и могут быть «имитированы вручную» «; т. е. Тит может быть сплющен в каждый класс, в который он смешивается, как и с композицией. –

+0

Я не знаю, что такое «MRO», можете ли вы это объяснить? И как бы я «имитировать их вручную»? Это то, о чем я прошу ... Я не могу придумать хороший способ инкапсулировать дополнительную функциональность. – mpen

0

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

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

+0

Позволяет ли AoP/PostSharp/Unity добавлять новые элементы, которые становятся частью системы * static * type? (Мой ограниченный опыт AoP был просто с точками сокращения аннотаций и т. Д.) –

+0

PostSharp перезаписывает код IL и должен быть в состоянии сделать это, да. – Lucero

+0

Да, я так считаю, через аспекты введения член/интерфейс (на уровне IL, как указано). Мой опыт также ограничен, но у меня не было много практических возможностей, чтобы слишком глубоко проникнуть в этот подход. –

2

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

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

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

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; 
    } 
} 
+0

Почему мы должны использовать классы WPF здесь? – Javid

4

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

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

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 компилируется в пустой класс.После компиляции 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

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

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

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

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

trait TColor { ... } 

class MyCircle 
{ 
    uses { TCircle; TColor } 
} 
1

Опираясь на what Lucero suggested, я пришел с этим:

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. 

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

+0

IMHO это лучше, чем принятый метод –

+0

Все еще не так эффективен, как PHP. –

Смежные вопросы