Что такое 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 - направленный конструктор, как бы я хотел приблизиться к этому?
Ответы
Ответ 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.