Лучшая альтернатива шаблону стратегии в Scala?
Когда я программирую на Java (или аналогичном языке), я часто использую простую версию шаблона Strategy, используя интерфейсы и классы реализации, чтобы обеспечить реализационные реализации определенной концепции в моем коде. p >
Как очень надуманный пример, я могу захотеть иметь общую концепцию Animal, которая может создать шум в моем Java-коде и хочу иметь возможность выбирать тип животного во время выполнения. Поэтому я бы написал код в следующих строках:
interface Animal {
void makeNoise();
}
class Cat extends Animal {
void makeNoise() { System.out.println("Meow"); }
}
class Dog extends Animal {
void makeNoise() { System.out.println("Woof"); }
}
class AnimalContainer {
Animal myAnimal;
AnimalContainer(String whichOne) {
if (whichOne.equals("Cat"))
myAnimal = new Cat();
else
myAnimal = new Dog();
}
void doAnimalStuff() {
...
// Time for the animal to make a noise
myAnimal.makeNoise();
...
}
Прост достаточно. Недавно, однако, я работал над проектом в Scala, и я хочу сделать то же самое. Кажется, достаточно легко сделать это, используя черты, с чем-то вроде этого:
trait Animal {
def makeNoise:Unit
}
class Cat extends Animal {
override def makeNoise:Unit = println("Meow")
}
class AnimalContainer {
val myAnimal:Animal = new Cat
...
}
Однако это кажется очень похожим на Java и не очень функциональным - не говоря уже о том, что черты и интерфейсы на самом деле не то же самое. Поэтому мне интересно, есть ли более идиоматический способ реализовать шаблон стратегии - или что-то в этом роде - в моем Scala коде, чтобы я мог выбрать конкретную реализацию абстрактного понятия во время выполнения. Или использует черты лучший способ достичь этого?
Ответы
Ответ 1
Вы можете сделать вариацию на шаблоне торта.
trait Animal {
def makenoise: Unit
}
trait Cat extends Animal {
override def makeNoise { println("Meow") }
}
trait Dog extends Animal {
override def makeNoise { println("Woof") }
}
class AnimalContaineer {
self: Animal =>
def doAnimalStuff {
// ...
makeNoise
// ...
}
}
object StrategyExample extends Application {
val ex1 = new AnimalContainer with Dog
val ex2 = new AnimalContainer with Cat
ex1.doAnimalStuff
ex2.doAnimalStuff
}
В терминах шаблона стратегии тип self в стратегии указывает, что он должен быть смешан с конкретной реализацией какого-либо алгоритма.
Ответ 2
Это может выглядеть так: шаблон дизайна в scala":
Как и любой язык, где функции являются первоклассными объектами или где закрыты, шаблон стратегии очевиден.
Напр. рассмотрим пример "налогообложения":
trait TaxPayer
case class Employee(sal: Long) extends TaxPayer
case class NonProfitOrg(funds: BigInt) extends TaxPayer
//Consider a generic tax calculation function. (It can be in TaxPayer also).
def calculateTax[T <: TaxPayer](victim: T, taxingStrategy: (T => long)) = {
taxingStrategy(victim)
}
val employee = new Employee(1000)
//A strategy to calculate tax for employees
def empStrategy(e: Employee) = Math.ceil(e.sal * .3) toLong
calculateTax(employee, empStrategy)
val npo = new NonProfitOrg(100000000)
//The tax calculation strategy for npo is trivial, so we can inline it
calculateTax(nonProfit, ((t: TaxPayer) => 0)
чтобы я мог выбрать конкретную реализацию абстрактного понятия во время выполнения.
Здесь вы используете верхнюю границу, чтобы ограничить специализации T в подклассах этими подтипами TaxPayer
.
Ответ 3
Начиная с Java, мне все еще нравится синтаксис стиля OO. Я также просто смотрю первую часть Deraling Scalaz (Disclaimer)) и использовал это как небольшое упражнение, чтобы продемонстрировать себе концепции Pimp My Library и Implicits. Я подумал, что могу поделиться своими выводами. В общем, есть немного больше накладных расходов на программирование при настройке таким образом, но я лично считаю, что использование чище.
Этот первый фрагмент демонстрирует добавление паттерна Pimp My Library.
trait TaxPayer
/**
* This is part of the Pimp My Library pattern which converts any subclass of
* TaxPayer to type TaxPayerPimp
*/
object TaxPayer {
implicit def toTaxPayerPimp[T <: TaxPayer](t: T) : TaxPayerPimp[T] =
new TaxPayerPimp[T] {
val taxPayer = t
}
}
/**
* This is an extra trait defining tax calculation which will be overloaded by
* individual TaxCalculator strategies.
*/
trait TaxCalculator[T <: TaxPayer] {
def calculate(t: T) : Long
}
/**
* This is the other part of the Pimp My Library pattern and is analogus to
* Scalaz Identity trait.
*/
trait TaxPayerPimp[T <: TaxPayer] {
val taxPayer: T
def calculateTax(tc: TaxCalculator[T]) : Long = tc.calculate(taxPayer)
}
case class Employee(sal: Long) extends TaxPayer
/**
* This is the employee companion object which defines the TaxCalculator
* strategies.
*/
object Employee {
object DefaultTaxCalculator extends TaxCalculator[Employee] {
def calculate(e: Employee) = Math.ceil(e.sal * .3) toLong
}
object BelgianTaxCalculator extends TaxCalculator[Employee] {
def calculate(e: Employee) = Math.ceil(e.sal * .5) toLong
}
}
case class NonProfitOrg(funds: BigInt) extends TaxPayer
/**
* This is the NonProfitOrg companion which defines it own TaxCalculator
* strategies.
*/
object NonProfitOrg {
object DefaultTaxCalculator extends TaxCalculator[NonProfitOrg] {
def calculate(n: NonProfitOrg) = 0
}
}
object TaxPayerMain extends Application {
//The result is a more OO style version of VonC example
val employee = new Employee(1000)
employee.calculateTax(Employee.DefaultTaxCalculator)
employee.calculateTax(Employee.BelgianTaxCalculator)
val npo = new NonProfitOrg(100000000)
npo.calculateTax(NonProfitOrg.DefaultTaxCalculator)
//Note the type saftey, this will not compile
npo.calculateTax(Employee.DefaultTaxCalculator)
}
Мы можем принять это немного дальше, используя implicits.
trait TaxPayer
object TaxPayer {
implicit def toTaxPayerPimp[T <: TaxPayer](t: T) : TaxPayerPimp[T] =
new TaxPayerPimp[T] {
val taxPayer = t
}
}
trait TaxCalculator[T <: TaxPayer] {
def calculate(t: T) : Long
}
/**
* Here we've added an implicit to the calculateTax function which tells the
* compiler to look for an implicit TaxCalculator in scope.
*/
trait TaxPayerPimp[T <: TaxPayer] {
val taxPayer: T
def calculateTax(implicit tc: TaxCalculator[T]) : Long = tc.calculate(taxPayer)
}
case class Employee(sal: Long) extends TaxPayer
/**
* Here we've added implicit to the DefaultTaxCalculator. If in scope
* and the right type, it will be implicitely used as the parameter in the
* TaxPayerPimp.calculateTax function.
*
*
*/
object Employee {
implicit object DefaultTaxCalculator extends TaxCalculator[Employee] {
def calculate(e: Employee) = Math.ceil(e.sal * .3) toLong
}
object BelgianTaxCalculator extends TaxCalculator[Employee] {
def calculate(e: Employee) = Math.ceil(e.sal * .5) toLong
}
}
/**
* Added implicit to the DefaultTaxCalculator...
*/
case class NonProfitOrg(funds: BigInt) extends TaxPayer
object NonProfitOrg {
implicit object DefaultTaxCalculator extends TaxCalculator[NonProfitOrg] {
def calculate(n: NonProfitOrg) = 0
}
}
object TaxPayer2 extends Application {
println("TaxPayer2")
val taxPayer = new Employee(1000)
//Now the call to calculateTax will
//implicitely use Employee.DefaultTaxCalculator
taxPayer.calculateTax
//But if we want, we can still explicitely pass in the BelgianTaxCalculator
taxPayer.calculateTax(Employee.BelgianTaxCalculator)
val npo = new NonProfitOrg(100000000)
//implicitely uses NonProfitOrg.defaultCalculator
npo.calculateTax
}