弃用Builder模式
当构造复杂对象时,需要很多参数,如果将所有参数都通过一个构造函数来传递,缺乏灵活性,但如果重载若干个带有不同参数的构造函数,代码就变得臃肿。Builder 模式可以简化构建过程。
在 Java 中 Builder模式 代码如下:
public class Person {
//'必选参数'
private String name;
//'以下都是可选参数'
private int gender;
private int age;
private int height;
private int weight;
//'私有构造函数,限制必须通过构造者构建对象'
private Person(Builder builder) {
this.name = builder.name;
this.gender = builder.gender;
this.age = builder.age;
this.height = builder.height;
this.weight = builder.weight;
}
//'构造者'
public static class Builder {
private String name;
private int gender;
private int age;
private int height;
private int weight;
//'必选参数必须在构造函数中传入'
public Builder(String name) {
this.name = name;
}
//'以下是每个非必要属性的设值函数,它返回构造者本身用于链式调用'
public Builder age(int age) {
this.age = age;
return this;
}
public Builder gender(int gender) {
this.gender = gender;
return this;
}
public Builder height(int height) {
this.height = height;
return this;
}
public Builder weight(int weight) {
this.weight = weight;
return this;
}
//'构建对象'
public Person build() {
return new Person(this);
}
}
然后就可以像这样构建Person
实例:
//'使用 Builder模式'
Person p = new Person.Builder("taylor")
.age(50)
.gender(1)
.weight(43)
.build();
//'使用构造函数'
Person p2 = new Person("taylor", 50, 1, 0, 43);
对比之下,Builder模式 有两个优势:
为参数标注语义:在Builder模式中,每个属性的赋值都是一个函数,函数名标注了属性语义。而直接使用构造函数时,很难分辨
50
,43
哪个是年龄,哪个是体重。可选参数:Builder模式中,除了必选参数,其他参数是可选的。但直接使用构造函数必须为所有参数赋值,比如上例中第四个参数身高被赋值为0。
但 Builder模式 也有代价,新增了一个中间类Builder
。
使用 Kotlin 的命名参数
+参数默认值
+数据类
语法,在没有任何副作用的情况下就能实现 Builder模式:
//'将Person定义为数据类'
data class Person(
var name: String,
//'为以下可选参数设置默认值'
var gender: Int = 1,
var age: Int= 0,
var height: Int = 0,
var weight: Int = 0
)
//'使用命名参数构建Person实例'
val p = Person(name = “taylor”,gender = 1,weight = 43)
关于数据类
、参数默认值
、命名参数
更详细的介绍可以点击这里
如果想增加参数约束条件可以调用require()
方法:
data class Person(
var name: String,
var gender: Int = 1,
var age: Int= 0,
var height: Int = 0,
var weight: Int = 0
){
//'在构造函数被调用的时候执行参数合法检查'
init {
require(name.isNotEmpty()){”name cant be empty“}
}
}
此时如果像下面这样构造 Person,则会抛出异常:
val p = Person(name="",gender = 1)
java.lang.IllegalArgumentException: name cant be empty
打印列表、map
调试程序时,经常需要打印列表内容,通常会这样打印:
for (String str:list) {
Log.v("test", "str="+str);
}
不同业务界面的数据类型不同,为了调试,这样的 for 循环就会散落在各处,而且列表内容会分若干条 log 输出,中间极有可能被别的log打断。
有没有一个函数可以打印包含任意数据类型的列表,并将列表内容组织成更具可读性的字符串?
用 Kotlin 的扩展函数
+泛型
+高阶函数
就能优雅地做到:
fun <T> Collection<T>.print(map: (T) -> String) =
StringBuilder("\n[").also { sb ->
//'遍历集合元素,通过 map 表达式将元素转换成感兴趣的字串,并独占一行'
this.forEach { e -> sb.append("\n\t${map(e)},") }
sb.append("\n]")
}.toString()
为集合的基类Collection
新增一个扩展函数,它是一个高阶函数,因为它的参数是另一个函数,该函数用 lambda 表示。再把集合元素抽象成泛型。通过StringBuilder
将所有集合内容拼接成一个自动换行的字符串。
写段测试代码看下效果:
data class Person(var name: String, var age: Int)
val persons = listOf(
Person("Peter", 16),
Person("Anna", 28),
Person("Anna", 23),
Person("Sonya", 39)
)
persons.print { "${it.name}_${it.age}" }.let { Log.v("test",it) }
打印结果如下:
V/test: [
Peter_16,
Anna_28,
Anna_23,
Sonya_39,
]
同样地,可以如法炮制一个打印 map 的扩展函数:
fun <K, V> Map<K, V?>.print(map: (V?) -> String): String =
StringBuilder("\n{").also { sb ->
this.iterator().forEach { entry ->
sb.append("\n\t[${entry.key}] = ${map(entry.value)}")
}
sb.append("\n}")
}.toString()
将 data 类转换成 map
有些数据类字段比较多,调试时,想把它们通通打印出来,在 Java 中,借助于 AndroidStudio 的 toString
功能倒是可以方便地生成可读性很高的字串:
public class Person {
private String name;
private int age;
@Override
public String toString() {
return ”Person{“ +
”name=‘“ + name + ’\” +
”, age=“ + age +
‘}’;
}
}
但是每新建一个数据类都要手动生成一个toString()
方法也挺麻烦。
利用 Kotlin 的 data class
可以省去这一步,但打印效果是所有字段都在同一行中:
data class Person(var name: String, var age: Int)
Log.v(“test”, “person=${Person("Peter", 16)}”)
//输出如下:
V/test: person=Person(name=Peter, age=16)
如果字段很多,把它们都打印在一行中可读性很差。
有没有一种方法,可以读取一个类中所有的字段信息? 这样我们就可以将他们组织成想要的形状。请看下面这个方法:
fun Any.ofMap() =
//'过滤掉除data class以外的其他类'
this::class.takeIf { it.isData }
//'遍历类的所有成员,过滤掉成员方法,只考虑成员属性'
?.members?.filterIsInstance<KProperty<Any>>()
//'将成员属性名和值存储在Pair中'
?.map { it.name to it.call(this) }
//'将Pair转换成map'
?.toMap()
为任意 Kotlin 中的类添加一个扩展函数
,它的功能是将data class
中所有的字段名及其对应值存在一个 map 中。其中用到的 Kotlin 语法糖如下:
isData
是KClass
中的一个属性,用于判断该类是不是一个data class
。KClass
是 Kotlin 中用来描述 类的类型,KClass
可以通过对象::class
语法获得。members
也是KClass
中的一个属性,它包含了所有类的方法和属性。filterIsInstance()
是Iterable
接口的扩展函数,用于过滤出集合中指定的类型。to
是一个infix
扩展函数,它的定义如下:
public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)
复制代码
带有
infix
标识的函数只允许带有一个参数,并且在调用时可以省略包裹参数的括号。这种语法叫中缀表达式
写段测试代码,结合上一节的打印 map 函数看下效果:
data class Person(var name: String, var age: Int)
Person("Peter", 16).ofMap()?.print { it.toString() }.let { Log.v("test","$it") }
测试代码先将Person
实例转换成 map,然后打印 map。输出结果如下:
V/test:
{
[age] = 16
[name] = Peter
}
若data class
嵌套会发生什么?
//'位置,嵌套在Person类中'
data class Location(var x: Int, var y: Int)
data class Person(var name: String, var age: Int, var locaton: Location? = null)
Person("Peter", 16, Location(20, 30)).ofMap()?.print { it.toString() }.let { Log.v("test", "$it") }
//'打印结果如下'
{
[age] = 16
[locaton] = Location(x=20, y=30)
[name] = Peter
}
期望得到类似 Json 的打印效果,但输出结果还差一点。是因为将Person
转化成Map
时并没有将嵌套的Location
也转化成键值对。
需要将ofMap()
方法重构成递归调用:
fun Any.ofMap(): Map<String, Any?>? {
return this::class.takeIf { it.isData }
?.members?.filterIsInstance<KProperty<Any>>()
?.map { member ->
val value = member.call(this)?.let { v->
//'若成员变量是data class,则递归调用ofMap(),将其转化成键值对,否则直接返回值'
if (v::class.isData) v.ofMap()
else v
}
member.name to value
}
?.toMap()
}
为了让打印结果也有嵌套缩进效果,打印 Map 的函数也需要相应地重构:
/**
* 打印 Map,生成结构化键值对子串
* @param space 行缩进量
*/
fun <K, V> Map<K, V?>.print(space: Int = 0): String {
//'生成当前层次的行缩进,用space个空格表示,当前层次每一行内容都需要带上缩进'
val indent = StringBuilder().apply {
repeat(space) { append(" ") }
}.toString()
return StringBuilder("\n${indent}{").also { sb ->
this.iterator().forEach { entry ->
//'如果值是 Map 类型,则递归调用print()生成其结构化键值对子串,否则返回值本身'
val value = entry.value.let { v ->
(v as? Map<*, *>)?.print("${indent}${entry.key} = ".length) ?: v.toString()
}
sb.append("\n\t${indent}[${entry.key}] = $value,")
}
sb.append("\n${indent}}")
}.toString()
}
写段测试代码,看看效果:
//'坐标类,嵌套在Location类中'
data class Coordinate(var x: Int, var y: Int)
//'位置类,嵌套在Person类中'
data class Location(var country: String, var city: String, var coordinate: Coordinate)
data class Person(var name: String, var age: Int, var locaton: Location? = null)
Person("Peter", 16, Location("china", "shanghai", Coordinate(10, 20))).ofMap()?.print().let { Log.v("test", "$it") }
//'打印如下'
{
[age] = 16,
[locaton] =
{
[city] = shanghai,
[coordinate] =
{
[x] = 10,
[y] = 20,
},
[country] = china,
},
[name] = Peter,
}
作者:唐子玄
链接:https://juejin.im/post/5e91a4dbe51d4546b90d1ee3
喜欢 就关注吧,欢迎投稿!
本网站文章均为原创内容,并可随意转载,但请标明本文链接
如有任何疑问可在文章底部留言。为了防止恶意评论,本博客现已开启留言审核功能。但是博主会在后台第一时间看到您的留言,并会在第一时间对您的留言进行回复!欢迎交流!
本文链接: https://leetcode.jp/kotlin实战|语法糖,总有一颗甜到你(持续更新)/