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