Что такое Scala эквивалентно шаблону Java-компоновщика? - PullRequest
52 голосов
/ 07 января 2011

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

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

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

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

Ответы [ 5 ]

52 голосов
/ 07 января 2011

Другой альтернативой шаблону 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)
28 голосов
/ 07 января 2011

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

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

  2. Используйте именованные и стандартныеаргументы и метод копирования.Классы case уже предоставляют это для вас, но вот пример, который не является классом 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. Используйте безопасный компоновщик типа рисунок .Лучшее введение, которое я знаю, - это этот блог , который также содержит ссылки на многие другие статьи на эту тему.

    По сути, шаблон безопасных типов построителя гарантирует во время компиляции, что все необходимые компонентыпредоставлена.Можно даже гарантировать взаимное исключение опций или арность.Стоимость кода сложность строителя, но ...

9 голосов
/ 07 января 2011

Это точно такой же шаблон. Скала учитывает мутации и побочные эффекты. Тем не менее, если вы хотите быть более чистым, каждый метод должен возвращать новый экземпляр объекта, который вы создаете, с измененным элементом (ами). Вы даже можете поместить функции в 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)
}

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

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

(Примечание. Скорее всего, здесь какой-то синтаксис облажался.)

8 голосов
/ 30 апреля 2013

Классы Case решают проблему, как показано в предыдущих ответах, но полученный 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();
0 голосов
/ 02 июня 2016

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...