public class Conception {}
/* La phase de conception intervient une fois complétée l'analyse du système
* à construire, et définit les structures de données et l'organisation du
* code. La phase d'analyse s'intéressait au "quoi", la phase de conception
* se concentre sur le "comment".
*/
/* I. Les attributs */
class Complexe {
/* Prenons une classe représentant les nombres complexes. La phase
* d'analyse lui a associé quatre attributs :
* - partie réelle [r]
* - partie imaginaire [img]
* - module [rho]
* - argument [theta]
* Ces arguments ne sont pas indépendants les uns des autres.
*/
private double r, img, rho, theta;
/* Un premier constructeur : il prend en paramètre une partie réelle
* et une partie imaginaire, et en déduit le module et l'argument.
*/
public Complexe(double r, double i) {
this.r = r;
this.img = i;
updateRho();
if (r!=0) this.theta = Math.arctan(i/r);
else this.theta=Math.PI/2.0;
}
/* Dans de telles conditions, toute modification de l'un des attributs
* doit être reflétée sur les autes.
* Dans le vocabulaire du cours précédent, la relation entre les quatre
* attributs [r], [img], [rho] et [theta] est un invariant de notre classe
* [Complexe].
* Une des raisons pour lesquelles je vous ai toujours dit beaucoup
* de mal des "setters" (méthodes publiques qui permettent de modifier la
* valeur d'un attribut) est qu'ils risquent d'invalider les invariants
* d'un objet s'ils sont mal écrits.
* En l'occurrence, si l'on veut fournir un setter pour la partie
* imaginaire d'un nombre complexe, alors il ne faut pas modifier
* uniquement l'attribut [img], mais aussi mettre à jour [rho] et [theta]
* en conséquence.
*/
public void setImg(double i) {
this.img = i;
updateRho();
this.theta = ...
}
private void updateRho() {
this.rho = Math.sqrt(this.r*this.r + this.img*this.img);
}
/* Dans cette version nous avons donc :
* - plusieurs attributs
* - un accès immédiat aux valeurs de ces attributs
* - un coût à payer pour faire des mises à jour à chaque fois que
* l'un des attributs est modifié
*/
}
/* On peut éviter ce genre de mises à jour si on élimine les informations
* redondantes dans la classe. Par exemple, on peut ne donner à une classe
* [Complexe] que les attributs [r] et [img], et calculer les valeurs de [rho]
* et [theta] uniquement à la demande. On dit alors que [rho] et [theta] sont
* des attributs dérivés.
*/
class Complexe {
/* Deux attributs */
private double r, img;
/* Un seul constructeur */
public Complexe(double r, double i) {
this.r = r;
this.img = i;
}
/* Un modificateur qui ne casse aucun invariant */
public void setImg(double i) {
this.img = i;
}
/* Un getter qui calcule à la volée la valeur de "l'attribut" qu'il
* représente.
*/
public double getRho() {
return Math.sqrt(this.r*this.r + this.img*this.img);
}
/* Dans cette version nous avons donc :
* - des attributs réels et des attributs dérivés
* - une possibilité de modifier facilement les valeurs des attributs réels
* - un coût à payer pour accéder aux valeurs des attributs secondaires
* Le choix entre les deux versions peut dépendre de l'usage attendu
* de la classe concernée, pour essayer de minimiser le coût des opérations
* les plus courantes.
*/
}
/* Digression : une variante dans laquelle on ajoute à chaque opération
* de lecture ou de modification un coût modique pour éviter les surcoûts
* importants sur une catégorie ou l'autre.
*/
class Complexe {
/* On prend les quatre attributs et on ajoute un booléen qui indique si
* la valeur de l'un des attributs a été modifiée.
*/
private double r, img, rho, theta;
private boolean change;
public Complexe(double r, double i) {
this.r = r;
this.img = i;
updateRho();
if (r!=0) this.theta = Math.arctan(i/r);
else this.theta=Math.PI/2.0;
}
/* Lors de la modification de [img] on ne met pas à jour les autres
* attributs, mais on signale qu'un changement a été apportée avec
* le booléen [change].
*/
public void setImg(double i) {
this.img = i;
this.change = true;
}
/* Lors de la lecture de [rho] on commence par consulter l'attribut
* [change] pour savoir si la valeur de [rho] est encore à jour ou si elle
* doit être recalculée.
*/
public double getRho() {
if (!this.change) { return this.rho; }
else {
updateRho(); updateTheta();
this.change=false;
return this.rho;
}
}
void updateRho() {
this.rho = Math.sqrt(this.r*this.r + this.img*this.img);
}
}
/* Reprenons la classe d'origine avec son constructeur. */
class Complexe {
private double r, img, rho, theta;
public Complexe(double r, double i) {
this.r = r;
this.img = i;
updateRho();
if (r!=0) this.theta = Math.arctan(i/r);
else this.theta=Math.PI/2.0;
}
/* On pourrait vouloir écrire un deuxième constructeur basé sur le
* module et l'argument.
* Ce n'est pas possible : le mécanisme de surcharge est basé sur le type
* des paramètres, qui sont identiques pour nos deux constructeurs.
*/
public Complexe(double rho, double theta) {
this.rho = rho;
this.theta = theta;
...
}
/* Une tentative pour donner néanmoins les deux possibilités : un
* constructeur qui prend en troisième paramètre un booléen indiquant
* un choix de coordonnées cartésiennes ou polaires.
*/
public Complexe(double p1, double p2, boolean cart) {
if (cart) {
this.r = r;
this.img = i;
updateRho();
if (r!=0) this.theta = Math.arctan(i/r);
else this.theta=Math.PI/2.0;
} else {
// Initialisation en coordonnées polaires.
}
}
/* On peut faire mieux avec le design pattern "factory", voir
* classe suivante.
*/
}
/* Design pattern "factory" : donner plusieurs manières de construire
* un objet, en les distinguant de manière plus explicite que par le seul
* mécanisme de surcharge.
*/
class Complexe {
private double r, img, rho, theta;
/* Le constructeur est indiqué "privé" : il n'est pas directement
* accessible de l'extérieur.
*/
private Complexe(double r, double i) {
this.r = r;
this.img = i;
updateRho();
if (r!=0) this.theta = Math.arctan(i/r);
else this.theta=Math.PI/2.0;
}
/* À la place, on fournit deux méthodes aux noms évocateurs, qui
* appellent le constructeur et renvoie l'objet construit.
* L'utilisation du mot-clé [static] sera expliquée au prochain cours.
*/
public static Complexe creeComplexeCartesien(double r, double i) {
return new Complexe(r, i);
}
public static Complexe creeComplexePolaire(double rho, double theta) {
double r = ...;
double i = ...;
return new Complexe(r, i);
}
}
/* II. Les associations */
/* Les associations peuvent être réalisées de multiples façons.
* On donne ici quelques exemples en fonction de l'arité de l'association
* et de la navigabilité souhaitée.
* Ces exemples sont des bonnes pratiques car ils font en sorte que les arités
* soient respectées en permanence, mais ils ne sont pas les seules manières
* de faire.
*/
/* II.A. Les associations unidirectionnelles */
/* On parle d'une association unidirectionnelle lorsque la navigation se fait
* toujours dans la même direction. Dans les exemples suivants, un objet de
* la classe [A] pourra accéder à un (ou des) objet de la classe [B].
* C'est le cas typique de la délégation.
*/
/* Association unidirectionnelle, arité 1-1 */
class A {
private B b;
public A() {
/* Création d'un objet B privé directement à l'intérieur de
* la classe : personne d'autre n'y a accès.
*/
this.b = new B();
}
}
class B {}
/* Association unidirectionnelle, arité *-1 */
class A {
private B b;
/* Ici, l'objet B est pris en paramètre : il a été défini a l'extérieur
* et peut être lié à d'autres objets A.
*/
public A(B b) {
this.b = b;
}
}
class B {}
/* Association unidirectionnelle, arité *-* */
class A {
/* Un objet de la classe [A] est associé à une collection d'objets de la
* classe [B]. Cette collection peut être ordonnée ou non, avec répétitions
* ou non, est donc être réalisée par différentes structures de données.
*/
private Set bs; // Alternative : private List bs;
public A() {
/* Création d'un ensemble vide */
this.bs = new HashSet();
}
/* Ajout à la collection d'un élément [B] défini à l'extérieur. */
public void addB(B b) {
bs.add(b);
}
}
class B {}
/* II.B. Les associations bidirectionnelles. */
/* On parle d'une association bidirectionnelle lorsque la navigation peut se
* faire dans les deux sens. Dans les exemples suivants, un objet de la classe
* [A] pourra accéder à un (ou des) objet de la classe [B], qui pourra lui-même
* accéder à l'objet [A] en retour.
*/
/* Association bidirectionnelle, arité 1-1 */
class A {
/* Association représentée par un attribut */
private B b;
public A() {
/* Le constructeur crée un [B] de manière à ce qu'il soit associé à
* l'objet A qu'on est en train de créer.
*/
this.b = new B(this);
}
}
class B {
/* Cette fois, un objet [B] a lui aussi un objet [A] en attribut. */
private A a;
/* Le constructeur prend en paramètre l'objet [A] auquel il faut
* s'associer.
*/
public B(A a) {
this.a = a;
}
}
/* La version que nous venons de voir est asymétrique : c'est en créant l'objet
* [A] que l'on crée la paire d'objets associés. On peut rendre la situation
* plus symétrique en ajoutant un constructeur [public A(B b)] à [A] et un
* constructeur [public B()] à [A].
*/
/* Association bidirectionnelle, arité 1-* */
class A {
private List bs;
public A() {
/* Initialisation avec une liste vide. */
this.bs = new ArrayList();
}
/* L'ajout d'un élément [b] à la liste implique aussi de mettre à jour
* cet objet [b].
*/
public void addB(B b) {
/* Ajoute b à notre liste. */
this.bs.add(b);
/* Pour l'instant l'état n'est pas cohérent :
* b.a n'a pas été mis à jour.
*/
b.setA(this);
/* Maintenant c'est bon. */
}
/* Cette méthode sera appelée par [B.setA]. */
protected void removeB(B b) {
this.bs.remove(b);
}
}
class B {
private A a;
protected void setA(A a) {
/* Quand on ajoute l'objet à la liste d'un [A], il faut aussi supprimer
* l'éventuelle ancienne association à un autre [A].
* Dans le cas général, tester d'abord a!=null.
*/
this.a.removeB(this);
/* Enfin, on peut faire la mise à jour proprement dite. */
this.a = a;
}
}
/* Exercice : faire une association bidirectionnelle *-* */