BT

Diffuser les Connaissances et l'Innovation dans le Développement Logiciel d'Entreprise

Contribuez

Sujets

Sélectionner votre région

Accueil InfoQ Articles Le Cake Pattern de Scala, expliqué aux développeurs Spring

Le Cake Pattern de Scala, expliqué aux développeurs Spring

Favoris

Nombreux sont les développeurs Java qui se mettent à Scala. Lorsque l'on débute en Scala, il est parfois frustrant de ne pas savoir faire, à la manière Scala, des choses simples que l'on savait faire en Java : l'injection de dépendances par exemple. Spring est un framework d'injection de dépendances, que la plupart des développeurs Java connaissent bien.

Dans l'univers Scala, on entend souvent parler de Scalaz, Cake Pattern et autres SBT. En tant que débutant, le conseil que l'on reçoit souvent est de laisser ces concepts et outils de côté au départ, jugés comme trop complèxes pour un débutant. Dans cet article, nous expliquerons comment faire de l'injection de dépendances élégante en Scala, sans framework (bien que Spring soit utilisable), à l'aide du Cake Pattern. Nous démystifierons le Cake Pattern, qui est en réalité assez simple et pas si éloigné du fonctionnement de Spring.

A travers une application de type Twitter simplifiée à son minimum vital, nous prendrons le développeur Spring par la main pour démontrer qu'il est possible, sans aucun outil autre que le langage Scala lui-même, de construire l'équivalent d'un ApplicationContext. Celui-ci présente l'avantage d'être immuable, extensible et fortement typé.

Préambule

Nous allons construire une application qui utilisera les composants suivants :

  • UserTweetService : un service capable à la fois de manager des Users et des Tweets
  • UserRepository : le repository pour stocker les utilisateurs
  • TweetRepository: le repository pour stocker les tweets

Le modèle restera simple :

 

case class User(id: String, firstName: String, lastName: String)
case class Tweet(id: String, userId: String, content: String)

 

Le Cake Pattern est possible grâce au mixage de traits et aux self-type references. Les traits que nous allons mixer sont appelés des composants.

Interface d'un composant

Un composant est quelque chose d'extrêmement simple. Nous déclarons son interface comme suit :

 

trait UserRepositoryComponent {
  val userRepository: UserRepository
}

trait UserRepository {
  def createUser(user: User): User
  def getUser(id: String): User
}

 

Ici, notre UserRepositoryComponent est en fait un trait qui expose une valeur userRepository de type UserRepository. La notation Spring XML équivalente serait :

 

<bean id="userRepository" interface="repository.UserRepository"/>

 

A noter que le composant expose une interface et non une implémentation concrète, ce qui explique l'invention de l'attribut xml interface qui n'existe pas réellement dans Spring. Le trait UserRepositoryComponent est simplement la déclaration d'un bean avec un type et un nom donné, mais on ne connait pas encore l'implémentation réelle de ce bean exposé.

Implémentation d'un composant

Nous devons maintenant fournir une pseudo-implémentation de ce composant :

 

trait InMemoryUserRepositoryComponent extends UserRepositoryComponent {

  override val userRepository: UserRepository = new InMemoryUserRepository

  class InMemoryUserRepository extends UserRepository {
    val allUsers = new HashMap[String,User]
    // L'implémentation est volontairement simplifiée, en pratique, on ne retournerait probablement pas null
    override def createUser(user: User): User = allUsers.put(user.id,user).getOrElse(null)
    override def getUser(id: String): User = allUsers.get(id).getOrElse(null)
  }
}

 

Cette pseudo-implémentation du composant UserRepositoryComponent permet de spécifier quelle implémentation du UserRepository sera exposée. Nous utilisons le terme "pseudo" car l'implémentation du composant reste un trait, mais ce trait expose déjà son unique membre userRepository avec une instance de InMemoryUserRepository. Lorsque le trait est mixé/instancié, nous n'avons pas besoin de fournir une implémentation pour ce membre et nous pouvons simplement écrire :

 

