web-dev-qa-db-fra.com

Comment implémenteriez-vous un motif de trait "trait" en C #?

Je sais que la fonctionnalité n'existe pas en C #, mais PHP a récemment ajouté une fonctionnalité appelée Traits qui, à mon avis, était un peu ridicule jusqu'à ce que je commence à y penser.

Disons que j'ai une classe de base appelée Client. Client a une propriété unique appelée Name.

Je développe maintenant une application réutilisable qui sera utilisée par de nombreux clients. Tous les clients conviennent qu'un client doit avoir un nom, ce qui le place dans la classe de base.

Maintenant, le client A arrive et dit qu'il doit également suivre le poids du client. Le client B n'a pas besoin du poids, mais il souhaite suivre la hauteur. Le client C veut suivre à la fois le poids et la taille.

Avec les traits, nous pourrions faire les traits de poids et de taille:

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

Maintenant, je peux répondre à tous les besoins de mes clients sans ajouter de peluche à la classe. Si mon client revient plus tard et dit "Oh, j'aime vraiment cette fonctionnalité, puis-je l'avoir aussi?", Je mets à jour la définition de la classe pour inclure le trait supplémentaire.

Comment voulez-vous accomplir cela en C #?

Les interfaces ne fonctionnent pas ici car je veux des définitions concrètes pour les propriétés et les méthodes associées, et je ne veux pas les ré-implémenter pour chaque version de la classe.

(Par "client", j'entends une personne littérale qui m'a employé en tant que développeur, alors que par "client", je parle d'une classe de programmation; chacun de mes clients a des clients sur lesquels ils souhaitent enregistrer des informations)

43
mpen

Vous pouvez obtenir la syntaxe en utilisant des interfaces de marqueur et des méthodes d'extension.

Condition préalable: les interfaces doivent définir le contrat qui sera utilisé ultérieurement par la méthode d'extension. Fondamentalement, l'interface définit le contrat permettant de "mettre en oeuvre" un trait; idéalement, la classe dans laquelle vous ajoutez l'interface doit déjà avoir tous les membres de l'interface présents, de sorte que no une implémentation supplémentaire est requise.

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
}

Utilisez comme ceci:

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

Edit: La question a été posée de savoir comment stocker des données supplémentaires. Ceci peut également être résolu en faisant un codage supplémentaire:

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

Et ensuite, les méthodes de trait peuvent ajouter et récupérer des données si "l'interface de trait" hérite de 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
}

Remarque: en implémentant IDynamicMetaObjectProvider , l'objet permettrait même d'exposer les données dynamiques par le biais du DLR, rendant l'accès aux propriétés supplémentaires transparent si utilisé avec le mot clé dynamic.

46
Lucero

C # language (au moins jusqu'à la version 5) ne prend pas en charge les traits.

Cependant, Scala a Traits et Scala s'exécute sur la JVM (et CLR). Par conséquent, ce n'est pas une question de temps d'exécution, mais simplement de langage.

Considérez que les traits, du moins au sens de Scala, peuvent être considérés comme "assez magiques à compiler dans les méthodes proxy" (ils affectent le {pas} _ le MRO, qui est différent de Mixins en Ruby). En C #, la façon d’obtenir ce comportement serait d’utiliser des interfaces et «de nombreuses méthodes proxy manuelles» (par exemple, la composition).

Ce processus fastidieux pourrait être effectué avec un processeur hypothétique (peut-être une génération de code automatique pour une classe partielle via des modèles?), Mais ce n'est pas du C #.

Bonne codage.

8
user166390

Je voudrais signaler NRoles , une expérience avec roles en C #, où roles sont similaires à traits.

NRoles utilise un post-compilateur pour réécrire l'IL et injecter les méthodes dans une classe. Cela vous permet d'écrire du code comme ça:

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

où la classe Radio implémente RSwitchable et RTunable. Dans les coulisses, Does<R> est une interface sans membres, donc Radio est compilé dans une classe vide. La réécriture d’IL post-compilation injecte les méthodes RSwitchable et RTunable dans Radio, qui peuvent ensuite être utilisées comme si elle dérivait réellement des deux roles (d’un autre assembly):

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

