火车残骸和基本类型偏执问题解决方案

简介: 坏味道:缺乏封装。封装,将碎片式代码封装成可复用模块。但不同级别程序员对封装理解程度差异大,往往写代码的人认为自己提供了封装,但实际上,我们还是看到许多的代码散落在那里。

坏味道:缺乏封装。封装,将碎片式代码封装成可复用模块。但不同级别程序员对封装理解程度差异大,往往写代码的人认为自己提供了封装,但实际上,我们还是看到许多的代码散落在那里。


1 火车残骸


获得一篇博客作者名字:


String name = article.getAuthor().getName();


博客里有作者信息,想要获得作者名,通过“作者”找到“作者姓名”,这就是很多人凭借直觉写出的代码,这有问题!


是不是感觉自己无法理解封装了?

若你想写出上面这段,是不是得先了解Article、Author两个类的实现细节?即我们得知道,作者的姓名存储在作品的作者字段。


这就是问题:当你须先了解一个类的细节,才能写代码,这只能说明这封装不优雅。


翻翻你手头的项目,这种在一行代码中有连续多个方法调用的情况是不是随处可见?


Martin Fowler 在《重构》中给这种坏味道起的名字叫过长的消息链(Message

Chains),而有人则给它起了一个更为夸张的名字:火车残骸(Train Wreck),形容这样的代码像火车残骸一般,断得一节一节。


解决这种代码的重构方案叫隐藏委托关系(Hide Delegate),即把这种调用封装:


class Book {

 ...

 public String getAuthorName() {

   return this.author.getName();

 }

 ...

}

String name = book.getAuthorName();

2 产因


对封装理解不够,大部分人对封装仅停留在:数据结构+算法。

学习数据结构时,写代码都是拿到各种细节直接操作,但那是在做练习,不是工程。有人编写一个新类:


第一步是写出这类要用的字段

然后给这些字段生成各种 getXXX

很多语言或框架提供的约定就是基于这种 getter的,就像 Java 里的 JavaBean,相应配套工具lombok也很方便。让暴露越来越容易,封装反而无人在意了。


但要想成为架构师,就从少暴露细节开始。声明完一个类的字段后,请停下生成 getter 的手,思考类应该提供的行为。


3 迪米特法则


每个单元对其它单元只拥有有限知识,而且这些单元是与当前单元有紧密联系

每个单元只能与其朋友交谈,不与陌生人交谈

只与自己最直接的朋友交谈

该原则需要思考:哪些是直接朋友、陌生人。

火车残骸代码就是没考虑这些问题,直接闷头写代码。写时一时爽,重构火葬场。


按迪米特法写代码,会不会让代码里有太多简单封装的方法?


有可能,不过,这也是单独解决这一个坏味道可能带来的结果。这种代码本质是缺乏对封装的理解,而一个好的封装需要基于行为。所以,把视角再提升,应考虑类应该提供哪些行为,而非简单地把数据换一种形式呈现就止步了。


有些内部 DSL 的表现形式也是连续的方法调用,但 DSL 是声明性的,在说做什么(What),而这里的坏味道是在说怎么做(How),二者抽象级别不同,不要混谈。


4 基本类型偏执


public double getEpubPrice(final boolean highQuality,

        final int chapterSequence) {

 ...

}

根据章节信息获取 EPUB 价格。问题在返回值类型,即价格类型。在DB存储价格时,就是用一个浮点数,用 double 可保证计算的精度,这设计有问题?确实,这就是很多人使用基本类型(Primitive)作为变量类型思考的角度。但这种采用基本类型设计缺少一个模型。


虽价格本身用浮点数存储,但价格和浮点数本身不是同一概念,有着不同行为需求。一般要求商品价格大于 0,但 double 类型本身没这限制。


以“价格大于0”这个需求为例,使用 double 类型怎么限制?


if (price <= 0) {

 throw new IllegalArgumentException("Price should be positive");

}


如果使用 double 作为类型,那我们要在使用的地方都保证价格的正确性,像这样的价格校验就应该是使用的地方到处写。若补齐这缺失的模型,可引入一个 Price 类型,校验就可放在初始化时:


class Price {

 private long price;

 

 public Price(final double price) {

   if (price <= 0) {

     throw new IllegalArgumentException("Price should be positive");

   }

 

   this.price = price;

 }

}

引入一个模型封装基本类型的重构手法,叫以对象取代基本类型(Replace Primitive with Object)。有这模型,还可再进一步,如若让价格在对外呈现时只有两位,在没有 Price 类时,这样逻辑散落各处,代码里很多重复逻辑就是这样产生的。


可在 Price 类里提供一个方法:


public double getDisplayPrice() {

 BigDecimal decimal = new BigDecimal(this.price);

 return decimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();

}


使用基本类型和使用继承出现的问题异曲同工。大部分程序员都了解组合优于继承,即不要写出这样的代码:


public Books extends List<Book> {

 ...

}


而应该写成组合:


public Books  {

 private List<Book> books;

 ...

}


把Books写成继承,是因为在开发者眼里,Books 就是一个书的集合

有人用 double 做价格的类型,因为在他看来,价格就是一个 double

误区在于,一些程序员只看到模型相同之处,却忽略差异。Books 可能不需要提供 List 的所有方法,价格的取值范围与 double 也有差异。


但 Books 问题相对容易规避,因为产生了一个新模型,有通用的设计原则帮助我们判断这个模型构建得是否恰当,而价格问题却不容易规避,因为这里没有产生新的模型,也就不容易发现问题。


这种以基本类型为模型的坏味道称为基本类型偏执(Primitive Obsession)。这基本类型,不限于程序设计语言提供的各种基本类型,像字符串也是。很多对集合类型(比如数组、List、Map 等等)的使用也属于这坏味道:


