Eviter la dérivation grâce aux blocks?
Bonjour,
Est-il possible d'éviter de dériver toute une classe si seul un comportement varie?
Exemple classique:
interface ClasseDeBase : NSObject
@property int num1, num2;
- (int) combine;
implementation ClasseDeBase
- (int) combine
{
return 0; // La classe de base n'effectue aucun calcul
}
interface ClasseAdditionne: ClasseDeBase
implementation ClasseDeBase
- (int) combine
{
return _num1 + _num2 // combine redéfinit la méthode d'origine
}
L'idée consisterait à ne pas dériver ClasseDeBase, mais à stocker l'opération dans un block (peut-être dans l'initialisation de l'instance de ClasseDeBase). Est-ce est réalisable, et si oui comment ?
Mon problème est que sinon, je vais devoir dériver autant de classes que j'ai d'opérations (qui diffèrent peu).
Question subsidiaire : ai-je un problème de design pour en arriver à poser cette question?
Merci de m'amener vos lumières.
Réponses
Quel problème vois-tu à "dériver toute une classe" ?
1/ Oui on peut déclarer une propriété de type "block"
2/ Difficile de dire s'il y a un problème de conception. Les blocs sont plutôt utilisés pour des comportements temporaires et/ou pour des objets à "courte" durée de vie (objets requête, tri, animation, ...). Le principal problème d'une propriété de type bloc est qu'elle n'est pas archivable, c'est la raison pour laquelle on ne les utilise que pour les objets temporaires qui n'ont pas à être sauvegardés entre deux exécutions de l'application.
La solution classique pour ton problème est d'utiliser le pattern stratégie.
Tu as 3 mecanismes a ta disposition :
-blocks
-delegate
-derivation
Pour un meme probleme, on peut souvent utiliser les trois types de mecanismes.
Les blocks et les delegate sont plutot la pour ajouter du traitement à certains points clés d'un traitement general fourni par un objet utilitaire.
La derivation est un moyen d'etendre une classe en y ajoutant des traitements complementaires.
Exemple :
Tu fournis une classe qui permet de telecharger des fichiers via http.
Tu vas permettre aux clients de ta classe d'ajouter du traitement lorsque le telechargement se termine via un block.
Dans ce cas la derivation marcherait mais c'est un peu too much pour juste ajouter 3 lignes de codes pour afficher une image telechargee.
Tu fournis une classe generale de telechargement sans viser de protocol précis.
Tu vas exiger de tes clients qu'ils dérivent la classe pour implémenter le telechargement selon leur protocole.
Là tu vas avoir besoin de donnees et de fonctions membres supplmentaire pour implementer ton protocol donc il est plus propre de creer une nouvelle classe pour encapsuler tout ça.
D'accord avec le 1/ mais pas avec ça.^
Son cas est plutot un cas de derivation simple ou on a besoin de surcharger une methode. Pas besoin de pattern pour ça !
Le pattern strategie ajoute un objet intermediaire qui declenche les traitements.
Le nombre de fichiers dans le projet, essentiellement. Par exemple j'ai dû dériver tout NSMatrix juste pour redéfinir -hitTest, c'est le genre de truc qui m'agace un peu, même si c'est la seule façon de faire.
J'imaginais plutôt quelque chose du style
interface ClasseDeBase
@property [block] *calcul;
...
- (int) combine
{
calcul
return valeur_du_calcul
}
Note: la durée de vie du block est celle de la session. Pas d'archivage, d'autant que l'un des paramètres est une random value.
Il y a deux solutions en tout cas que j'exécrerais, c'est la solution IF et la solution SWITCH dans la classe de base...
C'est une simple question d'appréciation. Beaucoup de classes qui se ressemblent toutes sauf pour un comportement bien spécifique ; le pattern stratégie est tout indiqué :
- avec des objets pour implémenter la stratégie s'il a besoin de la persistance
- avec des blocs pour implémenter la stratégie s'il n'a pas besoin de la persistance (il semble qu'il soit dans ce cas là )
Sans plus d'infos, je ne vois pas de contre-indication à l'utilisation d'un block stocké en property comme indiqué par jpimbert.
Excellent. Merci AliGator pour l'exemple et aux autres pour l'échange d'idées. Je savais bien qu'il y avait pas mal de potentialités dans ces blocks.
Je suis conscient que dans ce cas le code n'y gagne pas forcément en clarté et que ce n'est pas très OOP comme design, mais il s'agit peut-être d'une affaire de goût. Il se trouve que j'ai défini une classe de base qui me convient très bien et effectue un certain nombre de tâches, et pour laquelle deux "paramètres" vont varier: un type de calcul et la condition à laquelle le résultat de ce calcul est correct. Je peux donc définir deux blocs de code: 1. comment effectuer le calcul, 2. à quelle condition une méthode de check renvoie YES, et les passer comme paramètres.
En OOP, je créerais deux méthodes "Calcule" et "Verifie" qui ne font rien dans la classe de base et donc doivent être définies dans les classes dérivées. Est-ce là la fonction des protocoles?
J'ai une question subsidiaire: si j'envoie "self" dans le block ci-dessus, à quel objet va-t-il faire référence? Sera-ce le self de l'objet qui exécute le code ou de l'objet qui a défini le code? Y a-t-il un risque à utiliser self?
Pour une fois je ne suis pas d'accord avec l'implémentation d'Ali.
Je déclarerais plutôt combineBlock comme prenant deux paramètres de façon à rendre son code autonome ; en tout cas indépendant de l'endroit dans le code où le bloc a été défini.
Pour ma part je considère que cette implémentation est tout à fait OOP. Ce n'est pas parce que l'on ne fait pas d'héritage qu'on ne fait pas de l'OOP. Les Design Pattern sont de l'OOP et ils sont loin de tous faire appel à l'héritage.
Après c'est une affaire de culte religieux. Il y a la secte de ceux-qui-considèrent-que-dans-le-pattern-Stratégie-il-faut-implémenter-la-stratégie-forcément-sous-forme-d'une-instance-d'objet. Et des religions plus permissives qui acceptent des stratégies sous forme de bloc, ou même de simple fonction C.
D'ailleurs une simple fonction C serait peut-être plus indiquée dans ton exemple justement pour rendre la fonction la plus autonome possible (et être encore plus OOP qu'avec un bloc).
On appelle ça une méthode abstraite.
En C, pour éviter ça on utilise un tableau de pointeurs de fonctions.
On peut faire la même chose avec les blocks:
jpimbert, (+mpergand qui a répondu pendant que j'éditais)
Je suis venu à l'Objective-C sans passer par la case C ^_^ ni même C++ ^_^
Tant qu'il s'agit d'objets, de méthodes, de message et de propriétés, je me retrouve dans les eaux familières du Pascal OOP, à quelques menues différences près. Maintenant, le C m'a toujours paru un peu boueux et j'essaie (autant que faire se peut) d'éviter d'y salir le bas de ma toge... Mais manifestement, c'est comme les maths qu'on a laissées loin derrière soi, elles finissent toujours par vous rattraper
Tu as bien compris le but que je cherche à atteindre: donner un maximum de flexibilité et d'autonomie à certaines de mes classes, sans en faire des classes fourre-tout. Entre la superclasse qui peut tout faire et atteint ses 10'000 lignes de code, et les sous-sous-classes qui rechargent toute la hiérarchie en mémoire, je me suis dit que les blocks fournissaient une solution pratique pour doter ma classe de base de petites dérivations ponctuelles.
Maintenant, comment, façon Aligator, implémenter cette substitution de fonction C en lieu et place d'un block, cela relève pour moi du terrain miné car inconnu...
Heu en quoi le code n'est pas autonôme et indépendant de l'endroit dans le code où il est défini, dans mon cas ?
Le seul objet que mon block capture, c'est obj lui-même (il ne capture ni num1 ni num2, si c'est ça qui te faisait dire qu'il était dépendant de l'endroit de la déclaration). A la limite, on pourrait passer l'objet lui-même comme paramètre, pour éviter de le capture via le block mais plutôt le récupérer en paramètre, mais ça ne changerai pas grand chose...
C'est une bonne question, tu fais bien de te la poser. Un block ressemble + à une fonction C qu'à une méthode Objective-C. Dans ce sens, elle n'a pas de notion de "self" au sens d'une variable représentant l'objet block, ou l'objet qui possède le block ou quoi, non.
Quand tu utilises, dans le code du block, une variable qui existe / a été déclarée à l'extérieur du block (comme c'est le cas finalement pour "obj" tel que je l'utilise dans le code du block de mon exemple), le block "capture" cette variable. La règle ne fait pas exception pour self. Si tu utilisais "self "dans ton block, cela représenterai le même "self" que quand tu l'utilises à l'extérieur du block, donc l'objet dans lequel tu es en train de déclarer ton block (un ViewController, certainement, par exemple).
Et oui il y a un risque à utiliser self (le risque existe pas que pour "self", mais il se présente plus couramment avec "self" qu'avec d'autres variables), c'est de faire un retain cycle. En effet, si tu utilises "self" directement dans le code du block, le block va capturer cet objet "self" pour pouvoir y faire référence plus tard. En pratique, il va donc retenir "self" (car "self" est une référence "__strong"). Si jamais self retiens lui-même le block, directement ou indirectement, tu auras donc un retain cycle (self retiens le block et le block retiens self).
Par exemple, si ton objet self (disons un UIViewController) retiens ton objet obj (imagines que tu fais self.objet = obj avec objet étant une @property(retain) que tu utilises pour stocker et retenir ton objet obj), alors self va retenir obj qui va retenir le block... qui va retenir self si tu utilises self dans le block et qu'il le capture. Donc tu auras un retain cycle, donc une fuite mémoire car se retenant mutuellement, personne ne sera jamais désalloué.
En général, si tu as ton Xcode à jour et donc avec le dernier compilateur et n'a pas trifouillé les warnings pour désactiver des trucs, les compilateurs modernes savent maintenant souvent te prévenir de ce genre de risque de retain cycle dans ce cas, donc tu auras sans doute un warning. Mais bon dans tous les cas il faut bien comprendre ce risque et pourquoi il existe.
La solution pour contourner cela, c'est de faire une variable __weak pour référencer self (par exemple "__weak __typeof__(self) weakSelf = self;") et que ce soit cette variable __weak (variable nommée weakSelf dans mon exemple) que le block capture. Le but étant que le block ne la retienne pas la variable quand il va la capturer dans son contexte (alors qu'avec le __strong self il le ferait) et donc éviter le retain cycle.
Une fois à l'intérieur du code du block, il faudra alors utiliser la variable weakSelf en lieu et place de self pour que le block ne fasse référence qu'à weakSelf (la variable __weak donc non retenue) et ne risque pas de capturer self.
Mieux encore, dès le début du block en général on retransforme cette variable weakSelf à nouveau en variable __strong pour que le code du block utilise bien une variable __strong (qui ne risque pas de passer automatiquement à nil si l'objet disparaà®t entre temps pendant l'exécution du code du block).
Exemple :
Si j'écris ça comme ça, tu auras un retain cycle car self retient sa propriété self.objet, cet objet retient le block, et comme le block référence directement la variable self (qui est __strong), du coup le block va retenir self... donc tu as un cycle d'objets qui se retiennent ce qui pose problème.La solution est que le block ne capture pas self, mais une variable référençant self de façon __weak et pas __strong. Quitte ensuite dès le début du block, à retransformer cette variable __weak en variable __strong pour pas risquer qu'elle repasse à nil sans prévenir pendant le code du block :
Dans ce cas self retiens l'objet, l'objet retient le block... mais le block ne retiens pas self, car il n'y fait jamais référence. La seule variable externe au block et à laquelle le code du block fait référence, c'est weakSelf, et comme c'est une weak reference, le block ne va pas faire un retain dessus, il ne va pas avoir une strong reference dessus mais une weak reference. Donc le retain cycle est brisé.
NB : "__typeof__(x)" est une directive qui va être remplacé automatiquement à la compilation par le type de la variable x. Si self est un objet de classe TotoViewController, "__typeof__(self)" est synonyme d'écrire "TotoViewController*".
J'appelle ça du polymorphisme. Un mécanisme de base de l'OOP où un traitement peut-être défini ou précisé dans une classe dérivée.
Désolé de chipoter mais je n'aime pas l'utilisation exagérée des patterns.
Le pattern strategie a en plus une dimension dynamique : en fonction de conditions qui vont se présenter au runtime on va pouvoir changer de traitement dynamiquement.
Par exemple, un objet Server (le commandant) va faire appel à différents objets d'Encoding (les stratèges) en fonction du type de liaison. Un objet EncodingBinaire pour du ftp, un objet EncodingBase64 pour du http, etc.
Ces objets stratèges sont dérivés d'une classe de base abstraite Encoding et redéfinissent les méthodes Encode/Decode.
(héritage+polymorphisme)
Ce n'est pas très différent avec un pointeur de fonction :
Le gros avantage du block c'est de pouvoir être écrit à l'endroit où on en a besoin. Par conséquent le block doit avoir la capacité à "capturer" les variables locales qu'il utilise.
Ben là aussi c'est une question de religion, et c'est comme les goûts et les couleurs ça ne se discute pas.
C'est si simple de rendre le bloc indépendant que je ne priverais pas de le faire, ce qui permettrait de réutiliser le même bloc pour plusieurs instances différentes.
Ah oui tu veux dire que si j'avais à créer plusieurs objets ClasseDeBase utilisant le même block, je pourrais pas réutiliser le même block à chaque fois car ne passant pas l'objet en paramètre de mon block, il faudrait que j'utilise à chaque fois l'objet différent à l'intérieur du code de chaque block ?
Oui c'est pas faux. Dans ce cas je te l'accorde, c'est mieux de passer alors l'objet en paramètre du block. Pourquoi toi tu disais "un block prenant 2 paramètres" par contre, tu voyais quoi comme 2 paramètres ?
Moi du coup j'aurai adapté mon code comme ça :
Et ça résout donc le problème en permettant de passer le même block à plusieurs instances si tu as besoin. Et en plus si tu utilises le obj passé en paramètre dans ton block et non pas obj1 ou self.objet1 ou quoi, tu limites les risques de retain cycle puisque tu utilises un paramètre, pas une capture de variable.Note qu'au pire, si dans mon API j'ai pas prévu de passer de paramètre (comme dans ma première solution) tu peux toujours faire un code pseudo-générique quand même. C'est pas aussi idéal, je te l'accorde, mais je veux dire que si le concepteur de la classe n'a pas prévu cette flexibilité, tu peux contourner cela : Si le block "combineBlock" ne prend pas d'argument en paramètre, tu peux créer un block qui prend un ClasseDeBase* en paramètre, comme dans mon code ci-dessous, et tu fais :
Mais bon je te l'accorde en effet c'est pas clean, c'est mieux si l'API utilise un block qui passe l'objet en paramètre, c'est plus générique, je te l'accorde.Je voyais (int num1, int num2).
Par pur réflexe j'essaie de diminuer le couplage entre les objets, surtout lorsque c'est facile à faire.
Je n'aime pas que ClasseDeBase connaisse le type de bloc (inévitable puisque ClasseDeBase doit l'appeler) et que dans le même temps le bloc de code ait à connaà®tre ClasseDeBase (ici on peut l'éviter).
Ta solution avec un seul paramètre ClasseDeBase est effectivement plus simple. La mienne diminue le couplage, et améliore en principe la maintenabilité.
C'est aussi une très bonne idée de partir d'abord sur la solution la plus simple quitte à refactoriser plus tard si nécessaire. Je t'accorde volontiers que dans le cas présent la probabilité est très très faible pour que @berfis ait à utiliser le même bloc avec une nouvelle classe.
Moi je suis plutôt de la secte de-ceux-qui-découplent-à -fond-la-caisse-même-si-c'est-plus-compliqué.
Ah par contre passer num1 et num2 est bien moins flexible, car si tu veux que la méthode "combine" (et donc l'appel du block combineBlock) utilise d'autres propriétés de ton objet tu seras bloqué. En indiquant explicitement les 2 paramètres dans ton block, tu te limites à l'utilisation de ces 2 entiers pour faire l'opération de "combine", alors que si tu passes ClasseDeBase tu ouvres la possibilité de tout faire, si jamais ClasseDeBase a une autre propriété "float num3" et que tu veux l'utiliser, tu peux.
En fait, on est d'accord, ça dépend des goûts et des couleurs. Mais également du contexte en fait.
Là avec l'exemple qu'on utilise, entre les noms un peu génériques "ClasseDeBase" et "combine" on sait pas trop en vrai à quoi ça correspond comme objets, ce que c'est sensé représenter. Si le but c'est d'avoir des blocks qui vont traiter 2 nombres et en sortir une valeur, que ça sera toujours une opération avec 2 nombres en entrée et un nombre en sortie, et que ces blocks ont du sens dans le contexte hors de la classe... alors oui il faut passer les 2 nombres en paramètre. Si la méthode a un but plus générique de dire "fais un traitement sur l'objet, ce traitement pouvant être quelconque, dépendre des propriétés de l'objet ou pas"... alors juste passer un seul paramètre, l'objet , est plus pertinent.
En gros si le rôle du block c'est de forcément toujours faire une opération avec 2 entiers et rien d'autre, passer les 2 entiers.
Si le rôle du block c'est plutôt un fonctor, une tâche a exécuter, qui va peut-être en interne utiliser les 2 entiers en question, mais peut-être pas, bref que ça peut faire un peu n'importe quoi qu'on veut laisser + de latitude (par exemple que la méthode doit retourner obj.num1 si obj.even = YES et obj.num2 si obj.even = NO, ça utilise bien les 2 entiers que tu as en propriété de ton objet, num1 et num2, mais pas que...), alors passer l'objet est plus flexible.
Ca dépend vraiment des cas et de l'architecture la plus adaptée selon le contexte.
AliGator,
En fait, c'est vrai que l'exemple est très abstrait, je voulais juste avoir une idée de l'utilisation de blocks.
Dans la réalité, deux méthodes de la classe de base (un ViewController, c'est exact) doivent pouvoir exécuter un code quelconque, parce que j'aimerais pouvoir implémenter par la suite des comportements qui ne sont pas encore définis maintenant.
une première méthode effectue un calcul sur la base d'un nombre variable de paramètres, par exemple:
multiplication de deux nombres
multiplication de trois nombres
complément à 100 d'un nombre donné
racine carrée d'un nombre
etc.
une seconde méthode doit retourner YES ou NO suivant que les valeurs entrées, correctement associées, correspondent ou non à la solution (une propriété du contrôleur), par exemple:
return (num1 * num2 == solution);
return (num1* num2 * num3 == solution);
etc.
D'après ce que tu dis, il faut donc plutôt passer une référence à l'objet lui-même en prenant garde de ne pas créer de retain cycle, si j'ai bien compris.
Quitte a mettre toutes les classes dans le meme fichier si tu penses avoir trop de fichier.
Bonjour FKDEV,
Merci pour tes réponses, ma solution évidente était depuis le début la dérivation: une classe de base qui fait le boulot général et quelques méthodes abstraites aux points-clés, courtes, avec juste le bon nombre de paramètres qui ciblent les bonnes propriétés... j'adore ça et je suis dans ma zone de confort.
Mais je suis fasciné depuis quelque temps par les potentialités offertes par les blocks (qui ne sont pas encore dans ma zone de confort ils en sont même encore loin). L'idée d'exécuter des fragments de code qui sont des propriétés est, je l'avoue, assez tentante... même si, quelque part, c'est de la dérivation déguisée (sur le principe). Ils ont en tout cas une utilité évidente pour moi, c'est de définir le code au bon endroit, avec tout le contexte à disposition, et de l'exécuter ailleurs (ou plus tard) plutôt que de faire appel à des sélecteurs séparés à qui on doit envoyer le contexte pour qu'ils s'y retrouvent -- comme pour les détestables NSOpenPanel, les sheets etc.
Regrouper les classes dans le même fichier est une bonne idée, à la condition expresse que ma hiérarchie de classes réponde bien à mes propres critères (c'est d'ailleurs un bon test) à savoir la concision, sinon je vais me retrouver avec un fichier aussi agaçant à manipuler qu'un AppDelegate fourre-tout... Mais bon, il y a les pragma pour mettre un peu "d'ordre".
Merci à tous pour vos avis, idées et surtout exemples de code (qui valent de l'or) j'y ai appris beaucoup de choses que je réemploierai certainement un jour prochain!