如何将构建器模式转换为函数式实现?
How To Convert the Builder Pattern to a Functional Implementation?
grpc-java
库是利用通用构建器模式创建具有特定属性的对象的库的一个很好的例子:
val sslContext = ???
val nettyChannel : NettyChannel =
NettyChannelBuilder
.forAddress(hostIp, hostPort)
.useTransportSecurity()
.sslContext(sslContext)
.build
给定一个使用这种模式的库,如何包装它以便可以使用适当的功能 API? 我认为 monad 是合适的使用的工具。
基本的第一次尝试如下所示:
val updateBuilder : (NettyChannelBuilder => Unit) => NettyChannelBuilder => NettyChannelBuilder =
updateFunc => builder => {
updateFunc(builder)
builder
}
val addTransportSecurity : NettyChannelBuilder => Unit =
(_ : NettyChannelBuilder).useTransportSecurity()
val addSslContext : NettyChannelBuilder => Unit =
builder => {
val sslContext = ???
builder sslContext sslContext
}
虽然这个方法很冗长,但至少可以组合:
val builderPipeline : NettyChannelBuilder => NettyChannelBuilder =
updateBuilder(addTransportSecurity) andThen updateBuilder(addSslContext)
val nettyChannel =
builderPipeline(NettyChannelBuilder.forAddress(hostIp, hostPort)).build
一个约束:不使用 scalaz
、cats
或其他一些第 3 方库。仅 scala 语言 "stuff".
注意:grpc只是一个示例用例,不是问题的重点...
提前感谢您的考虑和回复。
一个非常简单的函数式方法是有一个案例 class 收集配置,并有更新它们的值并传递它的方法,以便它可以在最后构建:
case class MyNettyChannel( ip: String, port: Int,
transportSecurity: Boolean,
sslContext: Option[SslContext] ) {
def forAddress(addrIp: String, addrPort: Int) = copy(ip = addrIp, port = addrPort)
def withTransportSecurity = copy(transportSecurity = true)
def withoutTransportSecurity = copy(transportSecurity = false)
def withSslContext(ctx: SslContext) = copy(sslContext = Some(ctx))
def build: NettyChannel = {
/* create the actual instance using the existing builder */
}
}
object MyNettyChannel {
val default = MyNettyChannel("127.0.0.1", 80, false, None)
}
val nettyChannel = MyNettyChannel.default
.forAddress(hostIp, hostPort)
.withTransportSecurity
.withSslContext(ctx)
.build
一种类似的方法(无需首先创建复制方法)是使用镜头,例如使用 quicklens 库:
val nettyChannel = MyNettyChannel.default
.modify(_.ip) .setTo(hostIp)
.modify(_.port) .setTo(1234)
.modify(_.transportSecurity).setTo(true)
.modify(_.sslContext) .setTo(ctx)
.build
我知道我们拒绝了 cats et al.
但我决定 post 这样做,首先,老实说,这是我自己的一个练习,其次,因为本质上这些库只是聚合 "common" 类型化的函数式 构造和模式。
毕竟,您会考虑从 vanilla Java/Scala 编写一个 HTTP 服务器,还是会拿一个经过实战测试的现成服务器? (对不起传福音)
无论如何,如果您确实需要,您可以用自己开发的实现替换他们的重量级实现。
我将在下面展示,想到的两个方案,第一个使用 Reader
monad,第二个使用 State
单子。我个人认为第一种方法比第二种方法笨拙一些,但它们都不太好看。我想一个更有经验的从业者可以比我做得更好。
在此之前,我发现以下内容比较有趣:Semicolons vs Monads
代码:
我定义了 Java Bean:
public class Bean {
private int x;
private String y;
public Bean(int x, String y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "Bean{" +
"x=" + x +
", y='" + y + '\'' +
'}';
}
}
和建造者:
public final class BeanBuilder {
private int x;
private String y;
private BeanBuilder() {
}
public static BeanBuilder aBean() {
return new BeanBuilder();
}
public BeanBuilder withX(int x) {
this.x = x;
return this;
}
public BeanBuilder withY(String y) {
this.y = y;
return this;
}
public Bean build() {
return new Bean(x, y);
}
}
现在是 Scala 代码:
import cats.Id
import cats.data.{Reader, State}
object Boot extends App {
val r: Reader[Unit, Bean] = for {
i <- Reader({ _: Unit => BeanBuilder.aBean() })
n <- Reader({ _: Unit => i.withX(12) })
b <- Reader({ _: Unit => n.build() })
_ <- Reader({ _: Unit => println(b) })
} yield b
private val run: Unit => Id[Bean] = r.run
println("will come before the value of the bean")
run()
val state: State[BeanBuilder, Bean] = for {
_ <- State[BeanBuilder, BeanBuilder]({ b: BeanBuilder => (b, b.withX(13)) })
_ <- State[BeanBuilder, BeanBuilder]({ b: BeanBuilder => (b, b.withY("look at me")) })
bean <- State[BeanBuilder, Bean]({ b: BeanBuilder => (b, b.build()) })
_ <- State.pure(println(bean))
} yield bean
println("will also come before the value of the bean")
state.runA(BeanBuilder.aBean()).value
}
由于这些单子求值的惰性,输出是:
will come before the value of the bean
Bean{x=12, y='null'}
will also come before the value of the bean
Bean{x=13, y='look at me'}
基本方法
如果构建器接口中的所有方法(也许build
本身除外)只是改变构建器实例和return this
,那么它们可以抽象为Builder => Unit
函数。如果我没记错的话,NettyChannelBuilder
也是如此。在这种情况下,您要做的是将一堆 Builder => Unit
组合成一个 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单子
这里也可以使用 Reader monad。 Reader monad 允许将两个函数 Context => A
和 A => Context => B
组合成 Context => B
。当然,您要在此处组合的每个函数都只是 Context => Unit
,其中 Context
是 NettyChannelBuilder
。但是build
方法是NettyChannelBuilder => ManagedChannel
,我们可以用这个方法把它加入到pipeline中。
这是一个没有任何第三方库的实现:
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())
}
配合for-comprehension语法使用更方便:
val pipeline = for {
_ <- MonadicBuilder.addTransportSecurity
sslContext = ???
_ <- MonadicBuilder.addSslContext(sslContext)
result <- MonadicBuilder.build
} yield result
val channel = pipeline run NettyChannelBuilder.forAddress("localhost", 80)
这种方法在更复杂的场景中很有用,当一些方法return其他变量时,应该在后面的步骤中使用。但是对于 NettyChannelBuilder
,其中大多数功能只是 Context => Unit
,在我看来,它只会添加不必要的样板。
对于其他 monad,State 的主要目的是跟踪对对象引用的更改,它很有用,因为该对象通常是不可变的。对于可变对象 Reader 工作得很好。
Free monad也用在类似的场景中,但是它增加了更多的样板,它通常的使用场景是当你想用一些actions/commands构建一个抽象语法树对象,然后用不同的口译员。
通用生成器
调整前两种方法以支持任何构建器或一般的可变 class 非常简单。尽管没有为变异方法创建单独的包装器,但使用它的样板文件增长了很多。例如,使用 monadic 构建器方法:
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))
grpc-java
库是利用通用构建器模式创建具有特定属性的对象的库的一个很好的例子:
val sslContext = ???
val nettyChannel : NettyChannel =
NettyChannelBuilder
.forAddress(hostIp, hostPort)
.useTransportSecurity()
.sslContext(sslContext)
.build
给定一个使用这种模式的库,如何包装它以便可以使用适当的功能 API? 我认为 monad 是合适的使用的工具。
基本的第一次尝试如下所示:
val updateBuilder : (NettyChannelBuilder => Unit) => NettyChannelBuilder => NettyChannelBuilder =
updateFunc => builder => {
updateFunc(builder)
builder
}
val addTransportSecurity : NettyChannelBuilder => Unit =
(_ : NettyChannelBuilder).useTransportSecurity()
val addSslContext : NettyChannelBuilder => Unit =
builder => {
val sslContext = ???
builder sslContext sslContext
}
虽然这个方法很冗长,但至少可以组合:
val builderPipeline : NettyChannelBuilder => NettyChannelBuilder =
updateBuilder(addTransportSecurity) andThen updateBuilder(addSslContext)
val nettyChannel =
builderPipeline(NettyChannelBuilder.forAddress(hostIp, hostPort)).build
一个约束:不使用 scalaz
、cats
或其他一些第 3 方库。仅 scala 语言 "stuff".
注意:grpc只是一个示例用例,不是问题的重点...
提前感谢您的考虑和回复。
一个非常简单的函数式方法是有一个案例 class 收集配置,并有更新它们的值并传递它的方法,以便它可以在最后构建:
case class MyNettyChannel( ip: String, port: Int,
transportSecurity: Boolean,
sslContext: Option[SslContext] ) {
def forAddress(addrIp: String, addrPort: Int) = copy(ip = addrIp, port = addrPort)
def withTransportSecurity = copy(transportSecurity = true)
def withoutTransportSecurity = copy(transportSecurity = false)
def withSslContext(ctx: SslContext) = copy(sslContext = Some(ctx))
def build: NettyChannel = {
/* create the actual instance using the existing builder */
}
}
object MyNettyChannel {
val default = MyNettyChannel("127.0.0.1", 80, false, None)
}
val nettyChannel = MyNettyChannel.default
.forAddress(hostIp, hostPort)
.withTransportSecurity
.withSslContext(ctx)
.build
一种类似的方法(无需首先创建复制方法)是使用镜头,例如使用 quicklens 库:
val nettyChannel = MyNettyChannel.default
.modify(_.ip) .setTo(hostIp)
.modify(_.port) .setTo(1234)
.modify(_.transportSecurity).setTo(true)
.modify(_.sslContext) .setTo(ctx)
.build
我知道我们拒绝了 cats et al.
但我决定 post 这样做,首先,老实说,这是我自己的一个练习,其次,因为本质上这些库只是聚合 "common" 类型化的函数式 构造和模式。
毕竟,您会考虑从 vanilla Java/Scala 编写一个 HTTP 服务器,还是会拿一个经过实战测试的现成服务器? (对不起传福音)
无论如何,如果您确实需要,您可以用自己开发的实现替换他们的重量级实现。
我将在下面展示,想到的两个方案,第一个使用 Reader
monad,第二个使用 State
单子。我个人认为第一种方法比第二种方法笨拙一些,但它们都不太好看。我想一个更有经验的从业者可以比我做得更好。
在此之前,我发现以下内容比较有趣:Semicolons vs Monads
代码:
我定义了 Java Bean:
public class Bean {
private int x;
private String y;
public Bean(int x, String y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "Bean{" +
"x=" + x +
", y='" + y + '\'' +
'}';
}
}
和建造者:
public final class BeanBuilder {
private int x;
private String y;
private BeanBuilder() {
}
public static BeanBuilder aBean() {
return new BeanBuilder();
}
public BeanBuilder withX(int x) {
this.x = x;
return this;
}
public BeanBuilder withY(String y) {
this.y = y;
return this;
}
public Bean build() {
return new Bean(x, y);
}
}
现在是 Scala 代码:
import cats.Id
import cats.data.{Reader, State}
object Boot extends App {
val r: Reader[Unit, Bean] = for {
i <- Reader({ _: Unit => BeanBuilder.aBean() })
n <- Reader({ _: Unit => i.withX(12) })
b <- Reader({ _: Unit => n.build() })
_ <- Reader({ _: Unit => println(b) })
} yield b
private val run: Unit => Id[Bean] = r.run
println("will come before the value of the bean")
run()
val state: State[BeanBuilder, Bean] = for {
_ <- State[BeanBuilder, BeanBuilder]({ b: BeanBuilder => (b, b.withX(13)) })
_ <- State[BeanBuilder, BeanBuilder]({ b: BeanBuilder => (b, b.withY("look at me")) })
bean <- State[BeanBuilder, Bean]({ b: BeanBuilder => (b, b.build()) })
_ <- State.pure(println(bean))
} yield bean
println("will also come before the value of the bean")
state.runA(BeanBuilder.aBean()).value
}
由于这些单子求值的惰性,输出是:
will come before the value of the bean
Bean{x=12, y='null'}
will also come before the value of the bean
Bean{x=13, y='look at me'}
基本方法
如果构建器接口中的所有方法(也许build
本身除外)只是改变构建器实例和return this
,那么它们可以抽象为Builder => Unit
函数。如果我没记错的话,NettyChannelBuilder
也是如此。在这种情况下,您要做的是将一堆 Builder => Unit
组合成一个 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单子
这里也可以使用 Reader monad。 Reader monad 允许将两个函数 Context => A
和 A => Context => B
组合成 Context => B
。当然,您要在此处组合的每个函数都只是 Context => Unit
,其中 Context
是 NettyChannelBuilder
。但是build
方法是NettyChannelBuilder => ManagedChannel
,我们可以用这个方法把它加入到pipeline中。
这是一个没有任何第三方库的实现:
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())
}
配合for-comprehension语法使用更方便:
val pipeline = for {
_ <- MonadicBuilder.addTransportSecurity
sslContext = ???
_ <- MonadicBuilder.addSslContext(sslContext)
result <- MonadicBuilder.build
} yield result
val channel = pipeline run NettyChannelBuilder.forAddress("localhost", 80)
这种方法在更复杂的场景中很有用,当一些方法return其他变量时,应该在后面的步骤中使用。但是对于 NettyChannelBuilder
,其中大多数功能只是 Context => Unit
,在我看来,它只会添加不必要的样板。
对于其他 monad,State 的主要目的是跟踪对对象引用的更改,它很有用,因为该对象通常是不可变的。对于可变对象 Reader 工作得很好。
Free monad也用在类似的场景中,但是它增加了更多的样板,它通常的使用场景是当你想用一些actions/commands构建一个抽象语法树对象,然后用不同的口译员。
通用生成器
调整前两种方法以支持任何构建器或一般的可变 class 非常简单。尽管没有为变异方法创建单独的包装器,但使用它的样板文件增长了很多。例如,使用 monadic 构建器方法:
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))