61 votes

Quel est l'équivalent Scala d'un modèle de constructeur Java?

Dans le travail que je fais au jour le jour, en Java, j'utilise les constructeurs de beaucoup de fluide interfaces, par exemple: new PizzaBuilder(Size.Large).onTopOf(Base.Cheesy).with(Ingredient.Ham).build();

Avec un quick-and-dirty Java approche, chaque appel de méthode se transforme le constructeur d'instance et les retours this. Immuablement, il implique le plus saisissant, le clonage, le constructeur d'abord avant de le modifier. La méthode build finalement ne le levage lourd sur le générateur d'état.

Ce est une belle façon d'atteindre le même en Scala?

Si je voulais m'assurer que onTopOf(base:Base) a été appelé qu'une seule fois, et ensuite seulement with(ingredient:Ingredient) et build():Pizza pourrait être appelée, à la une du générateur, comment pourrais-je aller sur cette approche?

59voto

James Strachan Points 6144

Une autre alternative au modèle Builder de Scala 2.8 consiste à utiliser des classes de cas immuables avec des arguments par défaut et des paramètres nommés. C'est un peu différent mais l'effet est intelligent par défaut, toutes les valeurs spécifiées et les choses spécifiées une fois avec vérification de la syntaxe ...

Ce qui suit utilise Strings pour les valeurs de brièveté / vitesse ...

 scala> case class Pizza(ingredients: Traversable[String], base: String = "Normal", topping: String = "Mozzarella")
defined class Pizza

scala> val p1 = Pizza(Seq("Ham", "Mushroom"))                                                                     
p1: Pizza = Pizza(List(Ham, Mushroom),Normal,Mozzarella)

scala> val p2 = Pizza(Seq("Mushroom"), topping = "Edam")                               
p2: Pizza = Pizza(List(Mushroom),Normal,Edam)

scala> val p3 = Pizza(Seq("Ham", "Pineapple"), topping = "Edam", base = "Small")       
p3: Pizza = Pizza(List(Ham, Pineapple),Small,Edam)
 

Vous pouvez également utiliser des instances immuables existantes comme un peu constructeurs aussi ...

 scala> val lp2 = p3.copy(base = "Large")
lp2: Pizza = Pizza(List(Ham, Pineapple),Large,Edam)
 

33voto

Daniel C. Sobral Points 159554

Vous avez trois principales solutions de rechange ici.

  1. Utiliser le même schéma qu'en Java, les classes et de tous.

  2. Utiliser nommé et arguments par défaut et une méthode de copie. Des classes de cas déjà de fournir ce pour vous, mais voici un exemple qui n'est pas une affaire de classe, de sorte que vous pouvez le comprendre mieux.

    object Size {
        sealed abstract class Type
        object Large extends Type
    }
    
    object Base {
        sealed abstract class Type
        object Cheesy extends Type
    }
    
    object Ingredient {
        sealed abstract class Type
        object Ham extends Type
    }
    
    class Pizza(size: Size.Type, 
                base: Base.Type, 
                ingredients: List[Ingredient.Type])
    
    class PizzaBuilder(size: Size.Type, 
                       base: Base.Type = null, 
                       ingredients: List[Ingredient.Type] = Nil) {
    
        // A generic copy method
        def copy(size: Size.Type = this.size,
                 base: Base.Type = this.base,
                 ingredients: List[Ingredient.Type] = this.ingredients) = 
            new PizzaBuilder(size, base, ingredients)
    
    
        // An onTopOf method based on copy
        def onTopOf(base: Base.Type) = copy(base = base)
    
    
        // A with method based on copy, with `` because with is a keyword in Scala
        def `with`(ingredient: Ingredient.Type) = copy(ingredients = ingredient :: ingredients)
    
    
        // A build method to create the Pizza
        def build() = {
            if (size == null || base == null || ingredients == Nil) error("Missing stuff")
            else new Pizza(size, base, ingredients)
        }
    }
    
    // Possible ways of using it:
    new PizzaBuilder(Size.Large).onTopOf(Base.Cheesy).`with`(Ingredient.Ham).build();
    // or
    new PizzaBuilder(Size.Large).copy(base = Base.Cheesy).copy(ingredients = List(Ingredient.Ham)).build()
    // or
    new PizzaBuilder(size = Size.Large, 
                     base = Base.Cheesy, 
                     ingredients = Ingredient.Ham :: Nil).build()
    // or even forgo the Builder altogether and just 
    // use named and default parameters on Pizza itself
    
  3. L'utilisation d'un type de sécurité du générateur de modèle. La meilleure introduction que je connaisse est de ce blog, qui contient également des références à de nombreux autres articles sur le sujet.

    En gros, un type de sécurité du générateur de modèle de garanties au moment de la compilation que tous les composants requis sont fournis. On peut même garantir l'exclusion mutuelle d'options ou d'arité. Le coût est la complexité du générateur de code, mais...

