Void-safe en Eiffel

Preview:

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