Jak zaimplementowałbyś wzorzec projektowy cechy w C#?

Jak zaimplementowałbyś wzorzec projektowy cechy w C#?

Możesz uzyskać składnię za pomocą interfejsów znaczników i metod rozszerzających.

Warunek wstępny:interfejsy muszą zdefiniować kontrakt, który jest później używany przez metodę rozszerzającą. Zasadniczo interfejs definiuje kontrakt umożliwiający „zaimplementowanie” cechy; w idealnym przypadku klasa, do której dodajesz interfejs, powinna już mieć wszystkich członków interfejsu, tak aby nie wymagana jest 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 w ten sposób:

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

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

Następnie metody cech mogą dodawać i pobierać dane, jeśli „interfejs cech” dziedziczy po 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:implementując IDynamicMetaObjectProvider jak również obiekt umożliwiłby nawet ujawnienie danych dynamicznych przez DLR, czyniąc dostęp do dodatkowych właściwości przezroczystym, gdy jest używany z dynamic słowo kluczowe.


Chciałbym wskazać na NRoles, eksperyment z rolami w C#, gdzie role są podobne do cech .

NRoles używa postkompilatora do przepisywania IL i wstrzykiwania metod do klasy. To pozwala 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 w zasadzie Radio kompiluje do pustej klasy. Przepisywanie IL po kompilacji wstrzykuje metody RSwitchable i RTunable w Radio , którego można następnie używać tak, jakby naprawdę pochodził z dwóch roli (z innego zestawu):

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

Aby użyć radio bezpośrednio przed przepisywaniem (to znaczy w tym samym asemblerze, w którym Radio typ jest zadeklarowany), musisz skorzystać z metod rozszerzających As<R> ():

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

ponieważ kompilator nie pozwoliłby na wywołanie TurnOn lub Seek bezpośrednio na Radio klasa.


Cechy można zaimplementować w C# 8 przy użyciu domyślnych metod interfejsu. Z tego powodu Java 8 wprowadziła domyślne metody interfejsu.

Używając C# 8, możesz napisać prawie dokładnie to, co zaproponowałeś w pytaniu. Cechy są implementowane przez interfejsy IClientWeight, IClientHeight, które zapewniają domyślną implementację ich metod. W takim przypadku zwracają po prostu 0:

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

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

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

ClientA i ClientB mieć cechy, ale ich nie wdrażać. ClientC implementuje tylko IClientHeight i zwraca inną liczbę, w tym przypadku 16 :

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

Kiedy getHeight() jest wywoływany w ClientB za pośrednictwem interfejsu wywoływana jest domyślna implementacja. getHeight() można wywołać tylko przez interfejs.

ClientC implementuje interfejs IClientHeight, więc wywoływana jest jego własna metoda. Metoda jest dostępna przez samą klasę.

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

Ten przykład SharpLab.io pokazuje kod utworzony z tego przykładu

Wiele cech cech opisanych w przeglądzie PHP na temat cech można łatwo zaimplementować za pomocą domyślnych metod interfejsu. Cechy (interfejsy) można łączyć. Możliwe jest również zdefiniowanie streszczenia metody wymuszające na klasach zaimplementowanie określonych wymagań.

Powiedzmy, że chcemy, aby nasze cechy miały sayHeight() i sayWeight() metody, które zwracają ciąg z wysokością lub wagą. Potrzebowaliby jakiegoś sposobu, aby wymusić wystawianie klas (termin skradziony z przewodnika PHP) do zaimplementowania metody zwracającej wzrost i wagę:

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

Klienci teraz mają zaimplementować je getHeight() lub getWeight() metody, ale nie musisz nic wiedzieć o say metody.

Zapewnia to czystszy sposób dekorowania

Link SharpLab.io do tej próbki.