13voto

Paweld2 Points 411

Les classes de cas résolvent le problème comme indiqué dans les réponses précédentes, mais l’API qui en résulte est difficile à utiliser à partir de java lorsque vous avez des collections de scala dans vos objets. Pour fournir une interface API fluide aux utilisateurs de Java, essayez ceci:

 case class SEEConfiguration(parameters : Set[Parameter],
                               plugins : Set[PlugIn])

case class Parameter(name: String, value:String)
case class PlugIn(id: String)

trait SEEConfigurationGrammar {

  def withParameter(name: String, value:String) : SEEConfigurationGrammar

  def withParameter(toAdd : Parameter) : SEEConfigurationGrammar

  def withPlugin(toAdd : PlugIn) : SEEConfigurationGrammar

  def build : SEEConfiguration

}

object SEEConfigurationBuilder {
  def empty : SEEConfigurationGrammar = SEEConfigurationBuilder(Set.empty,Set.empty)
}


case class SEEConfigurationBuilder(
                               parameters : Set[Parameter],
                               plugins : Set[PlugIn]
                               ) extends SEEConfigurationGrammar {
  val config : SEEConfiguration = SEEConfiguration(parameters,plugins)

  def withParameter(name: String, value:String) = withParameter(Parameter(name,value))

  def withParameter(toAdd : Parameter) = new SEEConfigurationBuilder(parameters + toAdd, plugins)

  def withPlugin(toAdd : PlugIn) = new SEEConfigurationBuilder(parameters , plugins + toAdd)

  def build = config

}
 

Alors dans le code Java l'api est vraiment facile à utiliser

 SEEConfigurationGrammar builder = SEEConfigurationBuilder.empty();
SEEConfiguration configuration = builder
    .withParameter(new Parameter("name","value"))
    .withParameter("directGivenName","Value")
    .withPlugin(new PlugIn("pluginid"))
    .build();
 

11voto

wheaties Points 20917

C'est le même modèle exact. Scala permet de mutation et d'effets secondaires. Cela dit, si vous aimeriez être plus en plus pure, ont chaque méthode retourne une nouvelle instance de l'objet que vous êtes de la construction avec le ou les élément(s) modifié. Vous pouvez même mettre les fonctions au sein de l'Objet de classe de façon à ce qu'il y a un niveau plus élevé de séparation à l'intérieur de votre code.

class Pizza(size:SizeType, layers:List[Layers], toppings:List[Toppings]){
    def Pizza(size:SizeType) = this(size, List[Layers](), List[Toppings]())

object Pizza{
    def onTopOf( layer:Layer ) = new Pizza(size, layers :+ layer, toppings)
    def withTopping( topping:Topping ) = new Pizza(size, layers, toppings :+ topping)
}

pour que votre code pourrait ressembler à

val myPizza = new Pizza(Large) onTopOf(MarinaraSauce) onTopOf(Cheese) withTopping(Ham) withTopping(Pineapple)

(Note: j'ai probablement foiré certains de la syntaxe ici.)

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X