Как бы вы реализовали шаблон дизайна "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 >
Ответы
Ответ 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. Если вы хотите использовать свой собственный код, инъекция кода с использованием атрибутов С# - это один из подходов или как предлагаемые методы расширения для ограниченных случаев.
Действительно зависит от того, насколько сложно вы хотите получить. Если вы пытаетесь построить что-то сложное, я бы посмотрел на некоторые из этих инструментов, чтобы помочь.