POO C# - 3. Les propriétés
Après cette quête, tu pourras utiliser des propriétés plutôt que de déclarer des accesseurs
Objectifs
- Savoir définir des propriétés
- Savoir utiliser des propriétés
- Savoir les utiliser pour simplifier le code
Un accesseur simplifié
Lorsque tu écris une classe, tu caches tous ses attributs en leur assignant une visibilité privée. Afin qu'il soit possible de modifier un attribut privé, tu écris des accesseurs qui te permettent de contrôler les accès aux attributs en-dehors d'une classe dans laquelle ils sont définis.
Et ça fait beaucoup de code pour peu de fonctionnalités!
Entièrement d'accord ! Dès lors que tu déclares un attribut, tu dois déclarer des méthodes permettant d'accéder à l'attribut en question, ce qui prend du temps et alourdit le code contenu dans une classe. Figure-toi que tu n'es pas le seul à trouver lourde la mise en place de l'encapsulation.
Afin de simplifier la mise en place de l'encapsulation des attributs d'une classe, Microsoft a introduit dans le langage C# le concept de propriété. Les propriétés sont à utiliser de manière récurrente dès lors que tu dois encapsuler un attribut. Les propriétés permettent de supprimer de la complexité et des lignes de code.
Exemple :
Voici une classe représentant une personne dont les attributs sont correctement encapsulés en définissant des accesseurs en lecture et écriture pour les deux attributs de la classe.
class Person
{
private string _name;
private int _age;
public string GetName()
{
return _name;
}
public void SetName(string name)
{
_name = name;
}
public int GetAge()
{
return _age;
}
public void SetAge(int age)
{
_age = age;
}
}
Et voici la même classe dont l'encapsulation est correctement réalisée, mais en utilisant des propriétés :
class Person
{
public string Name { get; set; }
public int Age { get; set; }
}
On a gardé exactement le même comportement : Name et Age sont accessibles en lecture et écriture, mais en deux lignes.
Whaou, on a bien réduit le nombre de caractères dans le code ! Mais comment on définit une propriété, alors ? Et surtout, comment l’utilise t-on ?
Le mécanisme
Une propriété s'utilise comme un attribut, sauf qu'une logique spécifique est appliquée dès qu'une accession ou modification est réalisée sur une propriété.
Exemple :
Dans l'exemple de la classe Person, on a défini deux propriétés Name et Age. Voici un code qui manipule les propriétés de la classe Person :
class Program
{
public static Main(string[] args)
{
Person person = new Person();
// Set the value of a Person attributes
person.Name = "Robert";
person.Age = 50;
// Display the value of the attribute.
Console.WriteLine(person.Name + " is " + person.Age + " years old);
}
}
Le code ci-dessus va afficher Robert is 50 years old. On accède aux propriétés comme aux attributs, mais les propriétés ne se comportent pas comme les attributs. Dès lors qu'une propriété est affectée, l'accesseur set est invoqué et lors d'un accès, c'est l'accesseur get.
Mais avec ta technique, j'ouvre l'accès en lecture/écriture à toutes les propriétés ? Si je veux restreindre les accès, comment devrai-je m'y prendre ?
Contrôler la visibilité des accesseurs
La méthode "classique" (sans utiliser les propriétés) pour mettre en place l'encapsulation permet de définir de manière claire les possibilités d'accès pour chacun des attributs. Il est possible d'ouvrir un accès en lecture, mais pas en écriture, en écrivant les accesseurs associés.
public class Summer
{
private int _sum;
private IEnumerable<int> _summedCollection;
public void CalculateSum()
{
_sum = _summedCollection.Aggregate((x, y) => x + y);
}
public void SetSummedCollection(IEnumerable<int> summedCollection)
{
_summedCollection = summedCollection;
}
public int GetSum()
{
return _sum;
}
}
Voici un exemple de classe qui fait la somme d'un objet énumérable, c'est-à-dire n'importe quelle classe implémentant l'interface IEnumerable. La classe dispose de trois méthodes :
-
Une méthode pour calculer la somme du tableau (en utilisant Aggregate)
-
Un accesseur en écriture sur l'objet énumérable dont la somme est faite
-
Un accesseur en lecture sur la somme
CalculateSum calcule la somme des éléments d'un objet énumérable et la stocke dans l'attribut _sum. Il n'y a donc aucun besoin d'accéder en écriture à _sum, alors aucun accesseur en lecture n'est écrit pour l'attribut _sum.
La somme est calculée à partir d'un objet énumérable contenant des entiers, stocké dans l'attribut _summedCollection. On écrit donc un accesseur en écriture pour la collection dont on fait la somme, mais pas d'accesseur en lecture puisque nous n'en avons aucunement besoin.
Il est possible de réécrire cet exemple de classe correctement encapsulée en utilisant non plus les attributs, mais les propriétés.
public class Summer
{
public int Sum {
get => SummedCollection.Aggregate((x, y) => x + y);
}
public IEnumerable<int> SummedCollection { set; }
}
Instanciation et constructeur
Pour créer une nouvelle instance de classe, tu dois utiliser le constructeur. Pour ça, c'est le mot clé new dont tu as besoin.
Ainsi, ma classe Person qui n'a pas de constructeur explicite peut s'instancier et s'initialiser de cette façon :
Person p = new Person();
p.Name = "Robert";
p.Age = 42;
Une version raccourcie, et inspiré de ce que tu pourrais trouver en JavaScript serait :
Person P = new Person{
Name = "Robert",
Age = 42
};
Cool, avec ça, plus besoin de constructeur alors ?!
Eh bien... Oui et non. Cette syntaxe existe, elle présente des avantages indéniables, mais également quelques inconvénients.
Tout d'abord, il est nécessaire de connaitre les propriétés qui nécessitent d'être initialisé (je peux créer un new Person avec un Name, mais sans spécifier Age).
Ensuite, si l'une des propriété à un setter qui est private, tu ne pourras pas lui affecter de valeur avec cette syntaxe. Exemple
public class Person
{
public string Name { get; set; }
public int Age { get; private set; }
}
class Program
{
public static Main(string[] args)
{
Person person = new Person {
Name = "Robert",
Age = 42 // erreur, Age est inaccessible en écriture à cause de "private set;"
};
}
}
En revanche, un constructeur peut très bien utiliser le private set; :
public class Person
{
public string Name { get; set; }
public int Age { get; private set; }
public Person (string Name, int Age)
{
this.Name = Name;
this.Age = Age;
}
}
class Program
{
public static Main(string[] args)
{
Person person = new Person("Robert", 42);
}
}
Au final, tout dépend du besoin que tu as :
- Un constructeur sert à initialiser une classe avec toutes les informations nécessaires : si il y a 6 paramètres, il FAUT spécifier les 6 valeurs
- L'instanciation dynamique est plus souple, mais ne garantie pas une instanciation cohérente de ta classe. C'est même incompatible selon le niveau de visibilité de tes accesseurs.
Dans un premier temps, afin de te familiariser avec la programmation orientée objet, j'aurais tendance à te conseiller de toujours créer et utiliser des constructeurs. C'est un peu plus long, mais ça te permet de garder le contrôle sur ton code.
Quand tu seras plus à l'aise avec tout ça, et que tu souhaiterai aller plus en profondeur, tu pourra essayer de mélanger les deux façons de faire.
Challenge
Réécriture
Réécris le code suivant afin qu'il utilise des propriétés pour mettre en place l'encapsulation :
using System;
using System.Collections.Generic;
using System.Linq;
namespace Encapsulation
{
public class Program
{
public static void Main(string[] args)
{
House house = new House();
Room room = new Room();
Door houseDoor = new Door();
Door roomDoor = new Door();
house.AddDoor(houseDoor);
room.AddDoor(roomDoor);
house.AddRoom(room);
}
}
public class House
{
public List<Room> _rooms;
public List<Door> _doors;
public House()
{
_rooms = new List<Room>();
_doors = new List<Door>();
}
public void AddDoor(Door door)
{
_doors.Add(door);
}
public void AddRoom(Room room)
{
_rooms.Add(room);
}
}
public class Room
{
public List<Door> _doors;
public Room()
{
_doors = new List<Door>();
}
public void AddDoor(Door door)
{
_doors.Add(door);
}
}
public class Door
{
public bool _isOpen;
public Door()
{
_isOpen = false;
}
public void Open()
{
if (_isOpen)
{
Console.WriteLine("Door already opened. Ain't done anything.");
}
else
{
_isOpen = true;
}
}
public void Close()
{
if (!_isOpen)
{
Console.WriteLine("Door already closed. Ain't done anything.");
}
else
{
_isOpen = false;
}
}
}
}
Critères de validation
- Les accesseurs ne sont plus des méthodes, mais des propriétés
- Les propriétés sont en lecture seule
- Les méthodes
AddDoor, etAddRoomdes classesRoometHousen'existent plus au profit de l'utilisation des propriétés - Le code du point d'entrée Main est modifié en accord avec l'utilisation des propriétés
- Votre programme se compile et se lance sans erreur