C# – Propriétés ou méthodes

Lors d'un premier contact avec la programmation orientée objet (POO) en C#, l'opportunité d'offrir des services sous forme de propriétés ou sous forme de méthodes confond parfois les gens. Ce qui suit offre un bref survol syntaxique des deux approches, et propose des cas pour lesquels il serait préférable d'avoir recours à l'une ou à l'autre.

Objets et services

Prenons tout d'abord un cas très simple, soit celui d'un point sur le plan cartésien :

class Point
{
   const float X_DÉFAUT = 0.0f,
               Y_DÉFAUT = 0.0f;
   //
   // Attributs d'instance
   //
   float x, y;
   //
   // Méthodes (accesseurs pour x et y)
   //
   public float GetX()
   {
      return x;
   }
   public float GetY()
   {
      return y;
   }
   //
   // Constructeur par défaut
   //
   public Point()
   {
      x = X_DÉFAUT;
      y = Y_DÉFAUT;
   }
   //
   // Constructeur paramétrique
   //
   public Point(float valX, float valY)
   {
      x = valX;
      y = valY;
   }
}

Telle que proposée, cette classe décrit des objets immuables, donc qui ne changent plus une fois qu'ils ont été construits. Nous nous en tiendrons à de tels objets pour le moment, dans l'optique de simplifier le propos. Nous constatons ceci à partir des signes suivants :

À l'utilisation, un tel Point pourrait se manipuler comme suit :

// ...
static Point Déplacer(Point pt, float dx, float dy)
{
   return new Point(pt.GetX() + dx, pt.GetY() + dy);
}
static void Main(string [] args)
{
   Point pt = new Point();
   Console.WriteLine("Point à l'origine: ({0} , {1})", pt.GetX(), pt.GetY());
   //
   // Glisser pt de (1.5,-1.5)
   //
   pt = Déplacer(1.5f, -1.5f);
   Console.WriteLine("Point après déplacement: ({0} , {1})", pt.GetX(), pt.GetY());
}
// ...

Remarquez l'utilisation des services de pt : pour accéder à une copie de pt.x (attribut privé, donc inaccessible de prime abord sans passer par les services d'un Point), nous appelons pt.GetX(). On dit que GetX() dans ce cas-ci qu'il s'agit d'une méthode de pt. Puisque pt est une instance de Point, et puisque GetX() n'est pas qualifiée static, GetX() est une méthode d'instance de pt.

Les méthodes constituent la voie habituelle pour qu'un objet offre des services; tous les langages orientés objets commerciaux (C#, Java, C++, etc.) offrent ce mécanisme.

Propriétés

En fait, les propriétés jouent aussi un rôle structurel mineur, mais ce n'est pas le coeur de notre propos ici.

Les propriétés sont pour l'essentiel ce qu'on nomme du « sucre syntaxique », soit une facilité du langage pour que certaines opérations communes de programmation soient plus agréables ou plus conviviales. Il est possible de programmer sans elles, comme en fait foi leur absence dans beaucoup de langages connus. Cependant, dans les langages où elles existent, par exemple C# ou Delphi, les gens en tirent profit, et elles deviennent vite idiomatiques, ce qui explique que nous les présentions ici.

Une classe Point essentiellement identique à celle vue précédemment, du moins pour ce qui est de sa structure, mais exposant des propriétés plutôt que des méthodes, pourrait s'écrire comme suit :

class Point
{
   const float X_DÉFAUT = 0.0f,
               Y_DÉFAUT = 0.0f;
   //
   // Attributs d'instance
   //
   float x, y;
   //
   // Propriétés (accesseurs pour x et y)
   //
   public float X
   {
      get { return x; }
   }
   public float Y
   {
      get { return y; }
   }
   //
   // Constructeur par défaut
   //
   public Point()
   {
      x = X_DÉFAUT;
      y = Y_DÉFAUT;
   }
   //
   // Constructeur paramétrique
   //
   public Point(float valX, float valY)
   {
      x = valX;
      y = valY;
   }
}

