函数参数的定义原则

函数参数的定义原则

sjmyuan 88 2022-10-16

函数参数是函数签名的重要组成部分,它决定了函数的调用方式。在定义参数的时候,有的人会根据调用者当前已有的数据进行定义,有的人则会根据函数业务逻辑需要的数据进行定义。哪种方式更好呢?

先上例子

def getHead(list: List[String]): Option[String] = list.headOption

def parseStringToInt(x: Option[String]): Option[Int] = x.flatMap(_.toIntOption)

val list = List("1", "2", "3")
parseStringToInt(getHead(list)) // Some(1)

这里因为getHead​会返回一个Option[String]​,我们将parseStringToInt​的参数定义为Option[String]​。

这样定义会有一个问题,如果其他调用者返回的不是Option[String]​怎么办?我们要么将其转换为Option[String]​,要么重新定义一个parseStringToInt​。例如

def getAge(name: String): Either[String, String] = name match {
  case "Tom" => Right("29")
  case "Jim" => Right("30")
  case _ => Left("Unknown Name")
}

parseStringToInt(getAge("Tom")) // Compilation Failed

parseStringToInt(getAge("Tom").toOption) // Some(29)

def parseStringToIntForEither(x: Either[String, String]): Option[Int] = x.toOption.flatMap(_.toIntOption) 

parseStringToIntForEither(getAge("Tom")) // Some(29)

反之,如果我们根据parseStringToInt​的业务逻辑将参数定义为String​,就可以避免上面的问题

def parseStringToInt(x: String): Option[Int] = x.toIntOption

getHead(list).flatMap(parseStringToInt) // Some(1)

getAge("Tom").flatMap(x => parseStringToInt(x).toRight("Can not be pased to Int")) // Right(29)

我们可以用依赖反转原则做一个类比,函数参数的定义应该依赖函数的业务逻辑而不是函数的调用逻辑。函数的业务逻辑是指函数所要实现的功能,像抽象接口一样相对稳定,在上面的例子中是指如何将String​转换为Int​。函数的调用逻辑是指调用函数的各种场景,像具体实现一样的变化频繁,在上面的例子中是指将getHead​和getAge​的返回值转换为Int​。如果函数参数的定义依赖函数的调用逻辑,各个调用场景所需要的参数可能不一样,这样的代码很被复用。

我们还可以将上述原则引申到微服务,微服务API参数的定义应该依赖于该服务的业务能力而不是消费端的调用方式。如果不同的消费端由不同的团队维护,他们定义的参数名称,参数个数或参数类型很可能不一样,我们的服务需要做很多的适配,这将是一项十分痛苦的工作。

注意这里我们讨论的只有函数参数,函数返回类型并不适用上述原则,它同时受业务逻辑,调用逻辑和架构设计的影响。以parseStringToInt​为例,如果我们希望从架构层面统一返回类型,那它的返回类型可能会变为IO[Int]​ 或 Task[Int]​。如果有的调用方希望打印错误信息,那它的返回类型可能会变为Either[String, Int]​或 Try[Int]​。如果我们的业务逻辑没有任何的异常,那它的返回类型可能会变为Int​。