wake-up-neo.net

Wie würden Sie ein "Merkmal" -Designmuster in C # implementieren?

Ich weiß, dass das Feature in C # nicht existiert, aber PHP hat kürzlich ein Feature namens Traits hinzugefügt, von dem ich dachte, es sei zunächst ein bisschen dumm, bis ich darüber nachdachte.

Angenommen, ich habe eine Basisklasse namens Client. Client hat eine einzige Eigenschaft namens Name.

Jetzt entwickle ich eine wiederverwendbare Anwendung, die von vielen verschiedenen Kunden verwendet wird. Alle Kunden sind sich einig, dass ein Kunde einen Namen haben sollte, daher in der Basisklasse.

Nun kommt Kunde A und sagt, er müsse auch das Gewicht des Kunden ermitteln. Kunde B braucht das Gewicht nicht, aber er möchte die Höhe verfolgen. Kunde C möchte sowohl Gewicht als auch Höhe verfolgen.

Mit Merkmalen könnten wir die Merkmale "Gewicht" und "Höhe" festlegen:

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

Jetzt kann ich alle Bedürfnisse meiner Kunden erfüllen, ohne der Klasse zusätzliche Flusen hinzuzufügen. Wenn mein Kunde später zurückkommt und sagt "Oh, ich mag diese Funktion wirklich, kann ich sie auch haben?", Aktualisiere ich nur die Klassendefinition, um das zusätzliche Merkmal einzubeziehen.

Wie würdest du das in C # erreichen?

Schnittstellen funktionieren hier nicht, da ich konkrete Definitionen für die Eigenschaften und die zugehörigen Methoden möchte und nicht für jede Version der Klasse neu implementieren möchte.

(Mit "Kunde" meine ich eine wörtliche Person, die mich als Entwickler angestellt hat, während mit "Kunde" eine Programmierklasse gemeint ist; jeder meiner Kunden hat Kunden, über die er Informationen aufzeichnen möchte.)

43
mpen

Sie können die Syntax mithilfe von Markerschnittstellen und Erweiterungsmethoden ermitteln.

Voraussetzung: Die Schnittstellen müssen den Vertrag definieren, der später von der Erweiterungsmethode verwendet wird. Grundsätzlich definiert die Schnittstelle den Vertrag, um ein Merkmal "implementieren" zu können; Idealerweise sollten in der Klasse, in der Sie die Schnittstelle hinzufügen, bereits alle Mitglieder der Schnittstelle vorhanden sein, sodass eine zusätzliche Implementierung von no erforderlich ist.

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
}

Verwenden Sie wie folgt:

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

Edit: Es wurde die Frage gestellt, wie zusätzliche Daten gespeichert werden können. Dies kann auch durch zusätzliche Codierung behoben werden:

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

Und dann können die Trait-Methoden Daten hinzufügen und abrufen, wenn die "Trait-Schnittstelle" von IDynamicObject erbt:

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
}

Hinweis: Durch die Implementierung von IDynamicMetaObjectProvider würde das Objekt sogar die Offenlegung der dynamischen Daten über den DLR ermöglichen, wodurch der Zugriff auf die zusätzlichen Eigenschaften bei Verwendung des Schlüsselworts dynamic transparent wird.

46
Lucero

C # Sprache (mindestens bis Version 5) unterstützt keine Traits.

Allerdings hat Scala Eigenschaften und Scala wird auf der JVM (und CLR) ausgeführt. Es geht also nicht um die Laufzeit, sondern nur um die Sprache.

Bedenken Sie, dass Eigenschaften, zumindest im Sinne vonScala, als "ziemlich magisch zum Kompilieren in Proxy-Methoden" angesehen werden können (sie haben nicht Einfluss auf den MRO, der sich von unterscheidet Mixins in Ruby). In C # wäre der Weg, dieses Verhalten zu erreichen, die Verwendung von Schnittstellen und "vielen manuellen Proxy-Methoden" (z. B. Komposition).

Dieser mühsame Prozess könnte mit einem hypothetischen Prozessor durchgeführt werden (möglicherweise automatische Codegenerierung für eine Teilklasse über Vorlagen?), Aber das ist nicht C #.

Fröhliches Codieren.

8
user166390

Ich möchte auf NRoles verweisen, ein Experiment mit Rollen in C #, wobei RollenEigenschaften ähnlich sind.

NRoles verwendet einen Post-Compiler, um die AWL neu zu schreiben und die Methoden in eine Klasse einzufügen. So können Sie Code wie folgt schreiben:

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

