La programmation orientée-objet est un paradigme de programmation (un “style” de conception de programmes) au même titre que
la programmation impérative (/structurée/procédurale),
la programmation fonctionnelle,
la programmation logique,
…
impératif: C, Fortran, Assembleur, …
fonctionnel: Haskell, F#, Reason, Scheme, …
objet: Java, C#, Ruby, Smalltalk, …
multi-paradigmes: Scala, C++, OCaml, Python, …
Non ! De multiples modèles objets déterminés par
une collection de traits distincts,
mais des emphases/variantes significatives,
et une dimension culturelle/historique forte …
Pas de consensus universel, mais des caractéristiques communes !
(a propos du système Oberon)
“A lot of the developers and managers at Apple were gathered around watching a presentation from someone about some wonderful new product that would save the world. All through the presentation, he had been stating that the product was object-oriented while he blathered on.”
Finally, someone at the back of the room piped up:
“So, this product doesn’t support inheritance, right?”
“that’s right”.
“And it doesn’t support polymorphism, right?”
“that’s right”
“And it doesn’t support encapsulation, right?”
“that’s correct”.
To which the presenter huffily responded,
At this point the person replied,
(Source: “He invented the term”)
Source: The Forgotten History of OOP
“OOP to me means only messaging, local retention and protection and hiding of state-process, and extreme late-binding of all things.”
Alan Kay.
Termes fréquents: envoi de messages, encapsulation, attachement dynamique, classes, instances, champs, méthodes, héritage, polymorphisme, composition, délégation, …
Point
(2D)2 champs : x
et y
(valeurs numériques)
1 méthode “spéciale”: le constructeur
1 méthode “normale”: distance
(à l’origine)
Lecture :
Ecriture :
jshell> /open Point.java
jshell> Point point = new Point(1.0, 2.0)
point ==> Point@238e0d81
jshell> point.x
$1 ==> 1.0
jshell> point.distance()
$2 ==> 2.23606797749979
> Point point = new Point(1.0, 2.0);
> point
<object reference> (Point)
> point.distance()
2.23606797749979 (double)
class Point:
def __init__(self, x, y):
self.x = x; self.y = y
def distance(self):
return math.sqrt(self.x**2 + self.y**2)
>>> point = Point(1.0, 2.0)
>>> point.x
1.0
>>> point.distance()
2.23606797749979
class Point
def initialize(x, y)
@x = x; @y = y
end
def distance
Math.sqrt(@x**2 + @y**2)
end
end
irb> point = Point.new 1.0, 2.0
=> #<Point @x=1.0, @y=2.0>
irb> point.x
NoMethodError: undefined method 'x' for #<Point:0x00000001223ef8 @x=1.0, @y=2.0>
irb> point.distance
=> 2.23606797749979
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.distance = function () {
return Math.sqrt(this.x**2 + this.y**2);
}
> point = new Point(1.0, 2.0)
Point { x: 1, y: 2 }
> point.x
1
> point.distance()
2.23606797749979
Usage: Javascript, Lua, Self.
Voir aussi: Prototypes in JavaScript
class Point
constructor: (@x, @y) ->
distance: ->
Math.sqrt(@x**2 + @y**2)
coffee> point = new Point 1.0, 2.0
Point { x: 1, y: 2 }
coffee> point.x
1
coffee> point.distance()
2.23606797749979
class Point {
constructor(x, y) {
this.x = x; this.y = y;
}
distance() {
return Math.sqrt(this.x**2 + this.y**2);
}
}
> point = new Point(1.0, 2.0)
Point { x: 1, y: 2 }
> point.x
1
> point.distance()
2.23606797749979
“désigne le principe de regrouper des données brutes avec un ensemble de routines permettant de les lire ou de les manipuler.”
“(…) souvent accompagné du masquage de ces données brutes afin de s’assurer que l’utilisateur ne contourne pas l’interface qui lui est destinée.”
“L’ensemble se considère alors comme une boîte noire ayant un comportement et des propriétés spécifiés.”
Architecture. Le logiciel est réalisé par assemblage de composants – plus ou moins autonomes – pour réduire la complexité de l’ensemble.
Abstraction & Découplage. Ce que fait un objet (son interface) est plus important que comment il le fait (son implémentation) ; cette “ignorance sélective” contribue à abaisser la complexité (visible) de chaque composant.
Les mots-clés
public, protected, private
contrôlent l’accès aux attributs et méthodes.
En cas de champ XXX
non-public, on peut permettre son accès contrôlé à travers de fonctions (accesseurs).
Par exemple :
private XXX xxx;
public XXX getXXX() { ... };
public void setXXX(XXX xxx) { ... };
Spécification (boîte noire):
>>> Fraction(7)
7
>>> Fraction(2, 3)
2/3
>>> Fraction(1, 3) + Fraction(1, 6)
1/2
...
Constructeur
class Fraction:
def __init__(self, num, den=1):
self._num = num
self._den = den
self._simplify()
Méthode utilitaire
def _simplify(self):
gcd = math.gcd(self._num, self._den)
self._num = self._num / gcd
self._den = self._den / gcd
if self._den < 0:
self._num = - self._num
self._denom = - self._denom
Méthode d’addition
def __add__(self, other):
num = self._num * other._den + \
other._num * self._den
den = self._den * other._den
return Fraction(num, den)
Méthode de représentation
def __repr__(self):
if self._den == 1:
return f"{self._num}"
else:
return f"{self._num}/{self._den}"
Les données des fractions sont stockées dans les attributs (ou champs) _num
et _den
,
Les méthodes __init__
, simplify
, __add__
, … permettent de les manipuler.
En Python :
_num
ou appeler la méthode _simplify
.Par exemple:
>>> f = Fraction(4, 6)
>>> f._num = 7
>>> f
???
En Python (lecture seule ou “getter”):
def get_numerator(self):
return self._num
et optionnellement:
numerator = property(get_numerator)
Usage:
>>> f = Fraction(2, 3)
>>> f.get_numerator()
2
>>> f.numerator
2
Assemblage / Architecture
Les “objets” communiquent par envoi de messages.
“(…) considère les acteurs comme les seules fonctions primitives nécessaires pour la programmation concurrente.
Les acteurs communiquent par échange de messages. En réponse à un message, un acteur peut effectuer un traitement local, créer d’autres acteurs, ou envoyer d’autres messages.”
“Actors systems research was based on the assumption that massively parallel, distributed, computer systems could become prevalent, and therefore a convenient and efficient way to structure a computation was as a large number of self contained processes, called actors, communicating by sending messages to each other.”
“I realised that Erlang was the only true OO language
– the big thing about OO is message passing –
Java/C++ are not OO.”
See also Why OO Sucks
S’inscrivent dans cette philosophie:
Smalltalk, Erlang, Ruby, Elixir, etc.
Ruby
> 1 + 2
=> 3
L’opérateur +
calcul la somme des valeurs 1
et 2
.
Ruby
> 1.+(2)
=> 3
Le calcul est délégué à la méthode +
de l’objet 1
.
Ruby
> 1.send(:+, 2)
=> 3
L’addition est la réponse à un message
– contenant le symbole +
et l’objet 2
–
adressé à l’objet 1
.
Interpréter oldVal = map.put(key, val)
comme :
l’envoi du message "put"
,
contenant les données key
et val
(payload),
à l’objet map
,
qui répond oldValue
.
“Unfortunately, inheritance – though an incredibly powerful technique – has turned out to be very difficult for novices (and even professionals) to deal with.”
Alan Kay
(Smalltalk-72 n’a pas d’héritage)
What does Alan Kay think about inheritance in object-oriented programming?
Compatible avec le modèle objet (aggrégation/encapsulation de données et de code),
Réutilisation/Extension (sans modification) de code,
Flexibilité (polymorphisme & attachement dynamique).
Il repose sur les classes et les interfaces,
Les obligations du programmeur sont vérifiées par le compilateur (en partie).
Point
Le code
affiche
Point@76ed5528
D’où cette fonctionnalité “gratuite” vient-elle ?
Le code précédent est équivalent à
Point point = new Point(1.0, 2.0);
String string = point.toString();
java.lang.System.out.println(string);
Tous les objets Java peuvent être convertis en une chaîne de caractères,
Il existe une implémentation par défaut de cette méthode,
Elle est héritée de la classe Object
.
Object
Object
Notre classe Point
aurait pu être définie comme
(Le extends Object
est implicite.)
Les instances de Point
disposent donc gratuitement (héritent) des méthodes implémentées par Object
:
String toString()
boolean equals(Object object)
Object clone()
…
API : https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html
Dans Object
, toString
est implémentée comme
toString
System.out
est un PrintStream
, avec les méthodes :
println(String x)
println(Object x)
…
System.out.println
:
accepte les instances d’Object
, donc accepte toute instance dérivée, donc les Point
s (polymorphisme d’héritage).
ignore lorsqu’il invoque x.toString()
quelle méthode est effectivement exécutée, celle d’Object
ou d’une classe dérivée (liaison dynamique).
Les méthodes Java sont virtuelles : à l’exécution, les appels de fonctions sont déléguées aux classes dérivées quand les méthodes sont définies.
est équivalent à
Stricto sensu, Java ne permet pas l’héritage multiple : une seule classe parent est autorisée. Les hiérarchies de classes sont donc linéaires.
Mais les interfaces, similaires aux classes par certains aspects, permettent de contourner cette limitation.
Les interfaces sont des “contrats”, des engagements que votre classe s’engage à tenir.
Par exemple, une classe implémentant l’interface
// fichier XML.java
interface XML {
public String toXML();
}
s’engage à fournir une méthode toXML
donnant la représentation de ses instances comme chaîne de caractères XML.
Le compilateur Java va vérifier que vous remplissez votre contrat : compiler
public class Point implements XML {
// sans la méthode toXML
}
produit
error: Point is not abstract and does not override abstract method toXML() in XML
Par contre, il ne va pas s’opposer à ce que votre fonction retourne une chaîne de caractères qui ne serait pas du XML !
Seule la partie vérifiable du contrat est prise en charge par la compilateur. Attention au “contrat moral” qui peut venir en plus ; il est important de bien lire la documentation des interfaces !
Hiérarchie de classes et d’interface similaires (on peut étendre une interface en une autre interface).
Conversions avec des mécanismes similaires :
Si les upcasts peuvent être implicites, les downcasts doivent être explicites (du type général vers le type spécifique) et peuvent échouer à l’exécution.
On n’instancie pas directement une interface :
Il faut créer une classe qui implémente l’interface
déclaration de méthodes uniquement
(hors champs public static final
)
les méthodes sont toutes publiques,
pas d’implémentation (hors méthodes default
)
héritage/implémentation d’interfaces multiples :
interface I1 extends I2, I3 { ... }
class C implements I2, I3 { ... }
Hériter de – ou étendre – LinkedList
, une classe :
import java.util.LinkedList;
public class MyList extends LinkedList<Integer> {
public String toString() {
return "<" + super.toString() + ">";
}
}
Permet de réutiliser son implémentation.
$ java Main
<[1, 2]>
Mais la fonction addOneTwo
ne peut être utilisée qu’avec les instances de MyList
(ou qui en dérivent).
Son usage est donc (trop) limité …
La classe LinkedList
implémente de nombreuses interfaces (ou “contrats” vérifiés par le compilateur):
Serializable
, Cloneable
, …, Deque
, List
, Queue
En implémentant List<E>
, la classe LinkedList<E>
garantit qu’elle implémente la méthode add
:
boolean add(E e)
Toutes les classes implémentant List
sont désormais susceptibles d’utiliser addOneTwo
:
MyList
, LinkedList<Integer>
, Vector<Integer>
, etc.
list
>>> l = [1, 2, 3]
>>> l
[1, 2, 3]
>>> type(l)
<class 'list'>
>>> sum(l)
6
List
(usage)>>> l = List([1, 2, 3])
>>> l
<[1, 2, 3]>
>>> type(l)
<class 'List'>
>>> sum(l)
6
la représentation de ma liste a changé,
ainsi que son type, List
et non list
,
mais pas le reste des fonctionnalités.
en héritant de la class list
, on peut réutiliser ses fonctionnalités,
on peut également enrichir ou modifier (redéfinir) ses comportements.
List
(implementation)(repr
appelle la méthode __repr__
de item
)
le code de display
ne permet pas de dire quelle implémentation de __repr__
va être utilisée (attachement dynamique/tardif).
le “contrat moral” est d’utiliser comment argument un objet représentable.
tous les types d’objets respectant cette contrainte peuvent être utilisés comme argument (polymorphisme).
__repr__
spécifique dans votre classe, Python va se tourner vers les classes dont elle hérite (object
par défaut).(CC BY-SA 3.0, Link)
L’argument doit passer le test du canard:
“If it looks like a duck, swims like a duck, and quacks like a duck, then it probably is a duck.”
S’il échoue, une exception se produit (elle peut être gérée par le programme).
C’est avoir une liste (et non pas être une liste).
On peut “être une liste” sans hériter de list
: