什么是Scala等效于Java构建器模式?

时间:2021-12-31 13:48:36

In the work that I do on a day to day in Java, I use builders quite a lot for fluent interfaces, e.g.: new PizzaBuilder(Size.Large).onTopOf(Base.Cheesy).with(Ingredient.Ham).build();

在我每天用Java做的工作中,我使用构建器来进行流畅的接口,例如:new PizzaBuilder(Size.Large).onTopOf(Base.Cheesy).with(Ingredient.Ham).build( );

With a quick-and-dirty Java approach, each method call mutates the builder instance and returns this. Immutably, it involves more typing, cloning the builder first before modifying it. The build method eventually does the heavy lifting over the builder state.

使用快速而肮脏的Java方法,每个方法调用都会改变构建器实例并返回它。不可避免地,它涉及更多的打字,在修改之前首先克隆构建器。构建方法最终会对构建器状态进行繁重的处理。

What's a nice way of achieving the same in Scala?

什么是在Scala中实现相同的好方法?

If I wanted to ensure that onTopOf(base:Base) was called only once, and then subsequently only with(ingredient:Ingredient) and build():Pizza could be called, a-la a directed builder, how would I go about approaching this?

如果我想确保onTopOf(base:Base)只调用一次,然后只调用(成分:Ingredient)和build():可以调用Pizza,a-la是一个定向构建器,我将如何进行接近这个?

5 个解决方案

#1


51  

Another alternative to the Builder pattern in Scala 2.8 is to use immutable case classes with default arguments and named parameters. Its a little different but the effect is smart defaults, all values specified and things only specified once with syntax checking...

Scala 2.8中Builder模式的另一种替代方法是使用带有默认参数和命名参数的不可变case类。它有点不同,但效果是智能默认值,指定的所有值和仅通过语法检查指定的内容...

The following uses Strings for the values for brevity/speed...

以下使用字符串作为简洁/速度的值......

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)

You can then also use existing immutable instances as kinda builders too...

然后,您也可以将现有的不可变实例用作有点构建器......

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

#2


27  

You have three main alternatives here.

你有三个主要的选择。

  1. Use the same pattern as in Java, classes and all.

    使用与Java,类和所有相同的模式。

  2. Use named and default arguments and a copy method. Case classes already provide this for you, but here's an example that is not a case class, just so you can understand it better.

    使用命名和默认参数以及复制方法。案例类已经为您提供了这个,但这里的示例不是案例类,只是为了让您更好地理解它。

    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. Use a type safe builder pattern. The best introduction I know of is this blog, which also contains references to many other articles on the subject.

    使用类型安全构建器模式。我所知道的最好的介绍是这个博客,其中还包含有关该主题的许多其他文章的参考。

    Basically, a type safe builder pattern guarantees at compile time that all required components are provided. One can even guarantee mutual exclusion of options or arity. The cost is the complexity of the builder code, but...

    基本上,类型安全构建器模式在编译时保证提供所有必需的组件。人们甚至可以保证相互排斥选择或权利。成本是构建器代码的复杂性,但......

#3


9  

It's the same exact pattern. Scala allows for mutation and side effects. That said, if you'd like to be more of a purest, have each method return a new instance of the object that you're constructing with the element(s) changed. You could even put the functions within the Object of a class so that there's a higher level of separation within your code.

这是完全相同的模式。 Scala允许突变和副作用。也就是说,如果你想要更纯粹,那么让每个方法都返回一个你正在构造的对象的新实例,并改变元素。您甚至可以将函数放在类的Object中,以便在代码中实现更高级别的分离。

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

so that your code might look like

这样你的代码可能会像

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

(Note: I've probably screwed up some syntax here.)

(注意:我可能在这里搞砸了一些语法。)

#4


6  

Case classes solve the problem as shown in previous answers, but the resulting api is difficult to use from java when You have scala collections in your objects. To provide a fluent api to java users try this:

案例类解决了问题,如前面的答案中所示,但是当您在对象中有scala集合时,很难从java中使用生成的api。为java用户提供流畅的api试试这个:

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

}

Then in java code the api is really easy to use

然后在java代码中,api非常容易使用

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

#5


0  

