Как преобразовать шаблон Builder в функциональную реализацию? - PullRequest
0 голосов
/ 17 октября 2018

Библиотека grpc-java является хорошим примером библиотеки, которая использует общий шаблон компоновщика для создания объектов с определенными свойствами:

val sslContext = ???

val nettyChannel : NettyChannel = 
  NettyChannelBuilder
    .forAddress(hostIp, hostPort)
    .useTransportSecurity()
    .sslContext(sslContext) 
    .build

Если библиотека может использовать этот шаблон, как она может это сделать?быть обернутым так, чтобы можно было сделать доступным надлежащий функциональный API? Я думаю, что монада является подходящим инструментом для использования.

Базовая первая попытка будет выглядеть следующим образом:

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" на языке scala.

Примечание: grpc - это просто пример использования, а не основной вопрос ...

Заранее благодарим вас за внимание и ответ.

Ответы [ 3 ]

0 голосов
/ 17 октября 2018

Я знаю, что мы сказали нет cats et al., но я решил опубликовать это, во-первых, честно, как упражнение для себя, а во-вторых, поскольку по сути эти библиотеки просто объединяют "общий" типизированный функционал конструкции и узоры.

В конце концов, вы когда-нибудь задумывались о написании HTTP-сервера из vanilla Java / Scala или взяли бы готовый к бою сервер?(извините за евангелизацию)

Несмотря на это, вы могли бы заменить их тяжеловесную реализацию собственной, если вы действительно этого хотели.

Ниже я приведу две схемы, которые пришли на умпервый использует Reader монаду , второй использует State монаду .Я лично считаю, что первый подход немного более неуклюжий, чем второй, но оба они не слишком привлекательны для глаз.Я полагаю, что более опытный практикующий мог бы справиться с этим лучше, чем я.

До этого я нахожу довольно интересным следующее: Точки с запятой против монад


Код:

Я определил 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'}
0 голосов
/ 18 октября 2018

Базовый подход

Если все методы в интерфейсе компоновщика (за исключением, может быть, 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))
0 голосов
/ 17 октября 2018

Очень простой функциональный подход состоит в том, чтобы иметь класс 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
...