Quel code est le plus Swifty ?

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 ?


Réponses

  • CéroceCéroce Membre, Modérateur
    août 2016 modifié #2
    Une classe n'a pas d'intérêt si elle n'est pas mutable, alors autant utiliser une struct comme tu le fais.

    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.


  • 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. 




     


    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...  ??? 


  • Joanna CarterJoanna Carter Membre, Modérateur
    août 2016 modifié #4


    Une classe n'a pas d'intérêt si elle n'est pas mutable, alors autant utiliser une struct comme tu le fais.




     


    C'est ce que je pensais.


     




    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.




     


    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.


     




    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.




     


    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.


     




    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 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.




  • 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. 

  • CéroceCéroce Membre, Modérateur

    Tu n'aurais pas déclaré tes deux méthodes dans la classe mère, tu aurais pu compiler ton code.

    Certes, mais ce sont deux "template methods", destinées à  être surchargées. Si tu ne les déclares pas dans la classe mère, alors tu ne peux pas utiliser le polymorphisme avec UnitConverter qui serait le type générique.
  • FKDEVFKDEV Membre
    août 2016 modifié #7

    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.


  • CéroceCéroce Membre, Modérateur
    août 2016 modifié #8
    Si tu veux, il y a deux écoles:
    - 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.
     

    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.

    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" ? 


  • CéroceCéroce Membre, Modérateur
    août 2016 modifié #11

    Mais c'est "obligatoire" (conseillé) de définir le nom d'un Protocol par un adjectif (modifiable, imprimable) ? Nommer un Protocol "List" ?

    Tu as tout dit. Un protocole étant destiné à  décrire une capacité, on s'attend à  ce que soit un adjectif, mais ce n'est pas toujours faisable.
  • FKDEVFKDEV Membre
    août 2016 modifié #12


    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 ?




     


     


    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. 


  • Joanna CarterJoanna Carter Membre, Modérateur

    Enfin, j'ai réécrit les APIs de Measurement, totalement sans classes, en utilisant les protocol et les structs :



    public protocol UnitConverter
    {
    func baseUnitValue(fromValue value: Double) -> Double

    func value(fromBaseUnitValue baseUnitValue: Double) -> Double
    }


    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
    }
    }


    public struct UnitConverterReciprocal : UnitConverter
    {
    private let reciprocal: Double

    public init(reciprocal: Double)
    {
    self.reciprocal = reciprocal
    }

    public func baseUnitValue(fromValue value: Double) -> Double
    {
    return reciprocal / value
    }

    public func value(fromBaseUnitValue baseUnitValue: Double) -> Double
    {
    return baseUnitValue * reciprocal
    }
    }


    public protocol Unit
    {
    var symbol: String { get }
    }


    public protocol ConvertibleUnit : Unit
    {
    var converter: UnitConverter { get }

    static var baseUnit : Self { get }
    }


    public struct LengthUnit : ConvertibleUnit
    {
    public let symbol: String

    public let converter: UnitConverter

    private struct Symbol
    {
    static let kilometers = "km"
    static let meters = "m"
    static let feet = "ft"
    static let miles = "miles"
    static let yards = "yds"
    }

    private struct Coefficient
    {
    static let kilometers = 1000.0
    static let meters = 1.0
    static let feet = 0.3048
    static let miles = 1609.34
    static let yards = 0.9144
    }

    public static var kilometers: LengthUnit
    {
    return LengthUnit(symbol: Symbol.kilometers, converter: UnitConverterLinear(coefficient: Coefficient.kilometers))
    }

    public static var meters: LengthUnit
    {
    return LengthUnit(symbol: Symbol.meters, converter: UnitConverterLinear(coefficient: Coefficient.meters))
    }

    public static var feet: LengthUnit
    {
    return LengthUnit(symbol: Symbol.feet, converter: UnitConverterLinear(coefficient: Coefficient.feet))
    }

    public static var miles: LengthUnit
    {
    return LengthUnit(symbol: Symbol.miles, converter: UnitConverterLinear(coefficient: Coefficient.miles))
    }

    public static var yards: LengthUnit
    {
    return LengthUnit(symbol: Symbol.yards, converter: UnitConverterLinear(coefficient: Coefficient.yards))
    }

    public static var baseUnit : LengthUnit
    {
    return LengthUnit.meters
    }
    }


    public struct FuelEfficiencyUnit : ConvertibleUnit
    {
    public let symbol: String

    public let converter: UnitConverter

    private struct Symbol
    {
    static let litersPer100Kilometers = "L/100km"
    static let milesPerImperialGallon = "mpg"
    static let milesPerGallon = "mpg"
    }

    private struct Coefficient
    {
    static let litersPer100Kilometers = 1.0
    static let milesPerImperialGallon = 282.481
    static let milesPerGallon = 235.215
    }

    public init(symbol: String, reciprocal: Double)
    {
    self.symbol = symbol

    self.converter = UnitConverterReciprocal(reciprocal: reciprocal)
    }

    public static var litersPer100Kilometers: FuelEfficiencyUnit
    {
    return FuelEfficiencyUnit(symbol: Symbol.litersPer100Kilometers, reciprocal: Coefficient.litersPer100Kilometers)
    }

    public static var milesPerImperialGallon: FuelEfficiencyUnit
    {
    return FuelEfficiencyUnit(symbol: Symbol.milesPerImperialGallon, reciprocal: Coefficient.milesPerImperialGallon)
    }

    public static var milesPerGallon: FuelEfficiencyUnit
    {
    return FuelEfficiencyUnit(symbol: Symbol.milesPerGallon, reciprocal: Coefficient.milesPerGallon)
    }

    public static var baseUnit: FuelEfficiencyUnit
    {
    return FuelEfficiencyUnit.litersPer100Kilometers
    }
    }


    public struct Measurement<UnitType : ConvertibleUnit>
    {
    var value: Double

    let unit: UnitType

    public init(value: Double, unit: UnitType)
    {
    self.value = value

    self.unit = unit
    }

    public func canBeConverted<TargetUnit : ConvertibleUnit>(to unit: TargetUnit) -> Bool
    {
    return unit is UnitType
    }

    public func converting<TargetUnit : ConvertibleUnit>(to unit: TargetUnit) -> Measurement<TargetUnit>
    {
    if !canBeConverted(to: unit)
    {
    fatalError("Unit type not compatible")
    }

    let baseUnitValue = self.unit.converter.baseUnitValue(fromValue: value)

    let convertedValue = unit.converter.value(fromBaseUnitValue: baseUnitValue)

    return Measurement<TargetUnit>(value: convertedValue, unit: unit)
    }
    }
  • C'est élégant.


    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.
Connectez-vous ou Inscrivez-vous pour répondre.