À l'utilisation, nous aurions alors ceci :

// ...
static Point Déplacer(Point pt, float dx, float dy)
{
   return new Point(pt.X + dx, pt.Y + dy);
}
static void Main(string [] args)
{
   Point pt = new Point();
   Console.WriteLine("Point à l'origine: ({0} , {1})", pt.X, pt.Y);
   //
   // Glisser pt de (1.5,-1.5)
   //
   pt = Déplacer(1.5f, -1.5f);
   Console.WriteLine("Point après déplacement: ({0} , {1})", pt.X, pt.Y);
}
// ...

Comme vous pouvez le constater, rien de fondamental n'a changé, mais l'écriture pt.X est plus légère que l'écriture pt.GetX().

Propriétés automatiques

Dans le cas décrit jusqu'ici, il n'est pas possible de modifier les valeurs des attributs x et y d'un Point, et toutes les valeurs possibles sont acceptables pour ces deux attributs. C# permet dans un tel cas d'automatiser la génération des états et leur correspondance avec des propriétés.

Le code de la classe Point profitant de ce mécanisme irait comme suit :

class Point
{
   const float X_DÉFAUT = 0.0f,
               Y_DÉFAUT = 0.0f;
   //
   // Propriétés automatiques (modifiables à l'interne seulement)
   //
   public float X
   {
      get; private set;
   }
   public float Y
   {
      get; private set;
   }
   //
   // Constructeur par défaut
   //
   public Point()
   {
      X = X_DÉFAUT;
      Y = Y_DÉFAUT;
   }
   //
   // Constructeur paramétrique
   //
   public Point(float valX, float valY)
   {
      X = valX;
      Y = valY;
   }
}

Remarquez la disparition des attributs explicites x et y, même dans les constructeurs. Le code client, lui, ne changerait en rien.

Il est important à ce stade d'indiquer qu'une même classe peut avoir à la fois des méthodes et des propriétés. En C#, il est d'usage d'utiliser une propriété lorsqu'un service ressemble, à l'usage, à un état. Ainsi, une instance d'une hypothétique classe Rectangle aurait typiquement des propriétés Largeur, Hauteur, Surface et Périmètre plutôt que des méthodes GetLargeur(), GetHauteur(), GetSurface() et GetPérimètre().

En retour, pour certains services plus complexes ou qui ne correspondent pas en surface à un état, par exemple Déplacer(dx,dy), l'usage est d'avoir recours à une méthode. C'est d'abord et avant tout une question d'esthétique.

Mutation d'états

Les exemples présentés ci-dessus utilisent tous des objets immuables. Qu'en est-il d'objets modifiables, pour lesquels les états peuvent changer suite à la construction?

Avec une méthode, les paramètres utilisés comme valeurs sources pour l'attribut à modifier sont explicitement nommés dans la méthode, et sont utilisés comme dans n'importe quelle autre méthode. Pour les propriétés, la supposition est que le code client modifiera la valeur représentée par une affectation, et donc que la mutation de l'état de l'attribut reposera sur un seul paramètre, identifié par le nom implicite value du type de la propriété. Chose amusante toutefois : le compilateur génèrera automatiquement l'ensemble des opérations permettant de modifier les états de la valeur représentée par la propriété, incluant les opérateurs =, ++, +=, etc.

Les exemples ci-dessous montrent une classe Point, dont les instances sont modifiables cette fois, implémentée à partir de méthodes (à gauche) et à partir de propriétés (à droite), pour fins de comparaison. Les deux versions sont correctes et opérationnelles; on choisira celle qui nous sied le mieux en fonction des critères de notre entreprise. Nous la déclinerons de plusieurs manières :

Dans la version avec validation, le recours aux attributs est nécessaire même pour les propriétés.

Mutation d'états sans validation

