wake-up-neo.com

Was ist die Scala-Entsprechung zu einem Java-Builder-Muster?

Bei der Arbeit, die ich täglich in Java erledige, verwende ich häufig Builder für fließende Schnittstellen, z. B .: new PizzaBuilder(Size.Large).onTopOf(Base.Cheesy).with(Ingredient.Ham).build();

Bei einem schnellen Java-Ansatz verändert jeder Methodenaufruf die Builder-Instanz und gibt this zurück. Unverkennbar bedeutet dies mehr Tippen und Klonen des Builders, bevor er geändert wird. Die Build-Methode führt letztendlich das schwere Anheben über den Builder-Status aus.

Was ist ein schöner Weg, um das gleiche in Scala zu erreichen?

Wenn ich sicherstellen wollte, dass onTopOf(base:Base) nur einmal aufgerufen wurde, und anschließend nur with(ingredient:Ingredient) und build():Pizza aufgerufen werden konnten, a-la ein gerichteter Builder, wie gehe ich darauf vor?

51
Jakub Korab

Eine weitere Alternative zum Builder-Muster in Scala 2.8 ist die Verwendung von unveränderlichen Fallklassen mit Standardargumenten und benannten Parametern. Es ist ein wenig anders, aber der Effekt sind intelligente Standardwerte, alle Werte und Dinge nur einmal bei der Syntaxprüfung angegeben ...

Im Folgenden werden Strings für die Werte für die Kürze/Geschwindigkeit verwendet.

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)

Sie können dann auch vorhandene unveränderliche Instanzen als irgendwie Builder verwenden ...

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

Sie haben hier drei Hauptalternativen.

  1. Verwenden Sie dasselbe Muster wie in Java, Klassen und alle.

  2. Verwenden Sie benannte und Standardargumente und eine Kopiermethode. Fallklassen stellen dies bereits für Sie bereit, aber hier ist ein Beispiel, das keine Fallklasse ist, nur damit Sie es besser verstehen können.

    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. Verwenden Sie ein sicheres Builder-Muster vom Typ type. Die beste Einführung, die ich kenne, ist dieses Blog , die auch Verweise auf viele andere Artikel zum Thema enthält.

    Grundsätzlich garantiert ein typsicheres Builder-Muster zum Kompilierzeitpunkt, dass alle erforderlichen Komponenten bereitgestellt werden. Man kann sogar den gegenseitigen Ausschluss von Optionen oder Arity garantieren. Die Kosten sind die Komplexität des Builder-Codes, aber ...

27

Fallklassen lösen das Problem wie in den vorherigen Antworten gezeigt. Die resultierende API ist jedoch unter Java nur schwer zu verwenden, wenn Sie Scala-Sammlungen in Ihren Objekten haben. Um eine fließende API für Java-Benutzer bereitzustellen, versuchen Sie Folgendes:

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

}

Im Java-Code ist die API sehr einfach zu verwenden

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

Es ist genau das gleiche Muster. Scala ermöglicht Mutationen und Nebenwirkungen. Das heißt, wenn Sie mehr rein sein wollen, lassen Sie jede Methode eine neue Instanz des Objekts zurückgeben, das Sie mit den geänderten Elementen erstellen. Sie können sogar die Funktionen innerhalb des Objekts einer Klasse platzieren, sodass der Code stärker voneinander getrennt wird.

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

damit Ihr Code aussehen könnte

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

(Anmerkung: Ich habe wahrscheinlich etwas Syntax hier versaut.)

8
wheaties

die Verwendung von Scala-Teilanwendungen ist möglich, wenn Sie ein kleineres Objekt erstellen, das Sie nicht über Methodensignaturen übergeben müssen. Wenn eine dieser Annahmen nicht zutrifft, empfehle ich die Verwendung eines veränderlichen Builders, um ein unveränderliches Objekt zu erstellen. Da dies Scala ist, können Sie das Builder-Muster mit einer Fallklasse implementieren, die das Objekt mit einem Companion als Builder erstellen soll.

Da das Endergebnis ein konstruiertes unveränderliches Objekt ist, sehe ich nicht, dass es eines der Scala-Prinzipien besiegt. 

0
Andrew Norman