using Scala partial applies are feasible if you are building a smallish object that you don't need to pass over method signatures. If any of those assumptions don't apply, I recommend using a mutable builder to build an immutable object. With this being scala you could implement the builder pattern with a case class for the object to build with a companion as the builder.

如果要构建一个不需要传递方法签名的小对象,则使用Scala partial apply是可行的。如果这些假设中的任何一个不适用,我建议使用可变构建器来构建不可变对象。如果这是scala,您可以使用一个案例类来实现构建器模式,以便使用随播广告作为构建器来构建对象。

Given that the end result is a constructed immutable object I don't see that it defeats any of the Scala principles.

鉴于最终结果是一个构造的不可变对象,我不认为它会破坏任何Scala原则。

#1


51  

Another alternative to the Builder pattern in Scala 2.8 is to use immutable case classes with default arguments and named parameters. Its a little different but the effect is smart defaults, all values specified and things only specified once with syntax checking...

Scala 2.8中Builder模式的另一种替代方法是使用带有默认参数和命名参数的不可变case类。它有点不同,但效果是智能默认值,指定的所有值和仅通过语法检查指定的内容...

The following uses Strings for the values for brevity/speed...

以下使用字符串作为简洁/速度的值......

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)

You can then also use existing immutable instances as kinda builders too...

然后,您也可以将现有的不可变实例用作有点构建器......

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

#2


27  

You have three main alternatives here.

你有三个主要的选择。

  1. Use the same pattern as in Java, classes and all.

    使用与Java,类和所有相同的模式。

  2. Use named and default arguments and a copy method. Case classes already provide this for you, but here's an example that is not a case class, just so you can understand it better.

    使用命名和默认参数以及复制方法。案例类已经为您提供了这个,但这里的示例不是案例类,只是为了让您更好地理解它。

    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. Use a type safe builder pattern. The best introduction I know of is this blog, which also contains references to many other articles on the subject.

    使用类型安全构建器模式。我所知道的最好的介绍是这个博客,其中还包含有关该主题的许多其他文章的参考。

    Basically, a type safe builder pattern guarantees at compile time that all required components are provided. One can even guarantee mutual exclusion of options or arity. The cost is the complexity of the builder code, but...

    基本上,类型安全构建器模式在编译时保证提供所有必需的组件。人们甚至可以保证相互排斥选择或权利。成本是构建器代码的复杂性,但......

#3


9  

It's the same exact pattern. Scala allows for mutation and side effects. That said, if you'd like to be more of a purest, have each method return a new instance of the object that you're constructing with the element(s) changed. You could even put the functions within the Object of a class so that there's a higher level of separation within your code.

这是完全相同的模式。 Scala允许突变和副作用。也就是说,如果你想要更纯粹,那么让每个方法都返回一个你正在构造的对象的新实例,并改变元素。您甚至可以将函数放在类的Object中,以便在代码中实现更高级别的分离。

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

so that your code might look like

这样你的代码可能会像

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

(Note: I've probably screwed up some syntax here.)

(注意:我可能在这里搞砸了一些语法。)

#4


6  

Case classes solve the problem as shown in previous answers, but the resulting api is difficult to use from java when You have scala collections in your objects. To provide a fluent api to java users try this:

案例类解决了问题,如前面的答案中所示,但是当您在对象中有scala集合时,很难从java中使用生成的api。为java用户提供流畅的api试试这个:

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

}

Then in java code the api is really easy to use

然后在java代码中,api非常容易使用

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

#5


0  

using Scala partial applies are feasible if you are building a smallish object that you don't need to pass over method signatures. If any of those assumptions don't apply, I recommend using a mutable builder to build an immutable object. With this being scala you could implement the builder pattern with a case class for the object to build with a companion as the builder.

如果要构建一个不需要传递方法签名的小对象,则使用Scala partial apply是可行的。如果这些假设中的任何一个不适用,我建议使用可变构建器来构建不可变对象。如果这是scala,您可以使用一个案例类来实现构建器模式,以便使用随播广告作为构建器来构建对象。

Given that the end result is a constructed immutable object I don't see that it defeats any of the Scala principles.

鉴于最终结果是一个构造的不可变对象,我不认为它会破坏任何Scala原则。