val instance: InMemoryUserRepositoryComponent = new InMemoryUserRepositoryComponent { }

 

On utilise cette hiérarchie (interface/implémentation de composant) pour permettre d'utiliser des implémentations différentes du UserRepositoryComponent, qui exposent chacune des implémentations différentes du UserService.

A noter que cette fois-ci, l'implémentation InMemoryUserRepository est déclarée à l'intérieur du trait InMemoryUserRepositoryComponent, l'explication sera donnée plus tard.

Lorsque c'est cette implémentation du composant qui est choisie et instanciée, l'équivalent Spring est alors :

 

<bean id="userRepository" class="repository.impl.InMemoryUserRepository"/>

 

Finalisation de la couche repository

Nous pouvons donc définir nos deux repository de la manière suivante :

 

trait UserRepositoryComponent {
  val userRepository: UserRepository
}

trait UserRepository {
  def createUser(user: User): User
  def getUser(id: String): User
}

trait TweetRepositoryComponent {
   val tweetRepository: TweetRepository
 }

trait TweetRepository {
  def createTweet(tweet: Tweet): Tweet
  def getTweet(id: String): Tweet
  def getAllByUser(user: User): List[Tweet]
}

trait InMemoryUserRepositoryComponent extends UserRepositoryComponent {

  override val userRepository: UserRepository = new InMemoryUserRepository

  class InMemoryUserRepository extends UserRepository {
    val allUsers = new HashMap[String,User]
    override def createUser(user: User): User = allUsers.put(user.id,user).getOrElse(null)
    override def getUser(id: String): User = allUsers.get(id).getOrElse(null)
  }
}

trait InMemoryTweetRepositoryComponent extends TweetRepositoryComponent {

  override val tweetRepository: TweetRepository = new InMemoryTweetRepository

  class InMemoryTweetRepository extends TweetRepository {
    val allTweets = new HashMap[String,Tweet]
    override def createTweet(tweet: Tweet): Tweet = allTweets.put(tweet.id,tweet).getOrElse(null)
    override def getTweet(id: String): Tweet = allTweets.get(id).getOrElse(null)
    override def getAllByUser(user: User): List[Tweet] = allTweets.values.filter(user.id == _.userId).toList
  }
}

 

Lorsque ces deux composants sont instanciés, la déclaration Spring XML équivalente serait :

 

<bean id="userRepository" class="repository.impl.InMemoryUserRepository"/>
<bean id="tweetRepository" class="repository.impl.InMemoryTweetRepository"/>

 

A ce stade, il est déjà possible d'utiliser les deux composants Scala :

 

// On mixe les deux implémentations des deux composants
object RepositoriesApplicationContext extends InMemoryTweetRepositoryComponent with InMemoryUserRepositoryComponent

val userRepository: UserRepository = RepositoriesApplicationContext.userRepository
val tweetRepository: TweetRepository = RepositoriesApplicationContext.tweetRepository

// Ne compile pas: otherRepository n'existe pas dans RepositoriesApplicationContext
val otherRepository: OtherRepository = RepositoriesApplicationContext.otherRepository
// Ne compile pas car erreur dans le typage
val otherRepository2: UserRepository = RepositoriesApplicationContext.tweetRepository

userRepository.createUser( User("userId1","Sebastien","Lorber") )

 

Les deux traits InMemoryTweetRepositoryComponent et InMemoryUserRepositoryComponent sont mixés pour créer un object qui expose à la fois un userRepository et un tweetRepository. Le RepositoriesApplicationContext n'a pas besoin de fournir d'implémentation car les membres sont déjà fournis dans les traits mixés. La première différence qu'on remarque avec Spring est que l'utilisation de RepositoriesApplicationContext est typée. Aucun risque de se tromper de type lors de la récupération d'un bean. Aucun risque de demander à RepositoriesApplicationContext un bean qui n'existe pas : le compilateur nous l'interdit.

