Skip to content

Instantly share code, notes, and snippets.

@gbougeard
Created August 13, 2014 15:08
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gbougeard/0efc2d07497aa6363de2 to your computer and use it in GitHub Desktop.
Save gbougeard/0efc2d07497aa6363de2 to your computer and use it in GitHub Desktop.
Règles métiers et abstraction
Etant donnée la structure de données T suivante :
(id, type, montant) où type est soit AE (A emporter) ou SP (sur place).
Etant données les régles métiers suivantes pour un calcul de TVA :
RM1 : pout toute donnée T de type AE, la TVA est de 5%
RM2: pour tout donéne T de type SP la TVA est de 10%
1 - Comment implémentez-vous l'application de ces règles métiers sur
une liste de données T issue d'une base de données
2- Idem mais en entrée et en sortie on a du json
@ygrenzinger
Copy link

Je ne comprends pas la différence entre 1 et 2
switch (myData.type()) {
AE:
// TVA est de 5%
break;
SP:
//TVA est de 10%
break;
}

où veux tu en venir ? Est-ce pour finir par utiliser shapeless ? :D

J'imagine que tu veux passer par un case class ?

@gbougeard
Copy link
Author

j'ai posé la question à des devs java qui n'ont pas d'experience scala.
L'idée que je veux mettre en exergue est normalement indépendant du langage choisi.

@ygrenzinger
Copy link

Je suis intéressé par la version fonctionnelle ... surtout en Clojure :D

@bartavelle
Copy link

data TaxType = AE | SP

data T = T { _tId :: Integer
           , _tTp :: TaxType
           , _tAmnt :: Decimal -- il ne faut pas utiliser des doubles pour des prix
           }

data TTC = TTC { _ttcId :: Integer
               , _ttcAmnt :: Decimal
               }

applytax :: T -> TTC
applytax (T i t a) = TTC i $ a * case t of
                   AE -> 1.05
                   SP -> 1.1

applyTaxJSON :: BS.ByteString -> BS.ByteString
applyTaxJSON = _Array . traverse %~ toJSON . applytax . fromJSON

applyTaxDB :: Handler [TTC]
applyTaxDB = map (applytax . entityValue) <$> selectList [] []

@gbougeard
Copy link
Author

@bartavelle le problème là c'est que les règles métiers ne sont pas dissociées. Bon après c'est vrai que l'on pourrait considérer que dans ce cas là ce sont deux "sous-règles" de la règle métier de calcul de TVA

@bartavelle
Copy link

Je ne pense pas que ça soit deux règles distinctes, mais on peut les séparer comme ça :

data T a = AE { _tid :: Integer, _tamnt :: Decimal, _tt :: a } 
         | SP { _tid :: Integer, _tamnt :: Decimal, _tt :: a } 

data HT = HT
data TTC = TTC

makePrisms ''T
makeLenses ''T

taxAE :: T a -> T a
taxAE = _AE . _2 *~ 1.05

taxSP :: T a -> T a
taxSP = _SP . _2 *~ 1.05

computeTax :: T HT -> T TTC
computeTax = taxAE . taxSP . (tt .~ TTC)

@ygrenzinger
Copy link

C'est du Haskell ? :D

