Базовый подход
Если все методы в интерфейсе компоновщика (за исключением, может быть, build
) просто изменяют экземпляр компоновщика и возвращают this
, тогда они могут быть абстрагированы как Builder => Unit
функции,Это верно для NettyChannelBuilder
, если я не ошибаюсь.В этом случае вы хотите объединить набор этих Builder => Unit
в один Builder => Unit
, который последовательно запускает исходные.
Вот прямая реализация этой идеи для NettyChannelBuilder
:
object Builder {
type Input = NettyChannelBuilder
type Output = ManagedChannel
case class Op(run: Input => Unit) {
def and(next: Op): Op = Op { in =>
this.run(in)
next.run(in)
}
def runOn(in: Input): Output = {
run(in)
in.build()
}
}
// combine several ops into one
def combine(ops: Op*): Op = Op(in => ops.foreach(_.run(in)))
// wrap methods from the builder interface
val addTransportSecurity: Op = Op(_.useTransportSecurity())
def addSslContext(sslContext: SslContext): Op = Op(_.sslContext(sslContext))
}
И вы можете использовать ее следующим образом:
val builderPipeline: Builder.Op =
Builder.addTransportSecurity and
Builder.addSslContext(???)
builderPipeline runOn NettyChannelBuilder.forAddress("localhost", 80)
Reader Monad
Здесь также можно использовать монаду Reader.Монада считывателя позволяет объединить две функции Context => A
и A => Context => B
в Context => B
.Конечно, каждая функция, которую вы хотите объединить, это просто Context => Unit
, где Context
равно NettyChannelBuilder
.Но метод build
- это NettyChannelBuilder => ManagedChannel
, и мы можем добавить его в конвейер с помощью этого подхода.
Вот реализация без сторонних библиотек:
object MonadicBuilder {
type Context = NettyChannelBuilder
case class Op[Result](run: Context => Result) {
def map[Final](f: Result => Final): Op[Final] =
Op { ctx =>
f(run(ctx))
}
def flatMap[Final](f: Result => Op[Final]): Op[Final] =
Op { ctx =>
f(run(ctx)).run(ctx)
}
}
val addTransportSecurity: Op[Unit] = Op(_.useTransportSecurity())
def addSslContext(sslContext: SslContext): Op[Unit] = Op(_.sslContext(sslContext))
val build: Op[ManagedChannel] = Op(_.build())
}
Этоудобно использовать его с синтаксисом для понимания:
val pipeline = for {
_ <- MonadicBuilder.addTransportSecurity
sslContext = ???
_ <- MonadicBuilder.addSslContext(sslContext)
result <- MonadicBuilder.build
} yield result
val channel = pipeline run NettyChannelBuilder.forAddress("localhost", 80)
Этот подход может быть полезен в более сложных сценариях, когда некоторые методы возвращают другие переменные, которые следует использовать в последующих шагах.Но для NettyChannelBuilder
, где большинство функций просто Context => Unit
, на мой взгляд, это только добавляет ненужный шаблон.
Что касается других монад, то основная цель State - отслеживать изменения ссылки на объект, и это полезно, потому что этот объект обычно неизменен.Для изменяемого объекта Reader работает просто отлично.
Свободная монада также используется в аналогичных сценариях, но добавляет гораздо больше стандартного шаблона, и ее обычный сценарий использования - когда вы хотите построить объект абстрактного синтаксического дерева с некоторымидействия / команды, а затем выполнить его с разными интерпретаторами.
Универсальный компоновщик
Довольно просто адаптировать два предыдущих подхода для поддержки любого компоновщика или изменяемого класса в целом.Хотя без создания отдельных оберток для методов мутации, шаблон для его использования значительно возрастает.Например, с подходом монадического строителя:
class GenericBuilder[Context] {
case class Op[Result](run: Context => Result) {
def map[Final](f: Result => Final): Op[Final] =
Op { ctx =>
f(run(ctx))
}
def flatMap[Final](f: Result => Op[Final]): Op[Final] =
Op { ctx =>
f(run(ctx)).run(ctx)
}
}
def apply[Result](run: Context => Result) = Op(run)
def result: Op[Context] = Op(identity)
}
Использование его:
class Person {
var name: String = _
var age: Int = _
var jobExperience: Int = _
def getYearsAsAnAdult: Int = (age - 18) max 0
override def toString = s"Person($name, $age, $jobExperience)"
}
val build = new GenericBuilder[Person]
val builder = for {
_ <- build(_.name = "John")
_ <- build(_.age = 36)
adultFor <- build(_.getYearsAsAnAdult)
_ <- build(_.jobExperience = adultFor)
result <- build.result
} yield result
// prints: Person(John, 36, 18)
println(builder.run(new Person))