Version avec méthodes Version avec propriétés
class Point
{
   const float X_DÉFAUT = 0.0f,
               Y_DÉFAUT = 0.0f;
   //
   // Attributs d'instance
   //
   float x, y;
   //
   // Méthodes (accesseurs pour x et y)
   //
   public float GetX()
   {
      return x;
   }
   public void SetX(float value)
   {
      x = value;
   }
   public float GetY()
   {
      return y;
   }
   public void SetY(float value)
   {
      y = value;
   }
   //
   // Constructeur par défaut
   //
   public Point()
   {
      x = X_DÉFAUT;
      y = Y_DÉFAUT;
   }
   //
   // Constructeur paramétrique
   //
   public Point(float valX, float valY)
   {
      x = valX;
      y = valY;
   }
   //
   // Autres services
   //
   void Déplacer(float dx, float dy)
   {
      SetX(GetX() + dx);
      SetY(GetY() + dy);
   }
}
// ...
static void Main(string [] args)
{
   Point pt = new Point(3.1f,5.2f);
   pt.Déplacer(2.0f, -1.0f);
   Console.Write("({0},{1})", pt.GetX(), pt.GetY());
}
class Point
{
   const float X_DÉFAUT = 0.0f,
               Y_DÉFAUT = 0.0f;
   //
   // Attributs d'instance
   //
   float x, y;
   //
   // Propriétés (accesseurs pour x et y)
   //
   public float X
   {
      get { return x; }
      set { x = value; }
   }
   public float Y
   {
      get { return y; }
      set { y = value; }
   }
   //
   // Constructeur par défaut
   //
   public Point()
   {
      X = X_DÉFAUT;
      Y = Y_DÉFAUT;
   }
   //
   // Constructeur paramétrique
   //
   public Point(float valX, float valY)
   {
      X = valX;
      Y = valY;
   }
   //
   // Autres services
   //
   void Déplacer(float dx, float dy)
   {
      X += dx;
      Y += dy;
   }
}
// ...
static void Main(string [] args)
{
   Point pt = new Point(3.1f,5.2f);
   pt.Déplacer(2.0f, -1.0f);
   Console.Write("({0},{1})", pt.X, pt.Y);
}

Mutation d'états sans validation (propriétés automatiques)

Version avec méthodes Version avec propriétés
class Point
{
   const float X_DÉFAUT = 0.0f,
               Y_DÉFAUT = 0.0f;
   //
   // Attributs d'instance
   //
   float x, y;
   //
   // Méthodes (accesseurs pour x et y)
   //
   public float GetX()
   {
      return x;
   }
   public void SetX(float value)
   {
      x = value;
   }
   public float GetY()
   {
      return y;
   }
   public void SetY(float value)
   {
      y = value;
   }
   //
   // Constructeur par défaut
   //
   public Point()
   {
      x = X_DÉFAUT;
      y = Y_DÉFAUT;
   }
   //
   // Constructeur paramétrique
   //
   public Point(float valX, float valY)
   {
      x = valX;
      y = valY;
   }
   //
   // Autres services
   //
   void Déplacer(float dx, float dy)
   {
      SetX(GetX() + dx);
      SetY(GetY() + dy);
   }
}
// ...
static void Main(string [] args)
{
   Point pt = new Point(3.1f,5.2f);
   pt.Déplacer(2.0f, -1.0f);
   Console.Write("({0},{1})", pt.GetX(), pt.GetY());
}
class Point
{
   const float X_DÉFAUT = 0.0f,
               Y_DÉFAUT = 0.0f;
   //
   // Propriétés (accesseurs pour x et y)
   //
   public float X
   {
      get; set;
   }
   public float Y
   {
      get; set;
   }
   //
   // Constructeur par défaut
   //
   public Point()
   {
      X = X_DÉFAUT;
      Y = Y_DÉFAUT;
   }
   //
   // Constructeur paramétrique
   //
   public Point(float x, float y)
   {
      X = x;
      Y = y;
   }
}   //
   // Autres services
   //
   void Déplacer(float dx, float dy)
   {
      X += dx;
      Y += dy;
   }