Pour le moment, nous avons vu la déclaration des composants, le mixage des composants pour créer l'équivalent d'un ApplicationContext, et l'utilisation de ce contexte, mais il n'y a toujours aucune notion de dépendance entre les composants.

Les self-type references

Avant d'aller plus loin, il est nécessaire de donner quelques explications sur les self-type references. Nous connaissons tous l'héritage :

 

trait A { ... }
trait B extends A { ... }

 

Dans ce cas, B est un A. B peut utiliser les membres visibles de A. Il est possible de caster B en A (implicitement) de la même manière qu'en Java. Cependant, il n'est pas possible d'écrire :

 

trait A extends B { ... }
trait B extends A { ... } 

 

Les self-type references ressemblent à première vue à de l'héritage :

 

trait A { ... }
trait B { self: A => 
  ...
}

 

Lorsqu'on utilise cette notation, on déclare que le type B a besoin de A pour être instancié. B peut alors voir les membres visibles de A, mais n'est pas pour autant un A. B a besoin d'être mixé avec un A :

 

object AInstance extends A
object BInstance extends B with A
object BInstance2 extends B // Ne compile pas car B n'est pas mixé avec un A!

 

Avec cette notation, il est également possible de définir une dépendance cyclique:

 

trait A { self: B => 
  ...
}
trait B { self: A => 
  ...
}

 

On ne peut donc plus créer d'instance de A sans la mixer avec B, et inversement. Les seules combinaisons possibles sont alors :

 

object AInstance extends A with B
object BInstance extends B with A

 

Application des self-type references aux composants

Nous allons maintenant créer le service UserTweetService qui va interragir avec nos repository. De la même manière, nous déclarons l'interface du composant :

 

trait UserTweetServiceComponent {
  val userTweetService: UserTweetService
}

trait UserTweetService {
  def createUser(user: User): User
  def createTweet(tweet: Tweet): Tweet
  def getUser(id: String): User
  def getTweet(id: String): Tweet
  def getUserAndTweets(id: String): (User,List[Tweet])
}

 

Notre première implémentation pose alors problème, on ne sait pas utiliser les repositories créés :

 

trait DefaultUserTweetServiceComponent extends UserTweetServiceComponent {

  override val userTweetService: UserTweetService = new DefaultUserTweetService

  class DefaultUserTweetService extends UserTweetService {
    override def createUser(user: User): User = ???
    override def createTweet(tweet: Tweet): Tweet = ???
    override def getUser(id: String): User = ???
    override def getTweet(id: String): Tweet = ???
    override def getUserAndTweets(id: String): (User,List[Tweet]) = ???
  }
}

 

Grace aux self-type references, il est possible de définir une dépendance de DefaultUserTweetServiceComponent vers les deux repository :

 

trait DefaultUserTweetServiceComponent extends UserTweetServiceComponent {
  self: UserRepositoryComponent with TweetRepositoryComponent =>
  ...
}

 

On peut déclarer la dépendance vers plusieurs composants grâçe au mot clé "with". Notons qu'on déclare bien une dépendance vers l'interface des composants repository.

On aurait pu écrire :

 

trait DefaultUserTweetServiceComponent extends UserTweetServiceComponent {
  self: InMemoryUserRepositoryComponent with InMemoryTweetRepositoryComponent =>
  ...
}

 

Dans ce cas, il y a un couplage fort entre l'implémentation de notre service et l'implémentation de nos repository. Il n'est plus possible de changer d'implémentation de repository au mixage.

L'implémentation finale du composant devient alors :

 

trait DefaultUserTweetServiceComponent extends UserTweetServiceComponent {

  self: UserRepositoryComponent with TweetRepositoryComponent =>

  override val userTweetService: UserTweetService = new DefaultUserTweetService

