Dans le cadre de mon travail quotidien en Java, j’utilise beaucoup de constructeurs pour les interfaces fluides, par exemple: new PizzaBuilder(Size.Large).onTopOf(Base.Cheesy).with(Ingredient.Ham).build();
Avec une approche Java rapide et sale, chaque appel de méthode mute l'instance de générateur et renvoie this
. Immutablement, cela implique plus de frappe, clonant le constructeur avant de le modifier. La méthode de construction finit par faire le gros du travail sur l’état du constructeur.
Qu'est-ce qu'un bon moyen d'atteindre le même objectif à Scala?
Si je voulais m'assurer que onTopOf(base:Base)
n'était appelé qu'une seule fois et que, par la suite, seuls with(ingredient:Ingredient)
et build():Pizza
puissent être appelés, à la manière d'un constructeur dirigé, comment pourrais-je m'y prendre?
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)
Vous avez trois alternatives principales ici.
Utilisez le même modèle qu'en Java, les classes et tous.
Utilisez des arguments nommés et par défaut et une méthode de copie. Les classes de cas fournissent déjà cela pour vous, mais voici un exemple qui n'est pas une classe de cas, afin que vous puissiez mieux le comprendre.
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
Utilisez un modèle de générateur sûr type. La meilleure introduction que je connaisse est ce blog , qui contient également des références à de nombreux autres articles sur le sujet.
Fondamentalement, un modèle de générateur de type sécurisé garantit au moment de la compilation que tous les composants requis sont fournis. On peut même garantir une exclusion mutuelle des options ou de l’arité. Le coût est la complexité du code de constructeur, mais ...
Les classes de cas résolvent le problème comme indiqué dans les réponses précédentes, mais l’API obtenue est difficile à utiliser à partir de Java lorsque vous avez des collections de scala dans vos objets. Pour fournir une 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
}
Ensuite, 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();
C'est le même modèle exact. Scala permet des mutations et des effets secondaires. Cela dit, si vous souhaitez être plus pur, demandez à chaque méthode de renvoyer une nouvelle instance de l'objet que vous construisez avec le ou les éléments modifiés. Vous pouvez même placer les fonctions dans l’objet d’une classe afin d’obtenir un niveau de séparation plus élevé dans 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)
}
afin que votre code puisse ressembler à
val myPizza = new Pizza(Large) onTopOf(MarinaraSauce) onTopOf(Cheese) withTopping(Ham) withTopping(Pineapple)
(Remarque: j'ai probablement bousillé une syntaxe ici.)
il est possible d'utiliser des applications partielles de Scala si vous construisez un objet de petite taille pour lequel vous n'avez pas besoin de passer par-dessus les signatures de méthode. Si l'une de ces hypothèses ne s'applique pas, je vous recommande d'utiliser un générateur mutable pour créer un objet immuable. Ceci étant scala, vous pouvez implémenter le modèle de générateur avec une classe de cas pour que l'objet soit généré avec un compagnon en tant que générateur.
Etant donné que le résultat final est un objet immuable construit, je ne vois pas qu'il vainc tous les principes de Scala.