// ...
static void Main(string [] args)
{
   Point pt = new Point(3.1f,5.2f);
   pt.Déplacer(2.0f, -1.0f);
   Console.Write("({0},{1})", pt.X, pt.Y);
}

Mutation d'états avec validation

La validation que nous appliquerons ici sera d'obliger le point à se situer dans le premier quadrant du plan cartésien. Pour simplifier la chose, nous accepterons une abcisse ou une ordonnée nulle.

Version avec méthodes Version avec propriétés
class CoordonnéeInvalideException
   : ApplicationException
{
}
class Point
{
   const float X_DÉFAUT = 0.0f,
               Y_DÉFAUT = 0.0f;
   //
   // Validation d'une coordonnée
   //
   static vool EstCoordonnéeValide(float coord)
   {
      return coord >= 0;
   }
   //
   // Attributs d'instance
   //
   float x, y;
   //
   // Méthodes (accesseurs pour x et y)
   //
   public float GetX()
   {
      return x;
   }
   public void SetX(float value)
   {
      if (!EstCoordonnéeValide(value))
         throw new CoordonnéeInvalideException();
      x = value;
   }
   public float GetY()
   {
      return y;
   }
   public void SetY(float value)
   {
      if (!EstCoordonnéeValide(value))
         throw new CoordonnéeInvalideException();
      y = value;
   }
   //
   // Constructeur par défaut
   //
   public Point()
   {
      x = X_DÉFAUT;
      y = Y_DÉFAUT;
   }
   //
   // Constructeur paramétrique
   //
   public Point(float valX, float valY)
   {
      x = valX;
      y = valY;
   }
   //
   // Autres services
   //
   void Déplacer(float dx, float dy)
   {
      SetX(GetX() + dx);
      SetY(GetY() + dy);
   }
}
// ...
static void Main(string [] args)
{
   Point pt = new Point(3.1f,5.2f);
   pt.Déplacer(2.0f, -1.0f);
   Console.Write("({0},{1})", pt.GetX(), pt.GetY());
}
class CoordonnéeInvalideException
   : ApplicationException
{
}
class Point
{
   const float X_DÉFAUT = 0.0f,
               Y_DÉFAUT = 0.0f;
   //
   // Validation d'une coordonnée
   //
   static vool EstCoordonnéeValide(float coord)
   {
      return coord >= 0;
   }
   //
   // Attributs d'instance
   //
   float x, y;
   //
   // Propriétés (accesseurs pour x et y)
   //
   public float X
   {
      get { return x; }
      set
      {
         if (!EstCoordonnéeValide(value))
            throw new CoordonnéeInvalideException();
         x = value;
      }
   }
   public float Y
   {
      get { return y; }
      set
      {
         if (!EstCoordonnéeValide(value))
            throw new CoordonnéeInvalideException();
         y = value;
      }
   }
   //
   // Constructeur par défaut
   //
   public Point()
   {
      x = X_DÉFAUT;
      y = Y_DÉFAUT;
   }
   //
   // Constructeur paramétrique
   //
   public Point(float valX, float valY)
   {
      X = valX;
      Y = valY;
   }
   //
   // Autres services
   //
   void Déplacer(float dx, float dy)
   {
      X += dx;
      Y += dy;
   }
}
// ...
static void Main(string [] args)
{
   Point pt = new Point(3.1f,5.2f);
   pt.Déplacer(2.0f, -1.0f);
   Console.Write("({0},{1})", pt.X, pt.Y);
}

Propriétés calculées

