it-roy-ru.com

Как преобразовать шаблон Builder в функциональную реализацию?

Библиотека 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 или какую-либо другую стороннюю библиотеку. Только скала на языке "мелочи".

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

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

7
Ramon J Romero y Vigil

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

Если все методы в интерфейсе компоновщика (за исключением, возможно, самого 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. Монада 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))
2
Kolmar

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

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

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

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

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


Код:

Я определил Java-бин:

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);
    }
}

Теперь для скала-кода:

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'}
1
Yaneeve

Очень простой функциональный подход заключается в наличии класса 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
0
Roberto Bonvallet