Pour utiliser radio directement avant la réécriture (c'est-à-dire, dans le même assembly que celui où le type Radio est déclaré), vous devez recourir aux méthodes d'extensions As<R> ():

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

puisque le compilateur ne permettrait pas d'appeler TurnOn ou Seek directement sur la classe Radio.

5
Pierre Arnaud

Il existe un projet académique, développé par Stefan Reichart du groupe de composition de logiciels de l'Université de Berne (Suisse), qui fournit une véritable implémentation de traits en langage C #.

Jetez un coup d’œil à le papier (PDF) sur CSharpT pour une description complète de ce qu’il a fait, basé sur le compilateur mono.

Voici un exemple de ce qui peut être écrit:

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

trait TColor { ... }

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

Ceci est vraiment une extension suggérée de la réponse de Lucero où tout le stockage était dans la classe de base.

Pourquoi ne pas utiliser les propriétés de dépendance pour cela?

Cela aurait pour effet d'alléger les classes de client au moment de l'exécution, lorsque de nombreuses propriétés ne sont pas toujours définies par tous les descendants. Cela est dû au fait que les valeurs sont stockées dans un membre statique.

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

En me basant sur ce que Lucero a suggéré , j’ai trouvé ceci:

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

Sortie:

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

Ce n'est pas aussi gentil que je le souhaiterais, mais ce n'est pas trop mal non plus.

2
mpen

Les traits peuvent être implémentés dans C # 8 à l'aide des méthodes d'interface par défaut. Java 8 a également introduit les méthodes d'interface par défaut pour cette raison. 

En utilisant le C # 8, vous pouvez écrire presque exactement ce que vous avez proposé dans la question. Les traits sont implémentés par les interfaces IClientWeight, IClientHeight qui fournissent une implémentation par défaut pour leurs méthodes. Dans ce cas, ils ne font que renvoyer 0:

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

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

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

ClientA et ClientB possèdent les caractéristiques mais ne les mettent pas en œuvre. ClientC implémente uniquement IClientHeight et renvoie un numéro différent, dans ce cas 16:

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

Lorsque getHeight() est appelé dans ClientB via l'interface, l'implémentation par défaut est appelée. getHeight() ne peut être appelé que via l'interface.

ClientC implémente l'interface IClientHeight de sorte que sa propre méthode est appelée. La méthode est disponible via la classe elle-même.

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

Cet exemple SharpLab.io montre le code généré à partir de cet exemple

La plupart des caractéristiques décrites dans PHP - Vue d'ensemble sur les traits peuvent être facilement implémentées avec les méthodes d'interface par défaut. Les traits (interfaces) peuvent être combinés. Il est également possible de définir des méthodes abstract pour forcer les classes à implémenter certaines exigences.

Disons que nous voulons que nos traits aient des méthodes sayHeight() et sayWeight() qui renvoient une chaîne avec la taille ou le poids. Ils auraient besoin d’un moyen pour forcer les classes d’exposition (terme volé dans le guide PHP) à mettre en œuvre une méthode qui renvoie la taille et le poids:

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

Les clients doivent maintenant pour implémenter la méthode getHeight() ou getWeight() mais n'ont pas besoin de connaître les méthodes say

Cela offre une façon plus propre de décorer 

Lien SharpLab.io pour cet exemple.

2
Panagiotis Kanavos

Cela ressemble à la version PHP de la programmation orientée aspect. Il existe des outils pour aider comme PostSharp ou MS Unity dans certains cas. Si vous voulez vous débrouiller, l'injection de code à l'aide d'attributs C # est une approche ou une méthode d'extension suggérée pour des cas limités. 

Cela dépend vraiment de la complexité de votre travail. Si vous essayez de construire quelque chose de complexe, je regarderais certains de ces outils pour vous aider.

0
RJ Lohan