Les exemples examinés jusqu'ici ont tous en commun de n'avoir que des accesseurs banals, qui se limitent à retourner un état connu a priori. Il est bien sûr possible d'implémenter des propriétés et des méthodes plus riches que cela. Par exemple, examinez la classe Rectangle ci-dessous, et portez attention aux méthodes GetSurface() et GetPérimètre() tout comme aux propriétés Surface et Périmètre.

Version avec méthodes Version avec propriétés
class DimensionInvalideException
   : ApplicationException
{
}
class Rectangle
{
   //
   // Attributs d'instance
   //
   int largeur,
       hauteur;
   //
   // Services de validation des états
   //
   static bool EstHauteurValide(int value)
   {
      return value > 0;
   }
   static bool EstLargeurValide(int value)
   {
      return value > 0;
   }
   //
   // Accesseurs
   //
   public int GetLargeur()
   {
      return largeur;
   }
   private void SetLargeur(int value)
   {
      if (!EstLargeurValide(value))
         throw new DimensionInvalideException();
      largeur = value;
   }
   public int GetHauteur()
   {
      return hauteur;
   }
   private void SetHauteur(int value)
   {
      if (!EstHauteurValide(value))
         throw new DimensionInvalideException();
      hauteur = value;
   }
   public int GetPérimètre()
   {
      return GetLargeur() * 2 + GetHauteur() * 2;
   }
   public int GetSurface()
   {
      return GetLargeur() * GetHauteur();
   }
   //
   // Constructeur paramétrique
   //
   public Rectangle(int valLargeur, int valHauteur)
   {
      SetLargeur(valLargeur);
      SetHauteur(valHauteur);
   }
   //
   // etc.
   //
}
// ...
static void Main(string [] args)
{
   Rectangle r = new Rectangle(3,5);
   Console.WriteLine("Surface: {0}", r.GetSurface());
}
class DimensionInvalideException
   : ApplicationException
{
}
class Rectangle
{
   //
   // Attributs d'instance
   //
   int largeur,
       hauteur;
   //
   // Services de validation des états
   //
   static bool EstHauteurValide(int value)
   {
      return value > 0;
   }
   static bool EstLargeurValide(int value)
   {
      return value > 0;
   }
   //
   // Accesseurs
   //
   public int Largeur
   {
      get { return largeur; }
      private set
      {
         if (!EstLargeurValide(value))
            throw new DimensionInvalideException();
         largeur = value;
      }
   }
   public int Hauteur
   {
      get { return hauteur; }
      private set
      {
         if (!EstHauteurValide(value))
            throw new DimensionInvalideException();
         hauteur = value;
      }
   }
   public int Périmètre
   {
      get { return Largeur * 2 + Hauteur * 2; }
   }
   public int Surface
   {
      get { return Largeur * Hauteur; }
   }
   //
   // Constructeur paramétrique
   //
   public Rectangle(int valLargeur, int valHauteur)
   {
      Largeur = valLargeur;
      Hauteur = valHauteur;
   }
   //
   // etc.
   //
}
// ...
static void Main(string [] args)
{
   Rectangle r = new Rectangle(3,5);
   Console.WriteLine("Surface: {0}", r.Surface);
}

Mentionnons pour conclure que, bien que j'aie placé la validation des modifications des attributs dans les mutateurs (méthodes Set... et propriétés set), suivant ainsi le modèle classique, il aurait été plus efficace ici de valider les tentatives de mutation des attributs à un niveau plus élevé : en effet, les mutateurs de cette classe sont privés, donc ne sont accessibles qu'à travers des appels générés par Rectangle, qui est un « client » pour le moins digne de confiance.

De plus, cette classe Rectangle est immuable (les états de ses instances ne changent pas une fois la construction complétée), donc le seul point de validation requis serait à la construction. Cela dit, ce code est correct et présente les similitudes et différences syntaxiques entre méthodes et propriétés, incluant des propriétés dont la partie get n'est pas banale.

Lectures complémentaires

Quelques liens pour en savoir plus.


Valid XHTML 1.0 Transitional

CSS Valide !