dabei implementiert die Klasse RadioRSwitchable und RTunable. Hinter den Kulissen ist Does<R> eine Schnittstelle ohne Mitglieder. Daher wird Radio im Wesentlichen in eine leere Klasse übersetzt. Das IL-Umschreiben nach der Kompilierung fügt die Methoden von RSwitchable und RTunable in Radio ein, die dann verwendet werden können, als ob sie wirklich von den beiden Rollen (von einer anderen Assembly) abgeleitet wären:

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

Um radio direkt vor dem erneuten Schreiben zu verwenden (d. H. In derselben Assembly, in der der Radio-Typ deklariert ist), müssen Sie auf die Erweiterungsmethoden As<R> () zurückgreifen:

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

da der Compiler nicht erlauben würde, TurnOn oder Seek direkt in der Radio-Klasse aufzurufen.

5
Pierre Arnaud

Es gibt ein wissenschaftliches Projekt, das von Stefan Reichart von der Software Composition Group der Universität Bern (Schweiz) entwickelt wurde und eine echte Umsetzung von eigenschaften der C # -Sprache bietet.

In dem Artikel (PDF) auf CSharpT finden Sie eine vollständige Beschreibung dessen, was er getan hat, basierend auf dem Mono-Compiler.

Hier ist ein Beispiel, was geschrieben werden kann:

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

trait TColor { ... }

class MyCircle
{
    uses { TCircle; TColor }
}
5
Pierre Arnaud

Dies ist wirklich eine empfohlene Erweiterung der Antwort von Lucero, bei der der gesamte Speicher in der Basisklasse war.

Wie wäre es mit Abhängigkeitseigenschaften dafür?

Dies hat zur Folge, dass die Clientklassen zur Laufzeit leichter werden, wenn Sie über viele Eigenschaften verfügen, die nicht immer von allen Nachkommen festgelegt werden. Dies liegt daran, dass die Werte in einem statischen Member gespeichert werden.

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

Aufbauend auf was Lucero vorschlug kam ich dazu:

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

Ausgabe:

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

Es ist nicht ganz so schön wie ich möchte, aber es ist auch nicht so schlimm.

2
mpen

Traits können in C # 8 mit Standardschnittstellenmethoden implementiert werden. Java 8 führte auch aus diesem Grund Standardschnittstellenmethoden ein. 

Mit C # 8 können Sie fast genau das schreiben, was Sie in der Frage vorgeschlagen haben. Die Merkmale werden von den Schnittstellen IClientWeight und IClientHeight implementiert, die eine Standardimplementierung für ihre Methoden bereitstellen. In diesem Fall geben sie nur 0 zurück:

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

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

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

ClientA und ClientB haben die Eigenschaften, implementieren sie jedoch nicht. ClientC implementiert nur IClientHeight und gibt eine andere Nummer zurück, in diesem Fall 16:

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

Wenn getHeight() in ClientB über die Schnittstelle aufgerufen wird, wird die Standardimplementierung aufgerufen. getHeight() kann nur über die Schnittstelle aufgerufen werden.

ClientC implementiert die IClientHeight-Schnittstelle, sodass eine eigene Methode aufgerufen wird. Die Methode ist über die Klasse selbst verfügbar.

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

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

Dieses SharpLab.io-Beispiel zeigt den aus diesem Beispiel erzeugten Code

Viele der Eigenschaftenmerkmale, die in der PHP - Übersicht über Merkmale beschrieben werden, können problemlos mit Standardschnittstellenmethoden implementiert werden. Eigenschaften (Schnittstellen) können kombiniert werden. Es ist auch möglich, abstract -Methoden zu definieren, um Klassen zu zwingen, bestimmte Anforderungen zu implementieren.

Nehmen wir an, wir möchten, dass unsere Eigenschaften sayHeight() und sayWeight() Methoden haben, die einen String mit der Höhe oder dem Gewicht zurückgeben. Sie würden eine Möglichkeit benötigen, um ausstellende Klassen (Begriff aus dem PHP Guide) zu zwingen, eine Methode zu implementieren, die die Höhe und das Gewicht zurückgibt:

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

Die Clients haben jetzt haben, um die getHeight()- oder getWeight()-Methode zu implementieren, müssen jedoch nichts über die say-Methoden wissen. 

Dies bietet eine sauberere Möglichkeit zum Dekorieren 

SharpLab.io-Link für dieses Beispiel.

2

Das klingt wie PHP-Version von Aspect Oriented Programming. In einigen Fällen gibt es Hilfsmittel wie PostSharp oder MS Unity. Wenn Sie einen eigenen Roll-Roll ausführen möchten, ist die Code-Injektion mit C # -Attributen ein Ansatz oder als Erweiterungsverfahren für begrenzte Fälle. 

Kommt drauf an, wie kompliziert du werden willst. Wenn Sie versuchen, etwas Komplexes zu erstellen, würde ich mir einige dieser Tools ansehen, die Ihnen helfen.

0
RJ Lohan