  class DefaultUserTweetService extends UserTweetService {
    override def createUser(user: User): User = userRepository.createUser(user)
    override def createTweet(tweet: Tweet): Tweet = tweetRepository.createTweet(tweet)
    override def getUser(id: String): User = userRepository.getUser(id)
    override def getTweet(id: String): Tweet = tweetRepository.getTweet(id)
    override def getUserAndTweets(id: String): (User,List[Tweet]) = {
      val user = userRepository.getUser(id)
      val tweets = tweetRepository.getAllByUser(user)
      (user,tweets)
    }
  }
}

 

Il est important de préciser que la classe DefaultUserTweetService doit être à l'intérieur du trait DefaultUserTweetServiceComponent. A l'extérieur, elle ne pourrait pas utiliser les membres userRepository et tweetRepository qui sont exposés par les composants dont dépend DefaultUserTweetServiceComponent.

Assemblage de l'application

Nous avons nos trois composants : un service qui dépend de deux repository. Il faut maintenant mixer les traits pour pouvoir utiliser notre application. Cependant, nous souhaitons restreindre la visibilité de certains beans, et n'exposer que la couche service aux clients de notre application.

Nous pouvons définir un trait qui n'expose que certains beans. Typiquement, une application est un ensemble de services et le client n'a pas à connaitre les détails d'implémentation.

 

trait MyApplication
  extends UserTweetServiceComponent
  // Ici on n'a qu'un seul service mais on aurait pu exposer tous les services de l'application:
  // with OtherServiceComponent 
  // with OtherServiceComponent2
  // with OtherServiceComponent3

 

Il est possible de définir un mixage de base entre les traits, qui sera notre "implémentation par défaut" de l'application :

 

trait MyApplicationMixin
  extends MyApplication
  with DefaultUserTweetServiceComponent
  with InMemoryUserRepositoryComponent
  with InMemoryTweetRepositoryComponent

 

A noter que nous utilisons DefaultUserTweetServiceComponent qui doit obligatoirement être mixé avec un UserRepositoryComponent et un TweetRepositoryComponent. Si ce mixage n'est pas fait, la compilation échoue. Il est toutefois possible de mixer DefaultUserTweetServiceComponent avec d'autres implémentations de repository, comme MongoDBTweetRepositoryComponent et MongoDBUserRepositoryComponent par exemple.

A partir de ce trait de mixage de base, nous pouvons créer une vraie instance de notre application. Encore une fois, pas besoin d'implémentation: chaque implémentation de composant fournit bien déjà une instance de son unique membre exposé.

 

val instance: MyApplication = new MyApplicationMixin { }

 

Lorsque nous créons cette instance de l'application, nous avons en quelque sorte créé un ApplicationContext à partir des fichiers Spring XML suivants :

 

<!-- application-context-inmemory-repositories.xml -->
<bean id="userRepository" class="repository.impl.InMemoryUserRepository"/>
<bean id="tweetRepository" class="repository.impl.InMemoryTweetRepository"/>

 

 

<!-- application-context-services.xml -->
<bean name="userTweetService" class="service.impl.DefaultUserTweetService">
    <property name="userRepository" ref="userRepository"/>
    <property name="tweetRepository" ref="tweetRepository"/>
</bean>

 

 

<!-- application-context.xml -->
<import resource="classpath*:/META-INF/application-context-inmemory-repositories.xml" />
<import resource="classpath*:/META-INF/application-context-services.xml" />

 

Le mixage des traits de type composant revient donc à faire l'équivalent de multiples imports Spring. Cependant, avec le Cake Pattern, le compilateur vérifie que les dépendances sont satisfaites.

 

// Ne compile pas car DefaultUserTweetServiceComponent a besoin d'un TweetRepositoryComponent
trait MyApplicationMixin2
  extends MyApplication
  with DefaultUserTweetServiceComponent
  with InMemoryUserRepositoryComponent

