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
或其他第三方库。只有scala语言“东西”。
注意:grpc只是一个示例用例,不是问题的重点...
预先感谢您的考虑和答复。
答案 0 :(得分:3)
如果构建器接口中的所有方法(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)
您可以像这样使用它:
Context => A
在这里也可以使用Reader monad。 Reader monad允许将两个功能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
的{{1}},我认为这只会增加不必要的样板。
对于其他monad,State的主要目的是跟踪对对象引用的更改,这很有用,因为该对象通常是不可变的。对于可变对象,Reader可以正常工作。
Free monad也用于类似情况,但是它增加了更多样板,并且其通常的使用情况是当您要使用一些动作/命令构建抽象语法树对象然后使用不同的解释器执行它时。< / p>
很容易将前面的两种方法改编为支持任何构建器或可变类。尽管没有为突变方法创建单独的包装器,但使用它的样板却增长了很多。例如,使用monadic builder方法:
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))
答案 1 :(得分:1)
我知道我们没有说cats et al.
,但是我决定首先将其发布,这是我本人的第二次练习,因为从本质上讲,这些库只是聚合“通用” 类型的函数的构造和模式。
毕竟,您是否会考虑使用Vanilla Java / Scala编写HTTP服务器,还是会立即进行一场经过测试的战斗? (对不起,传福音)
无论如何,如果您确实想要的话,可以用自己开发的一个替代它们的重量级实现。
下面我将介绍两个想到的方案,第一个使用Reader
monad ,第二个使用State
monad 。我个人发现第一种方法比第二种方法更笨拙,但是它们在外观上都不太漂亮。我想一个经验丰富的从业者可以比我做得更好。
在此之前,我发现以下内容非常有趣: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'}
答案 2 :(得分:0)
一种非常简单的功能性方法是使用一个case类来收集配置,并具有更新其值并将其传递的方法,以便可以在最后构建它:
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