Upload
bmarchal
View
590
Download
1
Embed Size (px)
DESCRIPTION
Void-safe augmente la stabilité et la fiabilité du logiciel tout en diminuant le travail pour le développeur ! Une opération totalement gagnante. Le langage de programmation Eiffel offre une excellente mise en œuvre du Void-safe mais certains des principes présentés ici s'appliquent à tous les langages.
Citation preview
VOID-SAFEEn Eiffel
Copyright 2012, Group SBenoît Marchal, Paul-Georges Crismer @declencheur @pgcrismer
Cette présentation est distribuée sous licence CC-BYPour les conditions d’utilisation, consulterhttp://creativecommons.org/licenses/by/2.0/be/
Agenda
Qu’est-ce le Void-safe ?
Nouveaux éléments de langage
En pratique
Nouveaux projets
Conversion d’un projet
Montrez-moi Void !
Heu… sérieusement :-)
durant l’exécution d’une application, une référence est
soit attachée à un objet
create a_domain.make
soit Void
a_domain := Void
Banal
synonymes dans tous les langages
SQL, JavaScript, Java, C# : null
Lisp, Pascal : nil
VB : nothing
C/C++ : Ø ou null
Parce que c’est utile
Mais
l’appel sur une référence non attachée/void
erreur d’exécution
plus fréquent dans un langage orienté-objet
les objets sont accessibles via des références
que dans un langage procédural
où (hormis C) les pointeurs étaient moins utilisés
Difficile à trouver
évidemment, on n’écrit pas
Void.do_something
create a_domain.make -- a_domain attachée
a_range := Void -- a_range détachée
a_space.lots_of_complex_stuff
a_space… heu… joker !
Similaire à ÷Ø
en calcul sur des entiers
on a besoin du zéro
on a besoin de diviser des entiers
mais il ne faut jamais diviser par zéro
c’est une erreur à l’exécution
mais… plus d’appels sur références que de divisions
De même, Void
Void
indispensable
source d’erreur difficile à détecter
références fréquentes dans un système orienté-objet
A propos…
appel sur Void déclenche une exception
qu’on pourrait intercepter
mais il est plus simple de tester la référence
d’où les innombrables assertions
a_param /= Void
et le bogue courant c’est d’oublier un test
Avoid a void
“That case occurs in system-oriented parts of programs, typically in libraries that implement fundamental data structures in the more application-oriented parts of a program, void references are generally unnecessary.[…] confine void references to specific parts of a system, largely preserving the application-oriented layers from having to worry about the issues [of void references]”
Concrètement
ACCOUNT: 345 PERSON: John
PERSON: JackACCOUNT: 123
ACCOUNT: 567
owner
owner
Void ici signifie“je ne le connais pas.”
owner
Et pourtant
ACCOUNT: 345 PERSON: John
PERSON: JackACCOUNT: 123
ACCOUNT: 567 PERSON: Unknown
is_nogood: False
is_nogood: False
is_nogood: True
owner
owner
owner
Ou encore
ACCOUNT: 345 REAL_PERSON: John
REAL_PERSON: JackACCOUNT: 123
ACCOUNT: 567 UNKNOWN_PERSON
<<deferred>>PERSON
REAL_PERSON UNKNOWN_PERSON
owner
owner
owner
Voire même
ACCOUNT: 345 PERSON: John
PERSON: JackACCOUNT: 123
ACCOUNT: 567 PERSON: Unknown
SHARED_UNKNOWN_PERSONShared_unknown: PERSON
is_unknown(person: PERSON): boolean
owner
owner
owner
{NONE}unknown_person
A propos du SQL Null
norme ISO : information manquante ou non applicable
complexe (logique à 3 valeurs, join,…)
mais il ne s’agit pas d’une référence, pas d’appel
souvent traduit par une référence détachable en Eiffel
sans doute pas la meilleure représentation
nous verrons quelques patterns alternatifs
Donc, Void
indispensable
source d’erreur difficile à détecter
appel sur une référence à Void
fréquent dans un système orienté-objet
à limiter aux parties techniques de l’application
EIFFEL VOID-SAFE
Void safety
renforcer le contrôle des types de référence
attached
detachable
cas particulier, pour les aspects techniques
certified attachment patterns
garantie du compilateur : plus d’appels sur Void
CAP
transforme un référence détachable en attachée
CAP de base
x.f(…) est void-safe si
x est un paramètre ou une variable locale
dans la portée d’un test d’attachement de x
n’est pas précédé par une assignation à x
Exemple de CAP
local* name: detachable STRINGdo* -- …* if name /= Void then* * name.append ("something else")* endend
Erreur VEVI
Variable is not properly set.
local* text: attached STRINGdo* text.append (" or something")end
Une solution possible
local* text: attached STRINGdo* text := "John"* text.append (" or something")end
Erreur VUTA
Target of the Object_call is not attached.
local* name: detachable STRINGdo* -- …* name.append (" or something")end
Une solution possible
local* name: detachable STRINGdo* -- …* if name /= Void then* * name.append (" or something")* endend
Erreur VUAR
Non-compatible actual parameter in feature call.
local* name: detachable STRING* text: attached STRINGdo* text := "nothing"* -- …* text.append (name)end
Une solution possible
local* name: detachable STRING* text: attached STRINGdo* text := "nothing"* -- …* if name /= Void then* * text.append (name)* endend
MICRO-EXERCICE
Créer un projet “Exo_VS”
Sans le compiler
Configurer le projet
2 34
1
Configurer le projet (bis)
5
Configurer le projet (ter)
6
Compiler
créer un projet “exo_vs”, sans le compiler
full-class checking? true
void safety: Complete void-safe
syntax: standard syntax
are types attached by default? true
base library: …base-safe.ecf
precompiled base: …base-safe.ecf
par clarté, on indique explicitement attached/detachable
normalement, on n’indique que detachable
complétez deux routines
pour les rendre void-safe et compiler :-)
notez que ce n’est pas la même erreur
bonus : pourquoi ?
Enoncé
Attaché
do_attached* local* * queen: attached STRING* do* * queen.mirror* * print (queen)* end
Détaché
do_detachable (language: detachable STRING)* do* * language.append (" is void-safe")* * print (language)* end
Proposition de solution
do_attached* local* * queen: attached STRING* do* * queen := "etihW wonS"* * queen.mirror* * print (queen)* end
do_detachable (language: detachable STRING)* do* * if language /= Void then* * * language.append (" is void-safe")* * * print (language)* * end* end
Proposition de solution
LANGAGENouvelles constructions
Agenda
Qu’est-ce le Void-safe ?
Nouveaux éléments de langage
En pratique
Nouveaux projets
Conversion d’un projet
VOID CLARITY
Un faux problème ?
pré-conditions de type /= Void assure déjà la sécurité
oui, pour du code existant
mais pour du nouveau code, réduit l’effort
moins à écrire
moins de bogues
augmente la lisibilité
En tous petits caractères
update_flag (a_document, a_file : STRING ; a_secretary : SELECT_CLASS)* require* * a_document_not_void : a_document /= Void* * a_file_not_void : a_file /= Void* * a_file_in_list : has_file (a_file)* * a_secretary_not_void : a_secretary /= Void* do* * -- …* end
lisible ?
Contrats logiciels
dans le contrat, on trouve
assertions métier, sémantiquement riches
a_file_in_list : has_file (a_file)
noyées dans… de la technique, un faux void-safe
a_file_not_void : a_file /= Void
Redonner de la lisibilité
update_flag (a_document, a_file : STRING ; a_secretary : SELECT_CLASS)* require* * a_file_in_list : has_file (a_file)* do* * -- …* end
Bénéfices
délègue au compilateur les considérations techniques
moins de code à écrire, déboguer, etc.
sécurité accrue, par rapport aux préconditions
une vraie documentation lisible et riche
maintenance, reprise du code simplifiée
LANGAGE
4 objectifs
statique : vérifiable à la compilation
général : applicable à tous les types
simple : à comprendre et raisonnable à implémenter
compatible : avec le langage et les applications
en pratique, ce n’est que partiel, il faut porter
Types attachés
qualifie l’attachement d’un type : attached/detachable
Void n’est permis que pour les références détachables
right: detachable LINKABLE[G]
attached est la valeur par défaut, on ne l’écrit pas
vérifier les paramètres du projet !
attention à l’ancien code, c’est un changement…
Un choix logique
local* name:* text:do* text := "nothing"* -- …* if name /= Void then* * text.append (name)* endend
attached STRING
mais inverse d’aujourd’hui
detachable STRING
Assignation
l’assignation préserve le caractère attaché
assigner un type attaché ou un paramètre attaché
uniquement depuis une référence attachée
text := "nothing"
if a_detachable /= Void then* text := a_detachableend
Initialisation
par défaut, une référence est initialisée à Void
il faut donner une valeur acceptable (erreur VEVI)
une variable doit avoir une valeur acceptable
variable locale, dans le corps de la routine
attribut, via le constructeur
CAP
certified attachment pattern
assurer la compatibilité
en intégrant des pratiques courantes
si elles sont sûres
en particulier, le test d’une variable locale /= Void
Exemple de CAP
local* name: detachable STRINGdo* -- …* if name /= Void then* * name.append ("something else")* endend
Autre exemple de CAP
local* current: detachable LIST_ITEM* -- …from* current := first_elementuntil* current = Void or else current.item.is_equal (sought)loop* current := l.rightend
CAP et pré-condition
do_detachable (name: detachable STRING)* require* * name_attached: name /= Void* do* * name.append (" is famous")* * print (text)* end
CAP et attributs
if x /= Void then* do_something* x.do_something_elseend
n’est pas void-safe si x est un attribut
multithreading : modification entre test et appel
monothread: do_something modifie l’attribut
Stable
impossible d’assigner Void à un attribut stable
donc une fois attaché, il le reste
stable x: detachable TYPE
permet d’utiliser le CAP avec des attributs
puisqu’il ne peut être détaché après le test…
sucre syntaxique pour une conversion
Test d’objet
if attached {TYPE} expression as a_local then* -- …* a_local.do_somethingend
teste le type de l’expression et l’assigne à local
remplace la tentative d’assignation ?=
restreint la portée de la variable locale au test
Versions simplifiées
if attached expression as local then* -- …* local.do_somethingend
if attached local then* -- …* local.do_somethingend
Et si on le “sait” ?
e.g., en combinant diverses pré-conditions
invariant : error implies attached message
report_error* require* * error_set: error* do* * -- message est attaché… pas pour le compilateur* end
Check… then… plus lisible
if attached message as a_message then* a_message.do_somethingelse* -- que mettre ici ? une erreur ?end
check attached message as a_message then* a_message.do_somethingend
Générique
un paramètre générique est-il attaché ou non ?
si nécessaire, on le précise lors de la déclaration
une déclaration de contrainte habituelle
class GENERIC_CLASS[T -> attached ANY]
Tableaux
les éléments d’un tableau sont initialisés à… Void
acceptable pour un type détachable
inacceptable pour un type attaché
make_filled (low, high: INTEGER; value: G)
make_empty et on fait grossir le tableau
si les indices progressent 1 par 1
OPTIONS DU PROJET
Options par cluster
cascade
Full class checkingPERSON
make_with_names (a_first, a_last)first_name: STRINGlast_name: STRING
TAXI_DRIVERmake
taxi: CAR
make* -- first_name & last_name ne sont pas attachés !* do* * create taxi.make" end
make_with_names (a_first, a_last: STRING)* do* * first_name := a_first* * last_name := a_last* end
Obligatoire en Void-safe
mais automatique en Eiffel 7.x
re-vérifie les features héritées dans le descendant
dans l’exemple, erreur VEVI
make_with_names (a_first, a_last: STRING)* do* * Precursor (a_first, a_last) * * create taxi.make* end
Void-safety
No void safety
compilation à l’ancienne, pas de contrôle
On demand void safety
vérifie l’initialisation des références attachées
Complete void safety
applique tous les contrôles
Void control
Void safety
le compilateur garantit l’absence d’appel sur Void
Void confidence
le programmeur a confiance dans son code
par contrat et/ou “On demand void safe”
choix réaliste et suffisant ?
Etat des lieux, Group S
2012
EiffelBase : Void-safe
EWF : Void-safe
ECLI/EPOM : Void-confident
Gobo : pas encore Void-safe (en cours)
EPOSIX : pas Void-safe
Donc, en pratique
on peut viser la void confidence
au moins jusqu’à la migration de Gobo
remplacer EPOSIX par EiffelBase, si possible
le projet doit être “On demand Void-safe”
et utiliser les nouveaux ECF
Syntax
Obsolete/transitional/standard syntax
note remplace indexing comme mot-clé
Provisional syntax
éléments en cours de normalisation
recommendation : Standard syntax
ISO 25436/ECMA 367
➡
Are types attached by default?
True, pour tout nouveau projet
conseillé pour un nouveau projet
False préserve l’ancien comportement
réservé aux conversions complexes
detachable
Type attachment
attached
EN PRATIQUENouveaux projets
Agenda
Qu’est-ce le Void-safe ?
Nouveaux éléments de langage
En pratique
Nouveaux projets
Conversion d’un projet
BOITE À OUTILS
De nouvelles habitudes
le void-safe est un outil
comme le système de type
comme les contrats logiciels
comme l’encapsulation
pour construire des systèmes plus robustes
Void-safe n’est pas un but en soi
Dans le code applicatif
minimiser les références détachables
pas un but de les éradiquer mais un moyen
mais il faut… se défaire de mauvaises habitudes
Void veut-il dire quelque chose : quoi ?
peut-on l’exprimer de façon plus claire ?
quelques patterns pour nous aider
Pattern du zéro pointé
ARRAY[PHOTO]CAMERAtook
count = 54
CAMERA ARRAY[PHOTO]took
count = 0
PHOTO
54
1
1
Void ici signifie
pas de photo donc zéro photo
soyons plus explicite encore
Référence détachable
CAMERAtook
à éviter
si la référence est détachable
if attached took as a_took then* accross a_took as i loop i.item.something endend
si la référence est attachée
accross took as i loop i.item.something end
toujours aussi correct mais plus lisible
zéro passage dans la boucle…
Quand l’appliquer ?
relation dont la cardinalité Ø-n
structure (tableau, liste,…), vide pour Ø
attention aux “faux null”
par exemple, dans la BD, la foreign key est nulle
Pattern de la fourmi
inherit ANY redefine default_create end
create default_create
feature {NONE} -- Constructor* default_create* * do* * * Precursor* * * create text.make_empty* * end
2
Référence attachée
lazy initialisation
on accepte l’initialisation par défaut (Void)
donc la référence doit être détachable
if attached text as a_text then* a_text.append (something)end
habituellement c’est une optimisation à priori
à éviter
Un compromis…Calcul (et donc bogues) Mémoire
make (a_text: STRING)* do* * default_create* * text.copy (a_text)* endmake_as_mirror (a_text: STRING)* do* * default_create* * text.copy (a_text.mirrored)* end
Et, bien entendu…
Choisir le défaut
chaîne vide
message informatif
“Nom inconnu”
données de test
“4200 0000 0000 0000”
voir aussi le pattern Iznogoud
Et pour les invariants…
confus, on n’est pas tenté de l’écrire ou de le lire
invariant* valid: card /= Void implies is_valid (card)
facile à écrire, lisible
le défaut est une carte de test, donc valide
invariant* valid: is_valid (card)
Quand l’appliquer ?
dès qu’une valeur par défaut raisonnable existe
attention aux “faux nulls”
par exemple, dans la BD, la colonne est nulle
coût
légère surconsommation mémoire
Pattern de la roue libre
feature -- Access* message: STRING* * attribute* * * create Result.make_empty* * end
initialise à une valeur par défaut
3
Attribut
si on l’initialise directement
message := "Hello world!"
le code de l’attribut ne sera pas exécuté
lazy initialisation automatique…
derrière le compilateur doit insérer des tests
donc c’est parfois plus coûteux que la fourmi
feature -- Access* message: STRING assign set_message* * attribute* * * create Result.make_empty* * endfeature -- Element change* set_message (a_message: STRING)* * do* * * message := a_message* * ensure* * * message_set: message = a_message* * end
Quand l’appliquer ?
similaire à la fourmi
dès qu’une valeur par défaut raisonnable existe
meilleur quand le coût de création est plus élevé
par exemple, une requête BD…
attention aux “faux nulls”
par exemple, dans la BD, la colonne est nulle
Pattern Iznogoud
pourquoi avoir une référence à Void
bogue : erreur d’initialisation ☞ Void-safe
lazy initialisation ☞ zéro pointé, fourmi, roue libre
valeur inconnue : Void a une sémantique métier
la plupart des routines ne font pas de différence
valeur par défaut et valeur inconnue
4
Vive la logique booléenne
ACCOUNT: 345 PERSON: John
PERSON: JackACCOUNT: 123
ACCOUNT: 567 PERSON: Unknown
is_nogood: False
is_nogood: False
is_nogood: True
owner
owner
owner
Inversons la charge
soit la valeur est utilisable, soit elle est inconnue
or la plupart des routines appliquent un défaut
if owner = Void then* print ("Unknown")else* print (owner.name)end
mais on doit répéter le test partout
la plupart des routines se contentent du défaut
name: STRING attribute Result := "Unknown" end
print (owner.name)
quelques routines traitent différemment le cas inconnu
if (owner.is_nogood) then* database.store_name_as_nullelse* database.store_name (owner.name)end
deferred class IZNOGOUD* feature {NONE} -- Constructor* * make_iznogoud* * * do* * * * is_nogood := True* * * ensure* * * * iznogoud: is_nogood* * * end* feature {ANY} -- Access* * -- default initialization is False* * is_nogood: BOOLEANend
Quand l’appliquer ?
extension des patterns précédents
dès qu’une valeur par défaut raisonnable existe
mais quelques routines ont un traitement spécial
erreur, avertissement, BD, etc.
peu d’algorithmes n’acceptent pas la valeur par défaut
mais il y en a dans la classe
Pattern de l’héritier maudit 5
ACCOUNT: 345 REAL_PERSON: John
REAL_PERSON: JackACCOUNT: 123
ACCOUNT: 567 UNKNOWN_PERSON
<<deferred>>PERSON
REAL_PERSON UNKNOWN_PERSON
owner
owner
owner
Quand l’appliquer ?
relation Ø-1
pour l’objet lié
des valeurs/traitements par défaut existent
variante plus intelligente du “Null object pattern”
inconvénient : deux classes supplémentaires
avantage : isole les créations par défaut
Pattern singleton orphelin 6
ACCOUNT: 345
STRING: JohnACCOUNT: 123
ACCOUNT: 567 STRING: Unknown
SHARED_UNKNOWN_PERSONshared_unknown: STRING
is_unknown(person: STRING): BOOLEAN
owner
owner
owner
{NONE}unknown_person
ACCOUNT: 789 owner
STRING: John
Quand l’appliquer ?
relation Ø-1
pour l’objet lié
des valeurs par défaut raisonnables existent
ou des traitements par défaut
difficile de modifier le graphe d’héritage
librairie ou lisibilité
Pattern de l’objet relation 7
OWNING
PERSON: JackACCOUNT: 123
ACCOUNT: 345 PERSON: John
OWNING
ACCOUNT: 567
Quand l’appliquer ?
relation Ø-1
on souhaite enrichir la relation
il n’y a pas de valeurs par défaut raisonnables
inconvénient
la navigation entre les 2 objets est indirecte
Pattern detachable
quand on ne peut pas travailler avec une valeur
inconnu ou inapplicable
de façon répétitive et très nombreuse
alors Eiffel offre une primitive :Void
le compilateur va garantir qu’on n’oublie pas un test
8
if touch.is_nogood then* touch.do_specialelse* touch.do_regularend
if touch /= Void then* touch.do_specialelse* touch.do_regularend
chou vert et vert chou, quand c’est fréquent
Quand l’appliquer ?
selon la fréquence de ces tests
si > 85% des algorithmes
avantage
le compilateur garantit qu’on oublie pas un test
EXERCICE
Table des matières
APPLICATION
la racine du système
IDEA
un concept dans une arborescence/Mind Map
nom, description
tableau d’enfants
Tout est détachable !
malheureusement
développeur n’a pas perçu le bénéfice du Void-safe
donc le code est difficile à lire, peu maintenable
votre mission
supprimer les detachables, test /= Void, etc.
en utilisant les 8 premiers patterns
APPLICATIONnote* description : "Void-safe exercice, as a TOC"* date : "$Date$"* revision : "$Revision$"
class* APPLICATION
create* make
feature {NONE} -- Initialization* make* * local* * * toc, void_safe, eiffel_void_safe,* * * micro_exercice, language,* * * practical_information, new_projects, options,* * * void_clarity, toolbox, exercice,* * * project_conversion : detachable IDEA* * do* * * create toc* * * toc.name := "Formation Void-safe"* * * create void_safe* * * void_safe.name := "Void-safe"* * * void_safe.description := "Eviter une erreur"* * * create eiffel_void_safe* * * eiffel_void_safe.name := "Eiffel"* * * create micro_exercice* * * micro_exercice.name := "Micro-exercice"* * * micro_exercice.description := "A corriger"* * * create language* * * language.name := "Langage"* * * language.description := "Nouveautés"* * * create practical_information* * * practical_information.name := "Pratique"* * * create new_projects* * * new_projects.name := "Les nouveaux projets"* * * create options* * * options.name := "Options"* * * options.description := "Option pour le projet"* * * create void_clarity* * * void_clarity.name := "Void-clarity"* * * create toolbox* * * toolbox.name := "Boîte à outils"
* * * toolbox.description := "9 patterns"* * * create exercice* * * exercice.name := "Exercice"* * * create project_conversion* * * project_conversion.name := "Conversions"* * * toc.add_child (void_safe)* * * toc.add_child (language)* * * toc.add_child (practical_information)* * * void_safe.add_child (eiffel_void_safe)* * * void_safe.add_child (micro_exercice)* * * practical_information.add_child (new_projects)* * * practical_information.add_child (project_conversion)* * * new_projects.add_child (options)* * * new_projects.add_child (void_clarity)* * * new_projects.add_child (toolbox)** * * new_projects.add_child (exercice)* * * toc.print_as_tree* * endend
IDEAnote* description: "One idea/word in a Mind Map."* date: "$Date$"* revision: "$Revision$"
class* IDEAfeature -- Access* name: detachable STRING assign set_name* description: detachable STRING* * * * assign set_description* children: detachable ARRAY [IDEA]
feature -- Display* print_as_tree* * do* * * print_as_tree_helper (0)* * endfeature -- Element change* set_name (a_name: detachable STRING)* * require* * * name_not_void: a_name /= Void* * do* * * name := a_name* * ensure* * * name_set: name = a_name* * end* set_description (a_description: detachable STRING)* * do* * * description := a_description* * ensure* * * description_set: description = a_description* * end
* add_child (an_idea: detachable IDEA)* * require* * * idea_not_void: an_idea /= Void* * do* * * if not attached children then* * * * create children.make_empty* * * end* * * if attached children as the_children then* * * * the_children.force (an_idea,* * * * * * *
* * the_children.count + 1)* * * end* * ensure* * * children_not_void: attached children* * * child_added: attached children as* * * * the_children implies* * * * the_children.has (an_idea)* * endfeature {IDEA} -- Implementation* print_as_tree_helper (spaces: INTEGER)* * requireaspaces_positive: spaces >= 0* * do* * * print_spaces (spaces)* * * if attached name as a_name then* * * * print (a_name)* * * else* * * * print ("##unknown##")* * * end* * * print_spaces (spaces)* * * print ("%N")* * * if attached description as a_description then* * * * print_spaces (spaces)* * * * print (">>")* * * * print (a_description)* * * * print ("%N")* * * end* * * if attached children as the_children then* * * * across the_children as cursor* * * * loop* * * * * cursor.item.print_as_tree_helper (spaces + 3)* * * * end* * * end
* * end
* print_spaces (spaces: INTEGER)* * require* * * spaces_positive: spaces >= 0* * do* * * across 1 |..| spaces as i loop print (' ') end* * endend
EN PRATIQUEConversion
Agenda
Qu’est-ce le Void-safe ?
Nouveaux éléments de langage
En pratique
Nouveaux projets
Conversion d’un projet
Pré-conditions
parameter_not_void: parameter /= Void
devenues inutiles pour les paramètres attachés
laissez-les, au moins dans un premier temps
elles faciliteront d’autres aspects de la conversion
elles permettent de compiler en void-safe ou non
if … /= Void
le plus gros effort
indiquent que Void a un signification
ils sont toujours vrais…
donc on doit adapter un test
à analyser au cas par cas
Ajuster les tests
en utilisant nos patterns
ré-écrire le test
if stuff.is_nogood then
utiliser une valeur par défaut ou un tableau vide
ou le rendre detachable
préconditions /= Void, pas de problème
Compromis de contrat
préserver autant que possible le contrat
mais le défaut à changer entre attaché/détachable
à évaluer au cas par cas
pour l’appelant, il est plus facile que vous
retourniez des attachés
acceptiez des détachables, si Void était permis