// Ne compile pas car DefaultUserTweetServiceComponent a besoin d'un UserRepositoryComponent
trait MyApplicationMixin3
  extends MyApplication
  with DefaultUserTweetServiceComponent
  with InMemoryTweetRepositoryComponent

// Compile
trait MyApplicationMixin4
  extends MyApplication
  with DefaultUserTweetServiceComponent
  with InMemoryUserRepositoryComponent
  with MongoDBTweetRepositoryComponent // on peut facilement changer d'implémentation

// Compile
trait MyApplicationMixin5
  extends MyApplication
  with NoDependencyUserTweetServiceComponent // On peut utiliser un service sans dépendances si besoin

 

Instanciation et utilisation de l'application

Nous pouvons commencer à utiliser notre application:

 

trait MyApplication
  extends UserTweetServiceComponent

trait MyApplicationMixin
  extends MyApplication
  with DefaultUserTweetServiceComponent
  with InMemoryUserRepositoryComponent
  with InMemoryTweetRepositoryComponent

val app: MyApplication = new MyApplicationMixin { }

// Code très basique pour tester l'application
val user1 = User("userId1","Sebastien","Lorber")
val user2 = User("userId2","Robert","Hue")
app.userTweetService.createUser( user1 )
app.userTweetService.createUser( user2 )
println("userId1 = " + app.userTweetService.getUser("userId1"))
println("userId2 = " + app.userTweetService.getUser("userId2"))
println("userId3 = " + app.userTweetService.getUser("userId3"))
println()
///////////////////////////////////////////////////////////////////////////////////////////
println("get user1 with tweets = " + app.userTweetService.getUserAndTweets(user1.id))
println("get user2 with tweets = " + app.userTweetService.getUserAndTweets(user2.id))
println()
///////////////////////////////////////////////////////////////////////////////////////////
app.userTweetService.createTweet( Tweet("tweetId1",user1.id,"Content tweet 1"))
app.userTweetService.createTweet( Tweet("tweetId2",user1.id,"Content tweet 2"))
app.userTweetService.createTweet( Tweet("tweetId3",user1.id,"Content tweet 3"))
app.userTweetService.createTweet( Tweet("tweetId4",user2.id,"Votez pour moi!"))
println("get user1 with tweets after insert = " + app.userTweetService.getUserAndTweets(user1.id))
println("get user2 with tweets after insert = " + app.userTweetService.getUserAndTweets(user2.id))

 

En sortie, nous avons:

userId1 = User(userId1,Sebastien,Lorber)
userId2 = User(userId2,Robert,Hue)
userId3 = null

get user1 with tweets = (User(userId1,Sebastien,Lorber),List())
get user2 with tweets = (User(userId2,Robert,Hue),List())

get user1 with tweets after insert = (User(userId1,Sebastien,Lorber),List(Tweet(tweetId2,userId1,Content tweet 2), Tweet(tweetId1,userId1,Content tweet 1)))
get user2 with tweets after insert = (User(userId2,Robert,Hue),List(Tweet(tweetId4,userId2,Votez pour moi!)))

Customisation de l'application de base

Avec Spring, il est parfois pénible, lors de tests d'intégration, de devoir créer et maintenir un contexte XML spécifique. Pour changer un simple détail d'implémentation d'un repository, cela peut nécessiter toute une gymnastique qui conduit souvent à la création de contextes XML inutiles. Il est tout de même possible d'overrider un bean déjà déclaré mais la pratique est peu courante et non recommandée.

L'interêt d'avoir utilisé un mixage de base pour notre application est qu'il devient ainsi très facile de créer des déclinaisons de notre application réelle. On peut par exemple créer une application qui est capable de trouver les tweets par leurs ids, mais incapable de trouver les tweets d'un utilisateur :

 

val app: MyApplication = new MyApplicationMixin {
  override val tweetRepository = new InMemoryTweetRepository {
    // On remplace la bonne implémentation par une mauvaise, volontairement
    override def getAllByUser(user: User): List[Tweet] = List()
  }
}

 

