Quel code est le plus Swifty ?
Joanna Carter
Membre, Modérateur
En jouant avec les nouveaux APIs iOS10, j'en voulais reproduire certains.
Suivant le code que j'ai trouvé dans le repo GitHub :
public class UnitConverter
{
public func baseUnitValue(fromValue value: Double) -> Double
{
return value
}
public func value(fromBaseUnitValue baseUnitValue: Double) -> Double
{
return baseUnitValue
}
}
public class UnitConverterLinear : UnitConverter
{
private let coefficient: Double
private let constant: Double
public init(coefficient: Double, constant: Double)
{
self.coefficient = coefficient
self.constant = constant
}
public convenience init(coefficient: Double)
{
self.init(coefficient: coefficient, constant: 0)
}
public override func baseUnitValue(fromValue value: Double) -> Double
{
return value * coefficient + constant
}
public override func value(fromBaseUnitValue baseUnitValue: Double) -> Double
{
return (baseUnitValue - constant) / coefficient
}
}
Ou, selon mes instincts et les recommandations Apple :
public protocol UnitConverter
{
func baseUnitValue(fromValue value: Double) -> Double
func value(fromBaseUnitValue baseUnitValue: Double) -> Double
}
extension UnitConverter
{
func baseUnitValue(fromValue value: Double) -> Double
{
return value
}
func value(fromBaseUnitValue baseUnitValue: Double) -> Double
{
return baseUnitValue
}
}
public struct UnitConverterLinear : UnitConverter
{
private let coefficient: Double
private let constant: Double
public init(coefficient: Double, constant: Double)
{
self.coefficient = coefficient
self.constant = constant
}
public init(coefficient: Double)
{
self.init(coefficient: coefficient, constant: 0)
}
public func baseUnitValue(fromValue value: Double) -> Double
{
return value * coefficient + constant
}
public func value(fromBaseUnitValue baseUnitValue: Double) -> Double
{
return (baseUnitValue - constant) / coefficient
}
}
Qu'en pensez-vous ?
Connectez-vous ou Inscrivez-vous pour répondre.
Réponses
Par ailleurs, un "code smell" personnel est que les méthodes ne doivent pas être abstraites: je sais bien que dans l'usage courant, on crée des "templates methods" qui sont destinées à être surchargées. Mais dans mon code, j'en étais arrivé à lever une exception qui disait "cette méthode est abstraite, elle doit être implémentée par les classes filles".
Aujourd'hui j'ai donc une règle simple: tout méthode abstraite doit faire partie d'un protocole. Comme ça, pas besoin de fournir une implémentation par défaut, qui de toute façon n'a aucun sens.
Dans l'exemple d'origine, la classe UnitConverter ne sert à rien: les méthodes sont implémentées parce qu'il le faut pour plaire au compilateur, mais elles renvoient la valeur non convertie. UnitConverter ne sera donc jamais utilisée.
Du coup, classe ou struct, j'aurais de toute façon créé un protocole UnitConversion comme tu l'as fait. Par contre, je suis d'avis de retirer son extension, et donc ne pas fournir d'implémentation par défaut pour ses deux méthodes.
Je ne comprends pas pourquoi le compilateur obligerait l'implémentation des méthodes baseUnitValue et value dans la classe UnitConverter. En effet cette dernière n'est l'implémentation d'aucun Protocol... Il y a peut être une subtilité qui m'échappe... ???
C'est ce que je pensais.
De mon avis, c'est beaucoup mieux d'utiliser un protocole dont c'est impossible de rater l'implémentation dans les classes qui l'adoptent.
J'ai implémenté les méthodes parce que je voulais "remplacer" le code dans le repo en évitant les classes mais, comme tu dis, l'implémentation dans l'extension n'est pas vraiment nécessaire, sauf si on voulait fournir un comportement par défaut ; du coup, on pourrait supprimer l'extension.
C'est nécessaire de déclarer tous le méthodes, que l'on veuille surcharger, dans la classe de base. Sinon, c'est pas possible de les appeler sur les instances des sous-classes tenues dans les références de la classe de base. Et, pour ça, il faut, au moins une méthode vide dans une classe, ou mieux, mettre les définitions dans un protocole.
Je suis d'accord avec toi. Mais je vois pas en quoi, dans ce cas précis, c'est une obligation du compilateur. Tu n'aurais pas déclaré tes deux méthodes dans la classe mère, tu aurais pu compiler ton code.
Mise à part cet élément, je suis en total accord avec vos analyses respectives.
En dépit de la mode, je pense que, dans ce cas, l'utilisation d'une classe de base est plus adaptée, et donc l'utilisation de classes dérivées pas de structs.
Ta famille de classe est hyper-spécialisée, on peut dire que UnitConverterLinear EST un UnitConverter plutôt qu'elle est capable de se comporter comme un UnitConverter. Si c'était un vrai projet, il y aurait sans doute l'existence de caractéristiques communes à tous les UnitConverter comme des unités. Une unité source et destination font partie de ce qu'est un UnitConverter. Il y a donc du sens à avoir une classe de base capable de stocker les données constantes/variables qui font un UnitConverter.
Ensuite l'implementation des conversions et le stockage des données/constantes nécessaires à la conversion ont leur place dans les classes dérivées.
Cela n'empêche pas d'ajouter un protocol par la suite si la fonctionnalité UnitConverter doit-être étendue à des familles de classes/structs qui ne dérivent pas de la classe de base.
C'est vrai qu'on a pas besoin dans ce cas du passage par référence des classes dans ce cas mais en terme de sécurité, le fait qu'on puisse utiliser des constantes initialisés dans l'init dans les classes aussi (let) offre un compromis suffisant dans ce cas.
- celle des classes, qui veut que Voiture hérite de Véhicule.
- celle des "traits" qui veut que Voiture se conforme au protocole "Roulable".
Pour faire un parallèle, c'est comme avoir un fichier qui est un scan d'une facture émise par le centre des impôts.
1) Dans quel dossier de mon disque dur dois-je le ranger ? Tu finis par créer une hiérarchie de dossiers Perso/Impôts/Scans/Factures/. Mais peut-être que Perso/Factures/Scans/Impôts/ serait mieux ? ça dépend des autres scans qu'on a déjà , en fait.
2) Une autre organisation consiste à tout balancer dans un gros dossier Perso/ et "tagger" les fichiers.
Moi, ce qui me déplait dans l'héritage, c'est qu'il aboutit à une organisation "taxonomique", qui en pratique fonctionne mal, comme dans le cas n°1. Alors, tu vas me dire qu'il faut pas hériter trop profondément, non plus.
C'est pour cela que l'approche 2) me semble plus pertinente. En gros, utiliser au maximum les protocoles parce que m'évite de réfléchir à la taxonomie.
J'ai construit un truc avec mes Lego, hop, j'ajoute quatre roues, c'est une voiture. Peu importe si c'était un véhicule avant, ou pas.
Toutefois, je trouve qu'un gros problème des protocoles est la difficulté à les nommer. ça finit en protocole UnitConversion et une classe ou struct UnitConversionImpl, faute de meilleur nom.
Pour l'instant, je ne sais pas encore si c'est une mode ou une tendance de fond... D'un côté, les classes nous rendent de bons services depuis longtemps. De l'autre, j'ai du mal à gérer cette complexité, aujourd'hui que tout semble asynchrone.
Il y a une redécouverte de Lisp et des langages fonctionnels. Je vous invite à regarder cette présentation par l'auteur de Clojure:
https://www.infoq.com/presentations/Are-We-There-Yet-Rich-Hickey
On n'est pas obligé d'être entièrement d'accord, mais ça donne à réfléchir.
Moi j'aime bien les 2. Dans le seul contexte que je vois j'ai pas d'à priori négatif même si j'ai une préférence pour les protocol. De toute façon c'est un débat qui a été posé depuis Swift 1.2 ou 2.0 quand y'a eu l'évolution des protocol. Doit-on privilégier les Classes ou les protocols ?
Merci Céroce pour ta réponse !
Mais c'est "obligatoire" (conseillé) de définir le nom d'un Protocol par un adjectif (modifiable, imprimable) ? Nommer un Protocol "List" ?
Il faut utiliser les deux. Cela tombe bien les deux sont compatibles.
Depuis java, les protocol (ou interfaces), ont pris en charge une partie du polymorphisme à la place des classes. Ils sont plus purs, mais les classes ont l'avantage de permettre de factoriser du code.
Il faut être pragmatique, si l'héritage permet de factoriser du code, alors on l'utilise, sinon on l'utilise pas.
Aujourd'hui avec les extensions de protocols, les protocols permettent eux aussi un partage de code.
Il reste aux classes le partage de code liées à des données internes.
Surtout il faut réfléchir sur des cas concrets pas sur des cas théoriques comme celui présenté ici. Les cas théoriques on les utilise pour apprendre pas pour décider.
Quand je parle de "mode", c'est qu'il y a le risque de se décider par avance, en fonction de ce qui est en vogue. Du coup, on aborde les problèmes en se demandant comment utiliser tel ou tel feature. Et non pas objectivement en cherchant le meilleur moyen de résoudre le problème en fonction des outils disponibles.
Par rapport aux nouvelles features, la question devrait être :"si j'utilise tel nouvelle feature plutôt que ce je connais, cela va-t-il améliorer mon code ? Le rendre plus évolutif, plus maintenable, plus rapide à exécuter, plus rapide à développer ?"
Quand on ne voit pas de différence majeure, on peut soit rester sur ce l'on connait, soit en profiter pour utiliser de nouvelle feature dans un but d'apprentissage.
Pour revenir sur le cas théorique présenté, si j'extrapole vers un cas pratique où on aurait plusieurs UnitConverter, je pense que tous les UnitConverter vont être assez proches et vont surement devoir partager des caractéristiques, donc je pense que la famille est des UnitConverter est une bonne candidate pour l'héritage.
Enfin, j'ai réécrit les APIs de Measurement, totalement sans classes, en utilisant les protocol et les structs :
En fait, avec les structs, on remplace la dérivation par la composition. Du coup on est obligé d'en savoir un peu plus sur le fonctionnement interne des objets.
C'est une manière de penser différente à acquérir.
Pour ceux qui maitrisent la POO classique, je ne sais pas ce que cela apporte.
D'une certaine manière c'est un retour à une certaine façon de faire du C qui emboitait les structs dans les structs.