用迭代法进行图像重构_用Kleisli组成进行重构

用迭代法进行图像重构

一段时间以来,我们一直在维护一个处理XML和JSON数据的应用程序。 通常,维护包括修复缺陷和添加次要功能,但有时需要重构旧代码。

例如,考虑一个通过路径提取XML节点的函数:

import scala.xml.{Node => XmlNode}
 
def getByPath(path: List[String], root: XmlNode): Option[XmlNode] =
  path match {
    case name::names =>
      for {
        node1 <- root.child.find(_.label == name)
        node2 <- getByPath(names, node1)
      } yield node2
    case _ => Some(root)
  }

此功能工作正常,但需求有所变化,现在我们需要:

  • 从JSON和其他类似树的数据结构中提取节点,而不仅仅是XML
  • 如果找不到节点,则返回描述性错误消息

这篇文章解释了如何重构getByPath以满足新的要求。

用Kleisli组成进行重构

让我们分解出一段代码,该代码创建一个函数来按名称提取子节点。 我们可以将其命名为createFunctionToExtractChildNodeByName ,但是为了简洁起见,我们将其命名为child。

val child: String => XmlNode => Option[XmlNode] =
  name => node => node.child.find(_.label == name)

现在我们可以进行以下关键观察:我们的getByPath是提取子节点的函数的顺序组合,下面的代码显示了这种组合的实现:

def compose(getChildA: XmlNode => Option[XmlNode],
            getChildB: XmlNode => Option[XmlNode]): XmlNode => Option[XmlNode] =
  node => for {
            a  <- getChildA(node)
            ab <- getChildB(a)
          } yield ab

幸运的是, Scalaz库提供了一种更通用的方式来编写函数A => M [A],其中M是单子。 该库定义了Kleisli [M,A,B]A => M [B]的包装器,该包装器的方法> =>以与andThe链接常规函数相同的方式链接Kleisli包装器。 我们将这种链称为Kleisli组成。 下面的代码提供了一个合成示例:

val getChildA: XmlNode => Option[XmlNode] = child(“a”)
val getChildB: XmlNode => Option[XmlNode] = child(“b”)
 
import scalaz._, Scalaz._
 
val getChildAB: Kleisli[Option, XmlNode, XmlNode] =
  Kleisli(getChildA) >=> Kleisli(getChildB)

注意我们在这里使用的无点样式。 对于函数式程序员来说,将函数编写为其他函数的组合是很常见的,而从不提及它们将应用于的实际参数。

Kleisli组合正是实现getByPath作为提取子节点的函数组合所需要的。

import scalaz._, Scalaz._
 
def getByPath(path: List[String], root: XmlNode): Option[XmlNode] =
  path.map(name => Kleisli(child(name)))
    .fold(Kleisli.ask[Option, XmlNode]) {_ >=> _}
    .run(root)

请注意,使用Kleisli.ask [Option,XmlNode]作为fold的中性元素。 当pathNil时,我们需要这个中立元素来处理特殊情况。 Kleisli.ask [Option,XmlNode]只是从任何节点Some(node)的函数的别名。

通过XmlNode抽象

让我们概括一下我们的解决方案,并通过XmlNode对其进行抽象。 我们可以将其重写为以下通用函数:

def getByPathGeneric[A](child: String => A => Option[A])
                       (path: List[String], root: A): Option[A] =
  path.map(name => Kleisli(child(name)))
    .fold(Kleisli.ask[Option, A]) {_ >=> _}
    .run(root)

现在,我们可以重用此泛型函数从JSON中提取节点(我们在此处使用json4s ):

import org.json4s._
 
def getByPath(path: List[String], root: JValue): Option[JValue] = {
  val child: String => JValue => Option[JValue] = name => json =>
    json match {
      case JObject(obj) => obj collectFirst {case (k, v) if k == name => v}
      case _ => None
    }
  getByPathGeneric(child)(path, root)
}

请注意,我们编写了一个新函数child: JValue => Option [JValue] ,用于处理JSON而不是XML,但是getByPathGeneric保持不变,并且可以处理XML和JSON。

期权抽象

我们可以进一步推广getByPathGeneric,并使用Scalaz通过Option对其进行抽象,后者提供scalaz.Monad [Option]的实例。 因此,我们可以按照以下方式重写getByPathGeneric

import scalaz._, Scalaz._
 
def getByPathGeneric[M[_]: Monad, A](child: String => A => M[A])
                                    (path: List[String], root: A): M[A]=
  path.map(name => Kleisli(child(name)))
    .fold(Kleisli.ask[M, A]) {_ >=> _}
    .run(root)

现在,我们可以实现我们的getByPathGenericgetByPath:

def getByPath(path: List[String], root: XmlNode): Option[XmlNode] = {
  val child: String => XmlNode => Option[XmlNode] = name => node =>
    node.child.find(_.label == name)
  getByPathGeneric(child)(path, root)
}

接下来,如果找不到该节点,则可以重用getByPathGeneric返回错误消息。

为此,我们将使用scalaz。\ / (又名disjunction ),它是scala.Either的单声道右偏版本。 最重要的是,Scalaz为隐式类OptionOps提供了toRightDisjunction [B](b:B)方法 ,该方法将Option [A]转换为scalaz.B \ / A,从而Some(a)变为Right(a)None变为Left (b) 。 您可以在其他博客中找到有关\ /的更多信息。

因此,我们可以编写一个函数,该函数重用getByPathGeneric ,如果找不到该节点,则返回错误消息,而不是None

type Result[A] = String\/A
 
def getResultByPath(path: List[String], root: XmlNode): Result[XmlNode] = {
  val child: String => XmlNode => Result[XmlNode] = name => node =>
    node.child.find(_.label == name).toRightDisjunction(s"$name not found")
  getByPathGeneric(child)(path, root)
}

结论

原始的getByPath函数仅处理XML数据,如果未找到该节点,则返回None。 我们还需要它来处理JSON并返回描述性消息,而不是None

我们已经看到了使用Scalaz提供的Kleisli组合如何分解出泛型函数getByPathGeneric ,我们使用泛型(为了支持JSON)和取(对Option进行泛化)进一步对其进行了抽象。

翻译自: https://www.javacodegeeks.com/2015/09/refactoring-with-kleisli-composition.html

用迭代法进行图像重构

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值