En réutilisant le code de test de l'application défini au dessus, nous avons cette fois en sortie :

userId1 = User(userId1,Sebastien,Lorber)
userId2 = User(userId2,Robert,Hue)
userId3 = null

get user1 with tweets = (User(userId1,Sebastien,Lorber),List())
get user2 with tweets = (User(userId2,Robert,Hue),List())

get user1 with tweets after insert = (User(userId1,Sebastien,Lorber),List())
get user2 with tweets after insert = (User(userId2,Robert,Hue),List())

Il n'est pas plus compliqué d'injecter un mock au sein de notre application:

 

val app: MyApplication = new MyApplicationMixin {
  override val tweetRepository = mock[InMemoryTweetRepository]
}

 

A noter que l'application est utilisée à partir du trait MyApplication qui n'expose que les services. Ceci permet d'éviter qu'un développeur utilise directement un repository depuis la couche controlleur :

 

val app: MyApplication = new MyApplicationMixin { }
val user1 = User("userId1","Sebastien","Lorber")
// Ne compile pas: le repository n'est pas exposé par le trait MyApplication
app.userRepository.createUser( user1 ) 

 

Gestion de la configuration

Avec Spring, nous sommes habitués aux fameux placeholders ${xxx} et aux fichiers .properties.

Avec le Cake Pattern, il est tout à fait possible de créer un composant ApplicationPropertiesComponent dont les autres composants dépendront, et qui fournira la configuration à chaque composant. Dans ce composant, on peut directement écrire du Scala, ce qui présente une alternative puissante à Spring qui se base sur les PropertySource ou le SpEL.

