![t](t.gif) |
![t](t.gif) |
1) Introduction sur les « objets » |
|
![t](t.gif) |
|
Texte original |
![t](t.gif) |
Traducteur :
Jérome Quelin |
|
![t](t.gif) |
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
|
![t](t.gif) |
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
A type does more than describe the
constraints on a set of objects; it also has a relationship with other types.
Two types can have characteristics and behaviors in common, but one type may
contain more characteristics than another and may also handle more messages (or
handle them differently). Inheritance expresses this similarity between types
using the concept of base types and
derived types. A base type contains
all of the characteristics and behaviors that are shared among the types derived
from it. You create a base type to represent the core of your ideas about some
objects in your system. From the base type, you derive other types to express
the different ways that this core can be realized.
|
![t](t.gif) |
Un type fait plus que décrire des contraintes sur un ensemble
d'objets ; il a aussi des relations avec d'autres types. Deux types peuvent avoir des
caractéristiques et des comportements en commun, mais l'un des deux peut avoir plus de
caractéristiques que l'autre et peut aussi réagir à plus de messages (ou y réagir de manière
différente). L'héritage exprime cette similarité entre les types en introduisant le concept
de types de base et de types dérivés. Un type de base contient toutes les
caractéristiques et comportements partagés entre les types dérivés. Un type de base est créé pour
représenter le coeur de certains objets du système. De ce type de base, on dérive d'autres types
pour exprimer les différentes manières existantes pour réaliser ce coeur.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
For example, a trash-recycling machine
sorts pieces of trash. The base type is “trash,” and each piece of
trash has a weight, a value, and so on, and can be shredded, melted, or
decomposed. From this, more specific types of trash are derived that may have
additional characteristics (a bottle has a color) or behaviors (an aluminum can
may be crushed, a steel can is magnetic). In addition, some behaviors may be
different (the value of paper depends on its type and condition). Using
inheritance, you can build a type hierarchy that expresses the problem
you’re trying to solve in terms of its types.
|
![t](t.gif) |
Prenons l'exemple d'une machine de recyclage qui trie les détritus. Le type
de base serait « détritus », caractérisé par un poids, une valeur, etc.. et peut être
concassé, fondu, ou décomposé. A partir de ce type de base, sont dérivés des types de détritus plus
spécifiques qui peuvent avoir des caractéristiques supplémentaires (une bouteille a une couleur) ou
des actions additionnelles (une canette peut être découpée, un container d'acier est magnétique).
De plus, des comportements peuvent être différents (la valeur du papier dépend de son type et de
son état général). En utilisant l'héritage, on peut bâtir une hiérarchie qui exprime le problème
avec ses propres termes.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
A second example is the classic
“shape” example, perhaps used in a
computer-aided design system or game simulation. The base type is
“shape,” and each shape has a size, a color, a position, and so on.
Each shape can be drawn, erased, moved, colored, etc. From this, specific types
of shapes are derived (inherited): circle, square, triangle, and so on, each of
which may have additional characteristics and behaviors. Certain shapes can be
flipped, for example. Some behaviors may be different, such as when you want to
calculate the area of a shape. The type hierarchy embodies both the similarities
and differences between the shapes.
|
![t](t.gif) |
Un autre exemple classique : les « formes géométriques »,
utilisées entre autres dans les systèmes d'aide à la conception ou dans les jeux vidéos. Le type de
base est la « forme géométrique », et chaque forme a une taille, une couleur, une
position, etc... Chaque forme peut être dessinée, effacée, déplacée, peinte, etc... A partir de ce
type de base, des types spécifiques sont dérivés (hérités) : des cercles, des carrés, des
triangles et autres, chacun avec des caractéristiques et des comportements additionnels (certaines
figures peuvent être inversées par exemple). Certains comportements peuvent être différents, par
exemple quand on veut calculer l'aire de la forme. La hiérarchie des types révèle à la fois les
similarités et les différences entre les formes.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
|
![t](t.gif) |
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Casting the solution in the same terms as
the problem is tremendously beneficial because you don’t need a lot of
intermediate models to get from a description of the problem to a description of
the solution. With objects, the type hierarchy is the primary model, so you go
directly from the description of the system in the real world to the description
of the system in code. Indeed, one of the difficulties people have with
object-oriented design is that it’s too simple to get from the beginning
to the end. A mind trained to look for complex solutions is often stumped by
this simplicity at first.
|
![t](t.gif) |
Représenter la solution avec les mêmes termes que ceux du problème est
extraordinairement bénéfique car on n'a pas besoin de modèles intermédiaires pour passer de la
description du problème à la description de la solution. Avec les objets, la hiérarchie de types
est le modèle primaire, on passe donc du système dans le monde réel directement au système du code.
En fait, l'une des difficultés à laquelle les gens se trouvent confrontés lors de la conception
orientée objet est que c'est trop simple de passer du début à la fin. Les esprits habitués à des
solutions compliquées sont toujours stupéfaits par cette simplicité.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
When you inherit from an existing type,
you create a new type. This new type contains not only all the members of the
existing type (although the private ones are hidden away and
inaccessible), but more important, it duplicates the interface of the base
class. That is, all the messages you can send to objects of the base class you
can also send to objects of the derived class. Since we know the type of a class
by the messages we can send to it, this means that the derived class is the
same type as the base class. In the previous example, “a circle is a
shape.” This type equivalence via inheritance is one of the fundamental
gateways in understanding the meaning of object-oriented
programming.
|
![t](t.gif) |
Quand on hérite d'un certain type, on crée un nouveau type. Ce nouveau type
non seulement contient tous les membres du type existant (bien que les membres privés soient cachés
et inaccessibles), mais plus important, il duplique aussi l'interface de la classe de la base.
Autrement dit, tous les messages acceptés par les objets de la classe de base seront acceptés par
les objets de la classe dérivée. Comme on connaît le type de la classe par les messages qu'on peut
lui envoyer, cela veut dire que la classe dérivée est du même type que la classe de base.
Dans l'exemple précédent, « un cercle est une forme ». Cette équivalence de type via
l'héritage est l'une des notions fondamentales dans la compréhension de la programmation orientée
objet.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Since both the base class and derived
class have the same interface, there must be some implementation to go along
with that interface. That is, there must be some code to execute when an object
receives a particular message. If you simply inherit a class and don’t do
anything else, the methods from the base-class interface come right along into
the derived class. That means objects of the derived class have not only the
same type, they also have the same behavior, which isn’t particularly
interesting.
|
![t](t.gif) |
Comme la classe de base et la classe dérivée ont toutes les deux la même
interface, certaines implémentations accompagnent cette interface. C'est à dire qu'il doit y avoir
du code à exécuter quand un objet reçoit un message particulier. Si on ne fait qu'hériter une
classe sans rien lui rajouter, les méthodes de l'interface de la classe de base sont importées dans
la classe dérivée. Cela veut dire que les objets de la classe dérivée n'ont pas seulement le même
type, ils ont aussi le même comportement, ce qui n'est pas particulièrement intéressant.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
You have two ways to differentiate your
new derived class from the original base class. The first is quite
straightforward: You simply add brand new functions to the derived class. These
new functions are not part of the base class interface. This means that the base
class simply didn’t do as much as you wanted it to, so you added more
functions. This simple and primitive use for
inheritance is, at times, the
perfect solution to your problem. However, you should look closely for the
possibility that your base class might also need these additional functions.
This process of discovery and iteration of your design happens regularly in
object-oriented programming.
|
![t](t.gif) |
Il y a deux façons de différencier la nouvelle classe dérivée de la classe
de base originale. La première est relativement directe : il suffit d'ajouter de nouvelles
fonctions à la classe dérivée. Ces nouvelles fonctions ne font pas partie de la classe parent. Cela
veut dire que la classe de base n'était pas assez complète pour ce qu'on voulait en faire, on a
donc ajouté de nouvelles fonctions. Cet usage simple de l'héritage se révèle souvent être une
solution idéale. Cependant, il faut tout de même vérifier s'il ne serait pas souhaitable d'intégrer
ces fonctions dans la classe de base qui pourrait aussi en avoir l'usage. Ce processus de
découverte et d'itération dans la conception est fréquent dans la programmation orientée
objet.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
|
![t](t.gif) |
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Although inheritance may sometimes imply
(especially in Java, where the keyword that indicates inheritance is
extends) that you are going to add new functions to the interface,
that’s not necessarily true. The second and more important way to
differentiate your new class is to change the behavior of an existing
base-class function. This is referred to as
overriding that
function.
|
![t](t.gif) |
Bien que l'héritage puisse parfois impliquer (spécialement en Java, où le
mot clef qui indique l'héritage est extends) que de nouvelles fonctions vont être
ajoutées à l'interface, ce n'est pas toujours vrai. La seconde et plus importante manière de
différencier la nouvelle classe est de changer le comportement d'une des fonctions
existantes de la superclasse. Cela s'appelle redéfinir cette fonction.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
|
![t](t.gif) |
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
To override a function, you simply create
a new definition for the function in the derived class. You’re saying,
“I’m using the same interface function here, but I want it to do
something different for my new
type.”
|
![t](t.gif) |
Pour redéfinir une fonction, il suffit de créer une nouvelle définition
pour la fonction dans la classe dérivée. C'est comme dire : « j'utilise la même interface
ici, mais je la traite d'une manière différente dans ce nouveau type ».
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Is-a vs. is-like-a relationships
|
![t](t.gif) |
Les relations est-un vs. est-comme-un
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
There’s a certain debate that can
occur about inheritance: Should inheritance override only base-class
functions (and not add new member functions that aren’t in the base
class)? This would mean that the derived type is exactly the same type as
the base class since it has exactly the same interface. As a result, you can
exactly substitute an object of the derived class for an object of the base
class. This can be thought of as pure substitution,
and it’s often referred to as the substitution
principle. In a sense, this is the ideal way to treat inheritance. We often
refer to the relationship between the base class and derived classes in this
case as an is-a relationship, because you can say “a circle is
a shape.” A test for inheritance is to determine whether you can state
the is-a relationship about the classes and have it make sense.
|
![t](t.gif) |
Un certain débat est récurrent à propos de l'héritage : l'héritage ne
devrait-il pas seulement redéfinir les fonctions de la classe de base (et ne pas ajouter
de nouvelles fonctions membres qui ne font pas partie de la superclasse) ? Cela voudrait dire que
le type dérivé serait exactement le même que celui de la classe de base puisqu'il aurait
exactement la même interface. Avec comme conséquence logique le fait qu'on puisse exactement
substituer un objet de la classe dérivée à un objet de la classe de base. On fait souvent référence
à cette substitution pure sous le nom de principe de substitution. Dans
un sens, c'est la manière idéale de traiter l'héritage. La relation entre la classe de base
et la classe dérivée dans ce cas est une relation est-un, parce qu'on peut dire « un
cercle est une forme ». Un test pour l'héritage est de déterminer si la relation
est-un entre les deux classes considérées a un sens.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
There are times when you must add new
interface elements to a derived type, thus extending the interface and creating
a new type. The new type can still be substituted for the base type, but the
substitution isn’t perfect because your new functions are not accessible
from the base type. This can be described as an
is-like-a[6]
relationship; the new type has the interface of the old type but it also
contains other functions, so you can’t really say it’s exactly the
same. For example, consider an air conditioner. Suppose your house is wired with
all the controls for cooling; that is, it has an interface that allows you to
control cooling. Imagine that the air conditioner breaks down and you replace it
with a heat pump, which can both heat and cool. The heat pump is-like-an
air conditioner, but it can do more. Because the control system of your house is
designed only to control cooling, it is restricted to communication with the
cooling part of the new object. The interface of the new object has been
extended, and the existing system doesn’t know about anything except the
original interface.
|
![t](t.gif) |
Mais parfois il est nécessaire d'ajouter de nouveaux éléments à l'interface
d'un type dérivé, et donc étendre l'interface et créer un nouveau type. Le nouveau type peut
toujours être substitué au type de base, mais la substitution n'est plus parfaite parce que les
nouvelles fonctions ne sont pas accessibles à partir de la classe parent. On appelle cette relation
une relation est-comme-un [6]; le nouveau type
dispose de l'interface de l'ancien type mais il contient aussi d'autres fonctions, on ne peut donc
pas réellement dire que ce soient exactement les mêmes. Prenons le cas d'un système de
climatisation. Supposons que notre maison dispose des tuyaux et des systèmes de contrôle pour le
refroidissement, autrement dit elle dispose d'une interface qui nous permet de contrôler le
refroidissement. Imaginons que le système de climatisation tombe en panne et qu'on le remplace par
une pompe à chaleur, qui peut à la fois chauffer et refroidir. La pompe à chaleur
est-comme-un système de climatisation, mais il peut faire plus de choses. Parce que le
système de contrôle n'a été conçu que pour contrôler le refroidissement, il en est restreint à ne
communiquer qu'avec la partie refroidissement du nouvel objet. L'interface du nouvel objet a été
étendue, mais le système existant ne connaît rien qui ne soit dans l'interface
originale.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
|
![t](t.gif) |
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Of course, once you see this design it
becomes clear that the base class “cooling system” is not general
enough, and should be renamed to “temperature control system” so
that it can also include heating—at which point the substitution principle
will work. However, the diagram above is an example of what can happen in design
and in the real world.
|
![t](t.gif) |
Bien sûr, quand on voit cette modélisation, il est clair que la classe de
base « Système de refroidissement » n'est pas assez générale, et devrait être renommée en
« Système de contrôle de température » afin de pouvoir inclure le chauffage - auquel cas
le principe de substitution marcherait. Cependant, le diagramme ci-dessus est un exemple de ce qui
peut arriver dans le monde réel.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
When you see the substitution principle
it’s easy to feel like this approach (pure substitution) is the only way
to do things, and in fact it is nice if your design works out that way.
But you’ll find that there are times when it’s equally clear that
you must add new functions to the interface of a derived class. With inspection
both cases should be reasonably
obvious.
|
![t](t.gif) |
Quand on considère le principe de substitution, il est tentant de se dire
que cette approche (la substitution pure) est la seule manière correcte de modéliser, et de fait
c'est appréciable si la conception fonctionne ainsi. Mais dans certains cas il est tout
aussi clair qu'il faut ajouter de nouvelles fonctions à l'interface d'une classe dérivée. En
examinant le problème, les deux cas deviennent relativement évidents.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Interchangeable objects with polymorphism
|
![t](t.gif) |
Polymorphisme : des objets interchangeables
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
When dealing with type hierarchies, you
often want to treat an object not as the specific type that it is, but instead
as its base type. This allows you to write code that doesn’t depend on
specific types. In the shape example, functions manipulate generic shapes
without respect to whether they’re circles, squares, triangles, or some
shape that hasn’t even been defined yet. All shapes can be drawn, erased,
and moved, so these functions simply send a message to a shape object; they
don’t worry about how the object copes with the message.
|
![t](t.gif) |
Il arrive qu'on veuille traiter un objet non en tant qu'objet du type
spécifique qu'il est, mais en tant qu'objet de son type de base. Cela permet d'écrire du code
indépendant des types spécifiques. Dans l'exemple de la forme géométrique, les fonctions manipulent
des formes génériques sans se soucier de savoir si ce sont des cercles, des carrés, des triangles
ou même des formes non encore définies. Toutes les formes peuvent être dessinées, effacées, et
déplacées, donc ces fonctions envoient simplement un message à un objet forme, elles ne se soucient
pas de la manière dont l'objet traite le message.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Such code is unaffected by the addition
of new types, and adding new types is the most common way to extend an
object-oriented program to handle new situations. For example, you can derive a
new subtype of shape called pentagon without modifying the functions that
deal only with generic shapes. This ability to extend a program easily by
deriving new subtypes is important because it greatly improves designs while
reducing the cost of software maintenance.
|
![t](t.gif) |
Un tel code n'est pas affecté par l'addition de nouveaux types, et ajouter
de nouveaux types est la façon la plus commune d'étendre un programme orienté objet pour traiter de
nouvelles situations. Par exemple, on peut dériver un nouveau type de forme appelé pentagone sans
modifier les fonctions qui traitent des formes génériques. Cette capacité à étendre facilement un
programme en dérivant de nouveaux sous-types est important car il améliore considérablement la
conception tout en réduisant le coût de maintenance.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
There’s a problem, however, with
attempting to treat derived-type objects as their generic base types (circles as
shapes, bicycles as vehicles, cormorants as birds, etc.). If a function is going
to tell a generic shape to draw itself, or a generic vehicle to steer, or a
generic bird to move, the compiler cannot know at compile-time precisely what
piece of code will be executed. That’s the whole point—when the
message is sent, the programmer doesn’t want to know what piece of
code will be executed; the draw function can be applied equally to a circle, a
square, or a triangle, and the object will execute the proper code depending on
its specific type. If you don’t have to know what piece of code will be
executed, then when you add a new subtype, the code it executes can be different
without requiring changes to the function call. Therefore, the compiler cannot
know precisely what piece of code is executed, so what does it do? For example,
in the following diagram the BirdController object just works with
generic Bird objects, and does not know what exact type they are. This is
convenient from BirdController’s perspective because it
doesn’t have to write special code to determine the exact type of
Bird it’s working with, or that Bird’s behavior. So
how does it happen that, when move( ) is called while ignoring the
specific type of Bird, the right behavior will occur (a Goose
runs, flies, or swims, and a Penguin runs or swims)?
|
![t](t.gif) |
Un problème se pose cependant en voulant traiter les types dérivés comme
leur type de base générique (les cercles comme des formes géométriques, les vélos comme des
véhicules, les cormorans comme des oiseaux, etc...). Si une fonction demande à une forme générique
de se dessiner, ou à un véhicule générique de tourner, ou à un oiseau générique de se déplacer, le
compilateur ne peut savoir précisément lors de la phase de compilation quelle portion de code sera
exécutée. C'est d'ailleurs le point crucial : quand le message est envoyé, le programmeur ne
veut pas savoir quelle portion de code sera exécutée ; la fonction dessiner peut être
appliquée aussi bien à un cercle qu'à un carré ou un triangle, et l'objet va exécuter le bon code
suivant son type spécifique. Si on n'a pas besoin de savoir quelle portion de code est exécutée,
alors le code exécuté lorsque on ajoute un nouveau sous-type peut être différent sans exiger de
modification dans l'appel de la fonction. Le compilateur ne peut donc précisément savoir quelle
partie de code sera exécutée, donc que va-t-il faire ? Par exemple, dans le diagramme suivant,
l'objet Contrôleur d'oiseaux travaille seulement avec des objets
Oiseaux génériques, et ne sait pas de quel type ils sont. Cela est pratique du
point de vue de Contrôleur d'oiseaux car il n'a pas besoin d'écrire du code
spécifique pour déterminer le type exact d'Oiseau avec lequel il travaille, ou le
comportement de cet Oiseau. Comment se fait-il donc que, lorsque
bouger() est appelé tout en ignorant le type spécifique de
l'Oiseau, on obtienne le bon comportement (une Oie court, vole ou
nage, et un Pingouin court ou nage) ?
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
|
![t](t.gif) |
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
The answer is the primary twist in
object-oriented programming: the compiler cannot make a function call in the
traditional sense. The function call generated by a non-OOP compiler causes what
is called early binding, a
term you may not have heard before because you’ve never thought about it
any other way. It means the compiler generates a call to a specific function
name, and the linker resolves this call to the absolute address of the code to
be executed. In OOP, the program cannot determine the address of the code until
run-time, so some other scheme is necessary when a message is sent to a generic
object.
|
![t](t.gif) |
La réponse constitue l'astuce fondamentale de la programmation orientée
objet : le compilateur ne peut faire un appel de fonction au sens traditionnel du terme. Un
appel de fonction généré par un compilateur non orienté objet crée ce qu'on appelle
une association prédéfinie, un terme que vous n'avez sans doute jamais entendu
auparavant car vous ne pensiez pas qu'on puisse faire autrement. En d'autres termes, le compilateur
génère un appel à un nom de fonction spécifique, et l'éditeur de liens résout cet appel à l'adresse
absolue du code à exécuter. En POO, le programme ne peut déterminer l'adresse du code avant la
phase d'exécution, un autre mécanisme est donc nécessaire quand un message est envoyé à un objet
générique.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
To solve the problem, object-oriented
languages use the concept of late
binding. When you send a message to an object, the code being called
isn’t determined until run-time. The compiler does ensure that the
function exists and performs type checking on the arguments and return value (a
language in which this isn’t true is called
weakly typed), but it
doesn’t know the exact code to execute.
|
![t](t.gif) |
Pour résoudre ce problème, les langages orientés objet utilisent le concept
d'association tardive. Quand un objet reçoit un message, le code appelé n'est pas
déterminé avant l'exécution. Le compilateur s'assure que la fonction existe et vérifie le type des
arguments et de la valeur de retour (un langage omettant ces vérifications est
dit faiblement typé), mais il ne sait pas exactement quel est le code à
exécuter.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
To perform late binding, Java uses a
special bit of code in lieu of the absolute call. This code calculates the
address of the function body, using information stored in the object (this
process is covered in great detail in Chapter 7). Thus, each object can behave
differently according to the contents of that special bit of code. When you send
a message to an object, the object actually does figure out what to do with that
message.
|
![t](t.gif) |
Pour créer une association tardive, Java utilise une portion spéciale de
code en lieu et place de l'appel absolu. Ce code calcule l'adresse du corps de la fonction, en
utilisant des informations stockées dans l'objet (ce mécanisme est couvert plus en détails dans le
Chapitre 7). Ainsi, chaque objet peut se comporter différemment suivant le contenu de cette portion
spéciale de code. Quand un objet reçoit un message, l'objet sait quoi faire de ce
message.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
In some languages (C++, in particular)
you must explicitly state that you want a function to have the flexibility of
late-binding properties. In these languages, by default, member functions are
not dynamically bound. This caused problems, so in Java dynamic binding
is the default and you don’t need to remember to add any extra keywords in
order to get polymorphism.
|
![t](t.gif) |
Dans certains langages (en particulier le C++), il faut préciser
explicitement qu'on souhaite bénéficier de la flexibilité de l'association tardive pour une
fonction. Dans ces langages, les fonctions membres ne sont pas liées dynamiquement par
défaut. Cela pose des problèmes, donc en Java l'association dynamique est le défaut et aucun mot
clef supplémentaire n'est requis pour bénéficier du polymorphisme.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Consider the shape example. The family of
classes (all based on the same uniform interface) was diagrammed earlier in this
chapter. To demonstrate polymorphism, we want to write a single piece of code
that ignores the specific details of type and talks only to the base class. That
code is decoupled from type-specific information,
and thus is simpler to write and easier to understand. And, if a new
type—a Hexagon, for example—is added through inheritance, the
code you write will work just as well for the new type of Shape as it did
on the existing types. Thus, the program is extensible.
|
![t](t.gif) |
Reprenons l'exemple de la forme géométrique. Le diagramme de la hiérarchie
des classes (toutes basées sur la même interface) se trouve plus haut dans ce chapitre. Pour
illustrer le polymorphisme, écrivons un bout de code qui ignore les détails spécifiques du type et
parle uniquement à la classe de base. Ce code est déconnecté des informations
spécifiques au type, donc plus facile à écrire et à comprendre. Et si un nouveau type - un
Hexagone, par exemple - est ajouté grâce à l'héritage, le code continuera de
fonctionner aussi bien pour ce nouveau type de Forme qu'il le faisait avec les
types existants. Le programme est donc extensible.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
If you write a method in Java (as you
will soon learn how to do):
|
![t](t.gif) |
Si nous écrivons une méthode en Java (comme vous
allez bientôt apprendre à le faire) :
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
void doStuff(Shape s) {
s.erase();
// ...
s.draw();
}
|
![t](t.gif) |
void faireQuelqueChose(Forme f) { f.effacer(); // ... f.dessiner(); }
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
This function speaks to any Shape,
so it is independent of the specific type of object that it’s drawing and
erasing. If in some other part of the program we use the doStuff( )
function:
|
![t](t.gif) |
Cette fonction s'adresse à n'importe quelle Forme, elle
est donc indépendante du type spécifique de l'objet qu'elle dessine et efface. Si nous utilisons
ailleurs dans le programme cette fonction faireQuelqueChose() :
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Circle c = new Circle();
Triangle t = new Triangle();
Line l = new Line();
doStuff(c);
doStuff(t);
doStuff(l);
|
![t](t.gif) |
Cercle c = new Cercle(); Triangle t = new Triangle(); Ligne l = new Ligne(); faireQuelqueChose(c); faireQuelqueChose(t); faireQuelqueChose(l);
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
The calls to doStuff( )
automatically work correctly, regardless of the exact type of the object.
|
![t](t.gif) |
Les appels à faireQuelqueChose() fonctionnent
correctement, sans se préoccuper du type exact de l'objet.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
This is actually a pretty amazing trick.
Consider the line:
|
![t](t.gif) |
En fait c'est une manière de faire très élégante. Considérons la
ligne :
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
doStuff(c);
|
![t](t.gif) |
faireQuelqueChose(c);
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
What’s happening here is that a
Circle is being passed into a function that’s expecting a
Shape. Since a Circle is a Shape it can be treated
as one by doStuff( ). That is, any message that
doStuff( ) can send to a Shape, a Circle can accept.
So it is a completely safe and logical thing to do.
|
![t](t.gif) |
Un Cercle est ici passé à une fonction qui attend une
Forme. Comme un Cercle est-une Forme,
il peut être traité comme tel par faireQuelqueChose(). C'est à dire qu'un
Cercle peut accepter tous les messages que faireQuelqueChose()
pourrait envoyer à une forme. C'est donc une façon parfaitement logique et sûre de
faire.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
We call this process of treating a
derived type as though it were its base type
upcasting. The name cast
is used in the sense of casting into a mold and the up comes from the
way the inheritance diagram is
typically arranged, with the base type at the top and the derived classes
fanning out downward. Thus, casting to a base type is moving up the inheritance
diagram: “upcasting.”
|
![t](t.gif) |
Traiter un type dérivé comme s'il était son type de base est
appelé transtypage ascendant, surtypage ou généralisation
(upcasting). L'adjectif ascendant vient du fait que dans un diagramme
d'héritage typique, le type de base est représenté en haut, les classes dérivées s'y rattachant par
le bas. Ainsi, changer un type vers son type de base revient à remonter dans le diagramme
d'héritage : transtypage « ascendant ».
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
|
![t](t.gif) |
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
An object-oriented program contains some
upcasting somewhere, because that’s how you decouple yourself from knowing
about the exact type you’re working with. Look at the code in
doStuff( ):
|
![t](t.gif) |
Un programme orienté objet contient obligatoirement des transtypages
ascendants, car c'est de cette manière que le type spécifique de l'objet peut être délibérément
ignoré. Examinons le code de faireQuelqueChose() :
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
s.erase();
// ...
s.draw();
|
![t](t.gif) |
f.effacer(); // ... f.dessiner();
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Notice that it doesn’t say
“If you’re a Circle, do this, if you’re a
Square, do that, etc.” If you write that kind of code, which checks
for all the possible types that a Shape can actually be, it’s messy
and you need to change it every time you add a new kind of Shape. Here,
you just say “You’re a shape, I know you can erase( )
and draw( ) yourself, do it, and take care of the details
correctly.”
|
![t](t.gif) |
Remarquez qu'il ne dit pas « Si tu es un Cercle, fais
ceci, si tu es un Carré, fais cela, etc... ». Ce genre de code qui vérifie
tous les types possibles que peut prendre une Forme est confus et il faut le
changer à chaque extension de la classe Forme. Ici, il suffit de dire :
« Tu es une forme géométrique, je sais que tu peux te dessiner() et
t'effacer(), alors fais-le et occupe-toi des détails spécifiques ».
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
What’s impressive about the code in
doStuff( ) is that, somehow, the right thing happens. Calling
draw( ) for Circle causes different code to be executed than
when calling draw( ) for a Square or a Line, but when
the draw( ) message is sent to an anonymous Shape, the
correct behavior occurs based on the actual type of the Shape. This is
amazing because, as mentioned earlier, when the Java compiler is compiling the
code for doStuff( ), it cannot know exactly what types it is dealing
with. So ordinarily, you’d expect it to end up calling the version of
erase( ) and draw( ) for the base class Shape,
and not for the specific Circle, Square, or Line. And yet
the right thing happens because of polymorphism. The compiler and run-time
system handle the details; all you need to know is that it happens, and more
important how to design with it. When you send a message to an object, the
object will do the right thing, even when upcasting is
involved.
|
![t](t.gif) |
Ce qui est impressionnant dans le code de
faireQuelqueChose(), c'est que tout fonctionne comme on le souhaite. Appeler
dessiner() pour un Cercle exécute une portion de code différente
de celle exécutée lorsqu'on appelle dessiner() pour un Carré ou
une Ligne, mais lorsque le message dessiner() est envoyé à une
Forme anonyme, on obtient le comportement idoine basé sur le type réel de la
Forme. Cela est impressionnant dans la mesure où le compilateur Java ne sait pas à
quel type d'objet il a affaire lors de la compilation du code de
faireQuelqueChose(). On serait en droit de s'attendre à un appel aux versions
dessiner() et effacer() de la classe de base
Forme, et non celles des classes spécifiques Cercle,
Carré et Ligne. Mais quand on envoie un message à un objet, il
fera ce qu'il a à faire, même quand la généralisation est impliquée. C'est ce qu'implique le
polymorphisme. Le compilateur et le système d'exécution s'occupent des détails, et c'est tout ce
que vous avez besoin de savoir, en plus de savoir comment modéliser avec.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Abstract base classes and interfaces
|
![t](t.gif) |
Classes de base abstraites et interfaces
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Often in a design, you want the base
class to present only an interface for its derived classes. That is, you
don’t want anyone to actually create an object of the base class, only to
upcast to it so that its interface can be used. This is accomplished by making
that class abstract using the abstract keyword. If anyone tries to
make an object of an abstract class, the compiler prevents them. This is
a tool to enforce a particular design.
|
![t](t.gif) |
Dans une modélisation, il est souvent souhaitable qu'une classe de base ne
présente qu'une interface pour ses classes dérivées. C'est à dire qu'on ne souhaite pas qu'il soit
possible de créer un objet de cette classe de base, mais seulement pouvoir surtyper jusqu'à elle
pour pouvoir utiliser son interface. Cela est possible en rendant cette classe abstraite
en utilisant le mot clef abstract. Le compilateur se plaindra si une tentative est
faite de créer un objet d'une classe définie comme abstract. C'est un outil
utilisé pour forcer une certain conception.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
You can also use the abstract
keyword to describe a method that hasn’t been implemented yet—as a
stub indicating “here is an interface function for all types inherited
from this class, but at this point I don’t have any implementation for
it.” An abstract method may be created only inside an abstract
class. When the class is inherited, that method must be implemented, or the
inheriting class becomes abstract as well. Creating an abstract
method allows you to put a method in an interface without being forced to
provide a possibly meaningless body of code for that method.
|
![t](t.gif) |
Le mot clef abstract est aussi utilisé pour décrire une
méthode qui n'a pas encore été implémentée - comme un panneau indiquant « voici une fonction
de l'interface dont les types dérivés ont hérité, mais actuellement je n'ai aucune implémentation
pour elle ». Une méthode abstract peut seulement être créée au sein d'une
classe abstract. Quand cette classe est dérivée, cette méthode doit être
implémentée, ou la classe dérivée devient abstract elle aussi. Créer une méthode
abstract permet de l'inclure dans une interface sans être obligé de fournir une
portion de code éventuellement dépourvue de sens pour cette méthode.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
The interface keyword takes the
concept of an abstract class one step further by preventing any function
definitions at all. The interface is a very handy and commonly used tool,
as it provides the perfect separation of interface and implementation. In
addition, you can combine many interfaces together, if you wish, whereas
inheriting from multiple regular classes or abstract classes is not
possible.
|
![t](t.gif) |
Le mot clef interface pousse le concept de classe
abstract un cran plus loin en évitant toute définition de fonction. Une
interface est un outil très pratique et très largement répandu, car il fournit une
séparation parfaite entre l'interface et l'implémentation. De plus, on peut combiner plusieurs
interfaces, alors qu'hériter de multiples classes normales ou abstraites est impossible.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Object landscapes and lifetimes
|
![t](t.gif) |
Environnement et durée de vie des objets
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
Technically, OOP is just about abstract
data typing, inheritance, and polymorphism, but other issues can be at least as
important. The remainder of this section will cover these
issues.
|
![t](t.gif) |
Techniquement, les spécificités de la programmation orientée objet se
résument au typage abstrait des données, à l'héritage et au polymorphisme, mais d'autres
particularités peuvent se révéler aussi importantes. Le reste de cette section traite de ces
particularités.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
One of the most important factors is the
way objects are created and destroyed. Where is the data for an object and how
is the lifetime of the object controlled? There are different philosophies at
work here. C++ takes the approach that control of efficiency is the most
important issue, so it gives the programmer a choice. For maximum run-time
speed, the storage and lifetime can be determined while the program is being
written, by placing the objects on the stack (these are sometimes called
automatic or scoped variables) or in the static storage area. This
places a priority on the speed of storage allocation and release, and control of
these can be very valuable in some situations. However, you sacrifice
flexibility because you must know the exact quantity, lifetime, and type of
objects while you're writing the program. If you are trying to solve a more
general problem such as computer-aided design, warehouse management, or
air-traffic control, this is too restrictive.
|
![t](t.gif) |
L'une des particularités les plus importantes est la façon dont les objets
sont créés et détruits. Où se trouvent les données d'un objet et comment sa durée de vie est-elle
contrôlée ? Différentes philosophies existent. En C++, qui prône que l'efficacité est le
facteur le plus important, le programmeur a le choix. Pour une vitesse optimum à l'exécution, le
stockage et la durée de vie peuvent être déterminé quand le programme est écrit, en plaçant les
objets sur la pile (ces variables sont parfois appelées automatiques ou de
portée) ou dans l'espace de stockage statique. La vitesse d'allocation et de libération est
dans ce cas prioritaire et leur contrôle peut être vraiment appréciable dans certaines situations.
Cependant, cela se fait aux dépends de la flexibilité car il faut connaître la quantité exacte, la
durée de vie et le type des objets pendant qu'on écrit le programme. Si le problème à résoudre est
plus général, tel que de la modélisation assistée par ordinateur, de la gestion d'entrepôts ou du
contrôle de trafic aérien, cela se révèle beaucoup trop restrictif.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
The second approach is to create objects
dynamically in a pool of memory called the heap. In this approach, you don't
know until run-time how many objects you need, what their lifetime is, or what
their exact type is. Those are determined at the spur of the moment while the
program is running. If you need a new object, you simply make it on the heap at
the point that you need it. Because the storage is managed dynamically, at
run-time, the amount of time required to allocate storage on the heap is
significantly longer than the time to create storage on the stack. (Creating
storage on the stack is often a single assembly instruction to move the stack
pointer down, and another to move it back up.) The dynamic approach makes the
generally logical assumption that objects tend to be complicated, so the extra
overhead of finding storage and releasing that storage will not have an
important impact on the creation of an object. In addition, the greater
flexibility is essential to solve the general programming
problem.
|
![t](t.gif) |
La deuxième approche consiste à créer les objets dynamiquement dans un pool
de mémoire appelé le segment. Dans cette approche, le nombre d'objets nécessaire n'est pas connu
avant l'exécution, de même que leur durée de vie ou leur type exact. Ces paramètres sont déterminés
sur le coup, au moment où le programme s'exécute. Si on a besoin d'un nouvel objet, il est
simplement créé dans le segment au moment où on en a besoin. Comme le stockage est géré de manière
dynamique lors de l'exécution, le temps de traitement requis pour allouer de la place dans le
segment est plus important que le temps mis pour stocker sur la pile (stocker sur la pile se résume
souvent à une instruction assembleur pour déplacer le pointeur de pile vers le bas, et une autre
pour le redéplacer vers le haut). L'approche dynamique fait la supposition généralement justifiée
que les objets ont tendance à être compliqués, et que le surcoût de temps dû à la recherche d'une
place de stockage et à sa libération n'aura pas d'impact significatif sur la création d'un objet.
De plus, la plus grande flexibilité qui en résulte est essentielle pour résoudre le problème
modélisé par le programme.
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |
|
![t](t.gif) |
![t](t.gif) |
![t](t.gif) |