Jak zaimplementowałbyś "cechę" wzorca projektowego w C#?

Wiem, że ta funkcja nie istnieje w C#, ale PHP ostatnio dodał funkcję o nazwie Traits , która na początku była trochę głupia, dopóki nie zacząłem o niej myśleć.

Powiedzmy, że mam klasę bazową o nazwie Client. Client posiada jedną właściwość o nazwie Name.

Teraz opracowuję aplikację wielokrotnego użytku, która będzie używana przez wielu różnych klientów. Wszyscy klienci zgadzają się, że klient powinien mieć nazwę, stąd jest w klasie bazowej.

Teraz przychodzi klient A i mówi, że musi również śledzić wagę klienta. Klient B nie potrzebuje wagi, ale chce śledzić wzrost. Klient C chce śledzić zarówno wagę, jak i wzrost.

Z cechami, możemy sprawić, że zarówno Waga, jak i wzrost cechują cechy:

class ClientA extends Client use TClientWeight
class ClientB extends Client use TClientHeight
class ClientC extends Client use TClientWeight, TClientHeight
Teraz mogę zaspokoić wszystkie potrzeby moich klientów bez dodawania dodatkowego puchu do klasy. Jeśli mój klient wróci później i powie " Och, naprawdę podoba mi się Ta funkcja, Czy mogę ją mieć?", Właśnie aktualizuję definicję klasy do Dołącz dodatkową cechę.

Jak można to osiągnąć w C#?

Interfejsy nie działają tutaj, ponieważ chcę konkretnych definicji właściwości i wszelkich powiązanych metod i nie chcę ich ponownie implementować dla każdej wersji klasy.

(przez "Klienta" mam na myśli dosłowną osobę, która zatrudniła mnie jako programistę, podczas gdy przez "Klienta" mówię o klasie programowania; każdy z moich klientów ma klientów, o których chcą rejestrować informacje)

Author: mpen, 2012-05-24

7 answers

Można uzyskać składnię za pomocą interfejsów znaczników i metod rozszerzeń.

Warunek konieczny: interfejsy muszą zdefiniować kontrakt, który jest później używany przez metodę rozszerzenia. Zasadniczo interfejs definiuje kontrakt na możliwość "zaimplementowania" cechy; najlepiej, aby klasa, w której dodajesz interfejs, miała już wszystkich członków interfejsu, tak że nie jest wymagana dodatkowa implementacja.

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
}

Użyj jak to:

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

Edit: pojawiło się pytanie, w jaki sposób można przechowywać dodatkowe dane. Można to również rozwiązać, wykonując dodatkowe kodowanie:

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

I wtedy metody trait mogą dodawać i pobierać dane, jeśli "interfejs trait" dziedziczy z 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
}

Uwaga: realizując IDynamicMetaObjectProvider obiekt pozwalałby nawet na ujawnienie dynamicznych danych przez DLR, dzięki czemu dostęp do dodatkowych właściwości byłby przezroczysty, gdy jest używany z dynamic słowo kluczowe.

 43
Author: Lucero,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-10-05 22:55:14

C# Język (przynajmniej do wersji 5) nie ma wsparcia dla cech.

Jednak Scala ma cechy i Scala działa na JVM (i CLR). Dlatego nie jest to kwestia czasu pracy, ale po prostu języka.

Rozważ, że cechy, przynajmniej w sensie Scali, mogą być uważane za "dość magiczne do kompilacji w metodach proxy" (nie wpływają one , a nie na MRO, które różni się od Mixinów w Ruby). W C# sposobem na uzyskanie takiego zachowania byłoby użycie interfejsy i " wiele ręcznych metod proxy "(np. skład).

Ten żmudny proces można by wykonać za pomocą hipotetycznego procesora (może automatyczne generowanie kodu dla częściowej klasy za pomocą szablonów?), ale to nie C#.

Szczęśliwe kodowanie.

 8
Author: ,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-05-23 23:45:19

Istnieje projekt akademicki, opracowany przez Stefana Reicharta z Software Composition Group na Uniwersytecie w Bernie (Szwajcaria), który zapewnia prawdziwą implementację cech do języka C#.

[1]}Spójrz na artykuł (PDF) na CSharpT aby uzyskać pełny opis tego, co zrobił, oparty na kompilatorze mono.

Oto przykład tego, co można napisać:

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

trait TColor { ... }

class MyCircle
{
    uses { TCircle; TColor }
}
 6
Author: Pierre Arnaud,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-02 09:14:23

Chciałbym wskazać NRoles , eksperyment z rolami W C#, gdzie role są podobne do cechy.

Nroles używa post-kompilatora do przepisywania IL i wprowadzania metod do klasy. Pozwala to na pisanie kodu w ten sposób:

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> { }

Gdzie klasa Radio implementuje RSwitchable i RTunable. Za kulisami, Does<R> jest interfejsem bez członków, więc zasadniczo Radio kompiluje się do pustej klasy. The post-compilation IL przepisywanie wtryskuje metody RSwitchable i RTunable do Radio, które mogą być następnie użyte tak, jakby naprawdę wywodziły się z dwóch ról (z innego zgromadzenia):

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

Aby użyć radio bezpośrednio przed przepisaniem (czyli w tym samym zestawie, w którym deklarowany jest typ Radio), musisz użyć metod rozszerzeń As<R>():

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

Ponieważ kompilator nie pozwala na wywołanie TurnOn lub Seek bezpośrednio na klasie Radio.

 4
Author: Pierre Arnaud,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-02 09:09:22

Jest to naprawdę sugerowane rozszerzenie do odpowiedzi Lucero, gdzie cała pamięć była w klasie bazowej.

Może użyjesz do tego właściwości zależności?

Spowodowałoby to, że klasy klienckie byłyby lekkie w czasie uruchamiania, gdy masz wiele właściwości, które nie zawsze są ustawiane przez każdego potomka. Dzieje się tak, ponieważ wartości są przechowywane w statycznym elemencie.

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;
    }
}
 2
Author: weston,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-05-24 10:57:19

Bazując na co zasugerował Lucero , wpadłem na to:

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

Wyjście:

Adam is 149.9 lbs.
Bob is 68.9 inches tall.
Cheryl is 119.9 lbs and 65.0 inches.
Nie jest tak miło, jak bym chciała, ale też nie jest tak źle.
 2
Author: mpen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 11:33:16

To brzmi jak PHP w wersji Aspect Oriented Programming. Istnieją narzędzia do pomocy, takie jak PostSharp lub MS Unity w niektórych przypadkach. Jeśli chcesz, aby roll-your-own, code-injection przy użyciu atrybutów C# jest jednym z podejść, lub jak sugerowane metody rozszerzenia dla ograniczonych przypadków.

Naprawdę zależy, jak skomplikowane chcesz się dostać. Jeśli próbujesz zbudować coś złożonego, poszukam niektórych z tych narzędzi, które pomogą.

 0
Author: RJ Lohan,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-05-23 23:59:04