Подтвердить что ты не робот

Что такое Scala, эквивалентный шаблону Java-строителя?

В работе, которую я делаю ежедневно на Java, я часто использую строителей для свободного интерфейса, например: new PizzaBuilder(Size.Large).onTopOf(Base.Cheesy).with(Ingredient.Ham).build();

Благодаря быстрому и грязному подходу Java каждый вызов метода мутирует экземпляр компоновщика и возвращает this. Неизменно, он включает в себя больше типизации, клонирование строителя прежде, чем изменить его. Метод сборки в конечном итоге делает тяжелый подъем над состоянием строителя.

Какой хороший способ добиться того же в Scala?

Если бы я хотел убедиться, что onTopOf(base:Base) был вызван только один раз, а затем можно было бы вызывать только with(ingredient:Ingredient) и build():Pizza, a-la - направленный конструктор, как бы я хотел приблизиться к этому?

4b9b3361

Ответ 1

Другой альтернативой шаблону Builder в Scala 2.8 является использование неизменяемых классов case с аргументами по умолчанию и именованными параметрами. Его немного отличается, но эффект - это умные значения по умолчанию, все указанные значения и вещи, которые только один раз указываются с проверкой синтаксиса...

Ниже используются строки для значений для краткости/скорости...

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)

Затем вы также можете использовать существующие неизменяемые экземпляры как собственные конструкторы...

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

Ответ 2

Здесь у вас есть три основных альтернативы.

  • Используйте тот же шаблон, что и в Java, классах и всех.

  • Используйте аргументы named и default и метод копирования. Классы классов уже предоставляют это для вас, но вот пример, который не является классом case, так что вы можете понять его лучше.

    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

Это то же точное изображение. Scala допускает мутацию и побочные эффекты. Тем не менее, если вы хотите быть более чистым, каждый метод возвращает новый экземпляр объекта, который вы создаете с измененными элементами. Вы даже можете поместить функции в объект класса, чтобы в вашем коде был более высокий уровень разделения.

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

чтобы ваш код выглядел как

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

(Примечание: я, вероятно, испортил некоторый синтаксис здесь.)

Ответ 4

Примеры классов решают проблему, как показано в предыдущих ответах, но полученный api трудно использовать из java, когда у вас есть коллекции scala в ваших объектах. Чтобы обеспечить быстрое использование api для java-пользователей, попробуйте следующее:

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

}

Тогда в java-коде api действительно прост в использовании

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

Ответ 5

с использованием Scala частичных применений возможны, если вы создаете маленький объект, который вам не нужен, чтобы передавать сигнатуры методов. Если какое-либо из этих предположений не применяется, я рекомендую использовать изменяемый построитель для создания неизменяемого объекта. С помощью этого Scala вы можете реализовать шаблон построителя с классом case для объекта, который будет создан вместе со своим компаньоном как строитель.

Учитывая, что конечный результат является построенным неизменным объектом, я не вижу, чтобы он побеждал любой из принципов Scala.