弃用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模式 有两个优势:

  1. 为参数标注语义:在Builder模式中,每个属性的赋值都是一个函数,函数名标注了属性语义。而直接使用构造函数时,很难分辨5043哪个是年龄,哪个是体重。

  2. 可选参数: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;@Overridepublic 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 语法糖如下:

  • isDataKClass中的一个属性,用于判断该类是不是一个data classKClass是 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

喜欢 就关注吧,欢迎投稿!

Kotlin实战 | 语法糖,总有一颗甜到你(持续更新)相关推荐

  1. kotlin常用语法扫盲及开发注意点,勿错失(持续更新)

    kotlin常用语法扫盲及开发注意点,勿错失(持续更新) 文章目录 kotlin常用语法扫盲及开发注意点,勿错失(持续更新) 单句表达式 可以直接 用 = 省略 大括号 ::双冒号作用 map,lis ...

  2. Kotlin 特性 语法糖 优势 扩展 高阶 MD

    Markdown版本笔记 我的GitHub首页 我的博客 我的微信 我的邮箱 MyAndroidBlogs baiqiantao baiqiantao bqt20094 baiqiantao@sina ...

  3. Kotlin(三) 语法糖

    在上篇函数中,我们知道了如何定义函数,那么现在定义个函数,取两个数中比较大的数: fun  getMaxNum(a : Int,b : Int)  : Int {       return max(a ...

  4. java语法糖效率高吗_打包 Java将持续向“高糖”方向发展,你真的了解Java语法糖吗? _好机友...

    Java语法糖概念 1. 语法糖Syntactic Sugar 糖衣语法,方便开发人员使用,JVM并不识别,会在编译阶段解语法糖,还原为基础语法. 2. com.sun.tools.javac.mai ...

  5. 【vue】手动实现vue的v-model语法糖,以及控制在输入法完成之后才更新model

    前沿 通过观察发现,中文输入完成之后,绑定的属性才更新: 面临的问题:中文输入过程中,每按下键盘就会触发input事件,而我们需要捕获中文真正输入完成后的事件,从而更新双向绑定的属性.所以,今天的主是 ...

  6. Kotlin 中的 run、let、with、apply、also、takeIf、takeUnless 语法糖使用和原理分析

    这些Kotlin的语法糖函数经常用,但也很容易搞混,所以转载一下,若混了可以回来再看 转载自公众号:纸上浅谈 正文: 在 Kotlin 有一些可以简化代码的语法糖,比如 run.let.with.ap ...

  7. Android学习-Kotlin语言入门-变量、函数、语法糖、when、for-in、主构造函数、单例类、函数式API、集合遍历、隐式Intent、Activity生命周期、四种启动模式、标准函数

    探究java语言的运行机制 变量.函数.语法糖 when条件语句 for循环 主构造函数.次构造函数 数据类和单例类 集合的创建与遍历 集合的函数式API 创建菜单 隐式使用Intent Activi ...

  8. kotlin 实战之面向对象特性全方位总结

    工匠若水可能会迟到,但是从来不会缺席,最终还是觉得将自己的云笔记分享出来吧 - 特别说明,kotlin 系列文章均以 Java 差异为核心进行提炼,与 Java 相同部分不再列出.随着 kotlin ...

  9. Kotlin技巧与自定义语法糖

    Kotlin技巧与自定义语法糖 kotlin,ktx,anko kotlin官方中文站:http://www.kotlincn.net/docs/reference/basic-syntax.html ...

  10. ef 在此上下文中只支持基本类型或枚举类型_Java 中的 6 颗语法糖

    作者:Java 技术栈来源:SegmentFault 思否社区 原文作者:danchu原文链接:https://blog.csdn.net/danchu/article/details/5498644 ...

最新文章

  1. 7.RabbitMQ RFC同步调用
  2. Django基础-数据分页
  3. 在本地安装git的HTML帮助文档
  4. Elasticsearch安装(Linux)
  5. 深入理解this机制系列第三篇——箭头函数
  6. mybatis 报错: Invalid bound statement (not found)
  7. eclipse spring mysql,eclipse环境下的springboot框架+mybatis访问MySQL报错空指针
  8. linux 修改密码和端口号_WINDOWS/LINUX系统修改管理员密码方法
  9. 使用STM32或GD32解析xml格式数据
  10. linux解压war包命令改配置文件脚本,自动解压war包,并替换胚子和文件,重启tomcat,实现自动化部署。...
  11. 【T1】飞跃专业版库存期初无法同步到【U+】通用财务
  12. 关于python搜题的软件哪个好_搜题用哪个软件好?
  13. python虚拟变量回归_虚拟变量回归中的截距项问题
  14. ArcGIS 线简化算法的使用及两种方法的比较
  15. 苹果Mac电脑开机启动时的声音如何关闭?
  16. specified sample format s16 is invalid or not supported(linux)
  17. libreoffice安装教程_Plasma 5.15.4+Ubuntu19.04 Kubuntu 19.04虚拟机安装和试用
  18. 天秤座男人对爱情的态度(图
  19. .Net Core 文件的上传下载
  20. Linux 编程之非局部跳转:longjmp siglongjmp

热门文章

  1. python中chr的用法_Python chr()用法及代码示例
  2. 基础篇——树莓派远程连接工具VNC不显示视频或摄像头画面解决方式
  3. Linux下QtCreator不能使用搜狗输入法输入中文
  4. JScript服务器运行,jscript模拟的“控制台”程序Web服务器教程
  5. win10上成功运行faster-rcnn.pytorch-1.0
  6. 网站打开速度慢怎么解决
  7. WEB 2.0及其相关技术
  8. 微信小程序踩坑指南--回填再造
  9. 用Firefox看CHM电子书
  10. Chrome浏览器地址栏自动填充