Il est également possible de changer la stratégie d'instanciation et le mixage des composants en fonction de la configuration, de même qu'on pourrait définir sous Spring un import de contexte XML dynamique (en fonction d'un placeholder).

Il existe un projet typesafe config pour gérer les fichiers de configuration properties ou JSON.

Ordre d'initialisation des composants

Supposons que nous souhaitons executer du code lors de l'initialisation d'un composant.

 

trait DefaultUserTweetServiceComponent extends UserTweetServiceComponent {

  self: UserRepositoryComponent with TweetRepositoryComponent =>

  override val userTweetService: UserTweetService = new DefaultUserTweetService

  class DefaultUserTweetService extends UserTweetService {

    userRepository.createUser(User("1","admin","admin")) // Création du compte admin

    // autres implémentations du service...
  }
}

 

Dans cet exemple, nous avons rajouté la création automatique d'un compte administrateur. Cette approche peut poser problème au runtime selon l'ordre du mixage.

Avec le mixage suivant, nous avons une NullPointerException car le userRepository n'est pas encore initialisé lorsque l'appel s'effectue :

 

    trait MyApplicationMixin
      extends MyApplication
      with DefaultUserTweetServiceComponent
      with InMemoryUserRepositoryComponent
      with InMemoryTweetRepositoryComponent

 

Mais avec le mixage suivant, en déclarant le service en dernier, cela fonctionne correctement:

 

    trait MyApplicationMixin
      extends MyApplication
      with InMemoryUserRepositoryComponent
      with InMemoryTweetRepositoryComponent
      with DefaultUserTweetServiceComponent

 

Dans la pratique, il vaut mieux éviter d'utiliser ce genre de bout de code, et procéder à l'initialisation une fois l'application réellement construite.

Avec Spring, nous utiliserions une interface InitializingBean ou une annotation @PostConstruct, et nous ne mettrions pas le code d'initialisation qui utilise les dépendances dans le constructeur. De la même manière, Scala propose un trait DelayedInit

Cette solution ne provoque pas d'erreur au runtime, quel que soit l'ordre du mixage :

 

trait DefaultUserTweetServiceComponent extends UserTweetServiceComponent {

  self: UserRepositoryComponent with TweetRepositoryComponent =>

  override val userTweetService: UserTweetService = new DefaultUserTweetService

  class DefaultUserTweetService extends UserTweetService with DelayedInit {

    def delayedInit(x: => Unit) {
      userRepository.createUser(User("1","admin","admin"))
    }

    // autres implémentations du service...
  }
}

 

Et si jamais il y a besoin de récupérer une instance fournie par une dépendance, il sera alors préférable d'utiliser une lazy val :

 

trait DefaultUserTweetServiceComponent extends UserTweetServiceComponent {

  self: UserRepositoryComponent with TweetRepositoryComponent =>

  override val userTweetService: UserTweetService = new DefaultUserTweetService

  class DefaultUserTweetService extends UserTweetService {

    lazy val adminUser: User = userRepository.getUser("1")

    // autres implémentations du service... (qui utilise probablement adminUser)
  }
}

 

L'ordre d'initialisation est sans doute un des aspects les plus complexes du Cake Pattern, qui demande une bonne maitrise du langage et de ses subtilités.

Gestion des scopes des beans

Avec Spring, nous avons la possibilité d'utiliser des scopes pour les beans créés (singleton, prototype, request...).

La gestion des scopes avec le Cake Pattern est simple : si nous avons besoin de quelque chose de plus complexe que le scope singleton, il suffit d'exposer l'objet fourni par le composant avec def plutot que val.

 

trait UserTweetServiceComponent {
  def userTweetService: UserTweetService
}

 

De cette manière, il est possible de définir dans l'implémentation du composant la stratégie à utiliser pour fournir le bean. C'est à nous de définir cette stratégie et de la coder, alors que Spring fournit des stratégies de scoping toutes prêtes.

Gestion des transactions

Spring propose l'annotation @Transactionnal pour gérer les transactions. Avec le Cake Pattern, rien n'est préconçu, mais le langage étant moins verbeux, il est très simple de gérer les transactions à la main.

 

def decrementAllActiveUsersCredit: Result = doInTransaction {
  ...
}

 

On peut aussi utiliser AspectJ ou n'importe quel autre framework d'AOP.

Conclusion

Le Cake Pattern permet de faire nativement et correctement de l'injection de dépendance. L'application créée est immuable et le compilateur et le support de l'IDE nous indiquent les problèmes avant le runtime, sans besoin de plugin additionnel.

On comprend facilement l'interêt de ce pattern, mais on regrettera sans doute de devoir réimplémenter soi-même certaines fonctionnalités de Spring comme la gestion des scopes ou des transactions par annotations.

Il est possible d'intégrer facilement ce pattern dans divers framework, dont Play 2. On peut définir les composants jusqu'à la couche controlleur, en utilisant un package object qui instancie notre application en se basant sur la configuration de Play 2, comme expliqué en bas de cet article.

Il sera également possible d'utiliser une version plutôt limitée du Cake Pattern avec Java 8. Par exemple, le manque de self-type references en Java ne permettrait pas de définir des dépendances cycliques entre deux services.

Il faut noter qu'on retrouve sur internet des exemples de Cake Pattern divers et variés. La version présentée dans cette article essaie d'être complète et d'expliquer au mieux comment structurer son application Cake Pattern, pour avoir un couplage faible entre les composants, un contrôle de la visibilité des beans exposés, et la possibilité de customiser facilement l'application.

Pour aller plus loin, cet article de Jonas Boner (auteur d'Akka) expose différentes stratégies d'injection de dépendances en Scala: Real-World Scala: Dependency Injection.

Evaluer cet article

Pertinence
Style

Contenu Éducatif

Bonjour étranger!

Vous devez créer un compte InfoQ ou cliquez sur pour déposer des commentaires. Mais il y a bien d'autres avantages à s'enregistrer.

Tirez le meilleur d'InfoQ

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Commentaires de la Communauté

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

Html autorisé: a,b,br,blockquote,i,li,pre,u,ul,p

BT