如何将生成器模式转换为功能实现?

时间:2018-10-17 13:27:38

标签: scala functional-programming builder grpc-java

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

一个约束:不能使用scalazcats或其他第三方库。只有scala语言“东西”。

注意:grpc只是一个示例用例,不是问题的重点...

预先感谢您的考虑和答复。

3 个答案:

答案 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。 Reader monad允许将两个功能A => Context => BContext => B组合成Context => Unit。当然,您要在此处组合的每个功能都只是Context,其中NettyChannelBuilderbuild。但是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