封装所有的基本类型和字符串

使用流的集合

封装之所以有难度,在于它是一个构建模型的过程,而很多程序员写程序,只是用粗粒度理解写着完成功能的代码,没有构建模型意识;还有一些人以为划分模块就叫封装,所以,才会看到这些坏味道。


所以,真正要写好代码,要对软件设计有深入学习。


5 总结


与封装有关的坏味道:


过长的消息链,或者叫火车残骸

基本类型偏执。

火车残骸的代码就是连续的函数调用,它反映的问题就是把实现细节暴露了出去,缺乏应有的封装。重构的手法是隐藏委托关系,实际就是做封装。软件行业有一个编程指导原则,叫迪米特法则,可以作为日常工作的指导,规避这种坏味道的出现。


基本类型偏执就是用各种基本类型作为模型到处传递,这种情况下通常是缺少了一个模型。解决它,常用的重构手法是以对象取代基本类型,也就是提供一个模型代替原来的基本类型。基本类型偏执不局限于程序设计语言提供的基本类型,字符串也是这种坏味道产生的重要原因,再延伸一点,集合类型也是。


这两种与封装有关的坏味道,背后体现的是对构建模型了解不足,其实,也是很多程序员在软件设计上的欠缺。想成为一个更好的程序员,学习软件设计是不可或缺的。


构建模型,封装散落的代码。


6 怎样的封装算高内聚?


链式调用不一定都是火车残骸,比如:


builder模式,每次调用返回的都是自身,不牵涉到其他对象,不违反迪米特法则

java stream操作,就是声明性操作。

构建模型还有一个好处是加了一层抽象,屏蔽了外部变化,类似防腐层。

比如DDD中领域内只处理本领域的对象,使用其他领域的对象要先经过转换而非直接使用。


JavaBean,用MyBatis Genarater或Lombok生成都会有Setter方法,这样DB查询或接受参数时,数据自动映射到这个对象。如果不用setter,怎么赋值?

现在的数据库映射用的都是反射实现,与setter关系不大。


若你的编码方式是置顶向下的,且当前层都只面向意图定义空类和空函数。写出提倡的这种风格其实很正常。

结合1描述的编码方式。顶层类中不会有基础类型,每个属性的类型都会是一个面向意图的类来承接。顶层函数的实现部分只会有一个个函数,哪怕函数实现只有一行。

目录
相关文章
常见的BUG---1、虚拟机启动之后,突然发现没有ens33网卡
常见的BUG---1、虚拟机启动之后,突然发现没有ens33网卡
|
缓存 运维 程序员
程序员进国企就必然废了吗?
程序员进国企就必然废了吗?
379 0
|
消息中间件 存储 SQL
跨系统数据一致性方案的思考(上)
本文主要意在总结沉淀现有问题解决经验过程,整理解决跨系统数据不一致问题的经验方法。 跨系统数据一致性,比较优秀的解决方案就是微服务化,不同应用系统采用统一数据源方式,这样可以有效避免数据一致性问题。 但是我们很多系统由于历史原因或者业务缘由,导致非服务化情况下,又要采取数据一致性方案。
跨系统数据一致性方案的思考(上)
|
缓存 NoSQL Redis
Redis高并发场景下秒杀超卖解决
Redis高并发场景下秒杀超卖解决
860 0
|
canal 存储 算法
跨系统实时同步数据解决方案
数据量太大,单存储节点存不下,就只能把数据分片存储。
1573 0
|
存储 弹性计算 安全
课时23:案例分享——钉钉
钉钉作为企业级产品,采用SaaS平台技术,依托阿里云的ECS、OSS等服务,实现快速部署与客户需求的高效适应。其数据存储于阿里云RDS中,确保安全性和可靠性,并通过高强度加密保障信息传输安全。阿里云的安全防护措施为钉钉提供了坚实后盾,使其能专注于优化和创新,提升用户体验。
360 0
|
缓存 NoSQL Java
高并发场景秒杀抢购超卖Bug实战重现
在电商平台的秒杀活动中,高并发场景下的抢购超卖Bug是一个常见且棘手的问题。一旦处理不当,不仅会引发用户投诉,还会对商家的信誉和利益造成严重损害。本文将详细介绍秒杀抢购超卖Bug的背景历史、业务场景、底层原理以及Java代码实现,旨在帮助开发者更好地理解和解决这一问题。
458 12
|
SQL Arthas 运维
取经阿里十年技术大佬,得到Java线上问题排查攻略!
再牛逼的程序员都写不出完美无缺的代码,作为后端开发工程师,一不小心就会遇到线上故障。如果线上故障处理不及时,就可能导致各种严重的后果。恰好最近部门出现了一次挺严重但幸运的是影响面不大的线上故障,最后在阿里工作十年的leader分享了线上问题的排查思路。结合这次分享,写下了这篇Java线上问题排查攻略。
|
SQL 运维 监控
如何排查线上问题的?
在当今的互联网时代,线上问题对企业的业务连续性和用户体验产生的影响越来越大。无论是网站崩溃、应用性能下降,还是服务中断,这些问题都可能对企业的声誉和用户满意度造成严重影响。因此,快速、准确地排查并解决线上问题变得至关重要。本文将介绍一些高效的线上问题排查方法,帮助您在面对线上问题时,迅速定位并解决问题。我们将在接下来的内容中详细讨论如何利用日志分析、监控系统、代码审查等手段,以及如何制定有效的应急预案。通过这些策略的实施,您将能够提高线上问题的解决速度,减少对业务的影响,并提高用户满意度。
567 2
|
数据采集 数据建模 BI
数据中台实战(05)-如何统一管理纷繁杂乱的数据指标?
数据中台实战(05)-如何统一管理纷繁杂乱的数据指标?
841 1

热门文章

最新文章