Plus sérieusement je ne comprends rien à ce code :(

@bartavelle
Copy link

C'est du Haskell, et c'est normal, ça utilise "lens".

@bartavelle
Copy link

Ca devrait être plus clair comme ça. On remarque qu'il n'est pas possible de mélanger des valeurs hors-taxe et TTC.

data T a = AE Integer Decimal
         | SP Integer Decimal

data HT
data TTC

taxAE :: Decimal -> Decimal
taxAE = (*) 1.05

taxSP :: Decimal -> Decimal
taxSP = (*) 1.10

computeTax :: T HT -> T TTC
computeTax (AE i v) = AE i (taxAE v)
computeTax (SP i v) = SP i (taxSP v)

@gbougeard
Copy link
Author

sealed trait Typ
case object AE extends Typ
case object SP extends Typ

case class Commande(id:Long, typ:Typ, montant:BigDecimal)

computeTax(c:Commande): Commande {
  c.typ match {
    case AE => c.copy(montant = c.montant * 1.05)
    case SP => c.copy(montant = c.montant * 1.10)
  }
}

@bartavelle
Copy link

Le soucis avec cette version c'est que tu peux faire ça :

computeTax(computeTax(Commande(1,AE,12)))

Normalement ça devrait interdit d'appliquer deux fois la TVA ;)

@gbougeard
Copy link
Author

J'avais pas capté tes types HT et TTC ;)

je me demande si ca passe ca :

type HT = Commande
type TTC = Commande

  def computeTax(c: HT): TTC = {
    c.typ match {
      case AE => c.copy(montant = c.montant * 1.05)
      case SP => c.copy(montant = c.montant * 1.10)
    }
  }

  val test = computeTax(computeTax(Commande(1,AE,12)))

ca compile :/

@divarvel
Copy link

avec des type alias tu n'amènes aucune garantie, c'est juste du renommage pour faire joli, mais le compilo ne voit pas la différence.

Pour comprendre comment la solution de bartavelle marche, regarde ce que sont les phantom types.

@gbougeard
Copy link
Author

 trait Montant
  trait HT extends Montant
  trait TTC extends Montant
  case class Commande[Mont <: Montant](id: Long, typ: Typ, montant: BigDecimal)

  def computeTax(c: Commande[HT]): Commande[TTC] = {
    c.typ match {
      case AE => c.copy(montant = c.montant * 1.05)
      case SP => c.copy(montant = c.montant * 1.10)
    }
  }

  val test = computeTax(computeTax(Commande[HT](1,AE,12))) 
//  type mismatch;
// [error]  found   : Commande[TTC]
// [error]  required: Commande[HT]

@andypetrella
Copy link

@divarvel les phantom types? Les type "en T" donc :-p.
Cela dit, pour être sérieux (un chouillat, promis), en Scala y a les type tags qui pourront aider.

@elecharny
Copy link

perso, j'ai une classe de base et deux classes héritées, avec une méthode calculeMontant() qui est implémentée dans chaque classe héritée. Je n'ai plus besoin de faire un truc style if type == SP then blah * 1.10, l'instance sait quoi faire. La classe de base est T, les deux classes qui en héritent sont SP et AE, la notion de type disparait complètement.

Approche objet, quoi...

C'est quoi le problème, en fait ?

@gbougeard
Copy link
Author

@elecharny Il n'y a pas de problème! C'est juste un débat d'idées.
Un autre pur javaiste m'a proposé quasiment la même chose que toi.
Perso ca me fait bizarre de représenter une règle métier en un objet car pour moi c'est une fonction.
Après si on prend en plus en compte la remarque de @bartavelle sur le fait que tu ne dois pas pouvoir appliquer le calcul de TVA sur un montant TTC, comment tu va le gérer? Avec un flag (booléen, enum)?

@elecharny
Copy link

L'approche objet, c'est l'encapsulation. C'est pour cela que la règel métier est intégrée à l'objet.

La sélection s'effectue sans avoir besoin de tester le type, ce qui est un avantage, parcequ'au cas où ton type change, tu n'as pas à réécrire ton code. Suppose que AE devient AF. Dans mon cas, je renomme la classe AE en AF, et c'est bon. Dans ton cas, tu vas devoir reprendre dans tout ton code les endroits où tu as :
case AE => // fait moi mal

Et comme tu peux avoir des dizaines de règles métier...

@abailly
Copy link

abailly commented Aug 14, 2014

Ce qui serait interessant comme probleme, c'est d'ajouter une regle metier: 90% du travail sur du code, c'est du travail sur du code existant. Comment les differentes solutions proposees se comportent si on ajoute un nouveau type de produit ? Une nouvelle regle ? Qu'est ce qu'il faut changer ?

@gbougeard
Copy link
Author

@manu , j'ai qu'un type AE, suffit de le renommer AF comme toi.

@elecharny
Copy link

@gbougeard Tu dois renommer AE en AF partout dans ton code. Pas moi : j'utilise l'interface T dans tous les cas, sans me préocupper de l'impact du rennomage de la classe fille.

@bartavelle
Copy link

Certes, mais c'est peu "coûteux" d'avoir à renommer le type partout dans la code car c'est une opération mécanique que l'on ne peut pas rater (sinon ça ne compilera pas).

Maintenant, avec l'approche objet, si la règle métier change, ou si il faut en ajouter une autre, la règle va être découpée et ré-implémentée pour chaque sous-classe. C'est à dire que pour la comprendre il va falloir lire autant de fichiers qu'il y a de classes. Tous les petits morceaux de code communs vont devoir être factorisés en autant de méthodes qui appartiendront à la classe mère et qui rendront la lecture difficile.

Finalement, je peux écrire ça :

chiffreAffaire :: [T a] -> Decimal
chiffreAffaire = sum . map getPrice

Qui va fonctionner pour le cas HT et le cas TTC (et qui ne permet pas de mélanger les valeurs HT et TTC). Le fait d'avoir un accesseur m'oblige en java à faire une méthode statique pour le cas HT et le cas TTC (enfin je crois ?).

@elecharny
Copy link

@bartavelle Tâche mécanique donc pénible. Bien sûr avec les IDE dont on dispose, c'est généralement pris en charge automatiquement, mais tu as parfois besoin de te taper la modif dans un éditeur de texte pur.

La règle métier est spécifique à chaque objet. Le fait d'avoir un gros switch en lieu et place de méthodes dédiées n'est aucunement un avantage, surtout en terme de lisibilité si ta méthode métier fait plus que prix x 1.05. Rien ne t'oblige par ailleurs à coller les parties communes dans la classe mère, mais si tu le fais, le risque d'oublier un des cas dans une des alternatives de ton switch est bien borné.

Non, tu n'est jamais obligé d'écrire un accesseur en Java. Les champs des classes peuvent être mis en visibilité public (beurk), protected (et accessibles par les classes héritées) voire en package protected (et accessibles par les classes de même package.

@bartavelle
Copy link

En haskell en tout cas et j'imagine dans plein d'autres langages le compilo t'indiques si tu oublies un cas dans un switch, donc tu ne peux pas oublier de cas ...

@bartavelle
Copy link

Ce que je veut dire par le fait que c'est une tâche mécanique, c'est qu'elle est certes rébarbative, mais surtout qu'il n'est pas possible de la rater (car sinon ça ne compilerait plus). L'aspect encapsulation n'est pas non plus une spécificité des langages objets, on peut reproduire exactement les mêmes propriétés en fonctionnel, en n'exportant pas les constructeurs en Haskell par exemple.

Le problème de cette approche par héritage est que ça ne compose pas. Disons que je veuille écrire une méthode qui calcule le chiffre d'affaire mensuel. Avec la solution que tu proposes, j'ai deux classes filles "sur place" et "à emporter", qui ont chacune une méthode getPrice et getPriceWithTaxes. Du coup j'ai le choix entre avoir :

  • une méthode statique chiffreAffaireMensuel(List<Commande> commandes, Bool avecOuSansTaxes), mais c'est franchement affreux
  • deux méthodes statiques chiffreAffaireMensuelSansTaxes et chiffreAffaireMensuelAvecTaxes

Admettons maintenant que je sois comptable et que je souhaite avoir le détail après que diverses autres taxes aient été prélevées, et le seul moyen de m'en sortir sans hurler c'est justement d'introduire un "objet fonction".

Et si je veux ajouter une taxe, je dois modifier les classes SP et AE. Donc je ne peux pas la livrer comme une bibliothèque sans y adjoindre des tonnes d'options qui permettront à l'utilisateur final de s'en servir dans un contexte non prévu.

Et comme on est vendredi, j'ajouterais que c'est pour cette raison que toutes les libs Java qui rencontrent du succès de retrouvent transformées en usines à gaz, justement en raison de ce problème de non composabilité.

@BernardNotarianni
Copy link

-module (tva).
-export ([calcul/1]).

calcul (List) ->
    [ tva (I,T,M) || {I,T,M} <- List ].

tva (_, Type, Montant) ->
    taux_tva (Type) * Montant.

taux_tva (ae) ->
    0.05;
taux_tva (sp) ->
    0.10.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment