大话重构 之 消除巨无霸类

当你看到别人写的超过千行的巨无霸类,以及随着时间的累积,自己写的类也稳步迈向巨无霸的时候,是不是既恐惧又无奈?一码今天就带小伙伴们征服巨无霸,打造属于自己的成就感。

过长类的缘由

当业务逻辑随着时间累积,并且越来越复杂时,这个类由本来的清秀怡人非常容易变得满脸横肉。

一个类中业务逻辑越来越多,首先它的职责就不再单一,一说这,小伙伴们都明白。

其次逻辑越多,涉及的状态越多,即实例变量越多,实例变量一多,重复代码也就随之而来。为啥?实例变量关系有远有近,关系较近的实例变量没有被清晰的打包出来,直接后果就是类中存在多处操作这些实例变量的重复代码片段。

“实例变量没有被清晰的打包”,实际上就是职责不单一。是否对职责不单一又有了一点新的认识呢?

另外说一点,能把类写成巨无霸的同学,通常都不修边幅,语言的表达层次很低。比如,找一组元素中的最小值,一定会亲手写个循环遍历一遍,4行代码没了。

有了前面的说明,我们就可以开始征服之旅啦。由浅入深,分三个方面来讲:提升语言表达层次,消除类内的重复,明确概念分离职责。

风景

提升语言表达层次

语言表达层次怎么理解,一码给你看两段在集合里面找最小值的代码:

表达层次低

val elements = ...
var min = Int.MaxValue
for (element <- elements)
    if (element < min)
        min = element

正常

val elments = ...
val min = elements.min()

“正常”版本说明了意图,表达层次高,明显获胜。Scala语言里面写出“层次低”版本的较少见,但是其它语言里就不一定了。碰到这种情况记得找下Apache/Guaua等库,没有的话自己封装一个库也可以,千万不要再写“层次低”这种吃力不讨好的代码了。如果自己封装库,重构的方法参考《消除重复代码》一文。

经过上面这一折腾,巨无霸应该瘦了十之有一了,接下来要面对的敌人是“很多实例变量”。

风景

消除类内的重复

类内部关系相近的实例变量,容易导致重复的代码片段,这给我们一个很好发现代码重复的提示。

那哪些实例变量的关系比较近呢?

  • 同样的前缀或后缀
  • 由空行隔开的实例变量组
  • 名字上的业务含义可以看出来的

有了上面的提示,接下来就是到类里面去扫描重复代码了,人肉扫描哈,有更好办法的小伙伴一定告诉我。找到后的具体重构方法,万变不离其宗,依然是 提取方法 ,请小伙伴们参考《消除重复代码》一文。

典型的效果是,原本5个上百行的方法,重构成5个几十行的方法(通常不超过40行)和10多个三四行的方法。

另外不得不提的是一些不咋个依赖实例变量的超大方法,如何重构涉及的内容较多也较独立,请参考《消除过长方法》一文。

噼里啪啦一阵,巨无霸这次瘦身比较明显,去了十之三四。如果达不到效果,微信上找我,一码就喜欢干这个事情,跟你一起重构。

到了这一步,还有500行的类,终于要用到今天的主角了:明确概念,分离职责。

风景

明确概念分离职责

关系较近的实例变量,以及使用它们的方法,需被冠以明确的概念,并独立成类。

这样做的好处:

  • 保持职责单一
  • 明确的概念更容易抽取出易用的接口,如此方便重用和扩展

这里有两种手法,一是 提取类 ,一是 提取子类 ,对应了组合和继承。后者不太直观,莫急,下面逐一说明。

提取类

class Person(val name: String,
             val officeAreaCode: String,
             val officeNumber: String) {
	def officePhone(): String = {
		officeAreaCode + "-" + officeNumber
	}
}

officeAreaCode和officeNumber有相同的前缀,而且officePhone方法中也一起使用,非常紧密。概念也很明确,完整的电话号码嘛。好了,提取一个独立的类TelephoneNumber。

class Person(val name: String,
             val officeTelphone: TelephoneNumber) {
	def officePhone(): String = {
		officeTelephone.telephoneNumber
	}
}

class TelephoneNumber(val areaCode: String,
                      val number: String) {
	def telephoneNumber(): String = {
		areaCode + "-" + number
	}
}

例子很简单,但已足以说明问题。

提取子类

为啥会有子类呢?不太直观哈。一般是在代码中出现不同的条件,有不同的处理时,可以用 提取子类 ,来个例子。

class Ojbect(val name: String,
             val type: String) {
    def doSomething() {
        type match {
            case "A" => doSomethingForA()
            case "B" => doSomethingForB()
            case "C" => doSomethingForC()
        }
    }
    
    def doSomethingForA() = { ..A.. }
    def doSomethingForB() = { ... }
    def doSomethingForC() = { ... }
}

其中的match是Scala特有的,和Java的switch类似,当然它有更强大的内涵,请小伙伴们自行GFSOSO。

type这个实例变量,是要抽取子类的典型特征,重构如下:

class Object(val name: String) {
    def doSomething(): Unit // 没有方法体
}

class A(val name: String)
    extends Object(name) {
    
    @override def doSomething() = { ..A.. }
}

class B(val name: String)
    extends Object(name) {
    
    @override def doSomething() = { ..B.. }
}

class C(val name: String)
    extends Object(name) {
    
    @override def doSomething() = { ..C.. }
}

相信一看代码,小伙伴们就懂了。

小技巧

当从内部无法明显看出哪些变量关系紧密时,可以转到外部观察,看哪些方法经常被一起使用,这有助于你找到明确的概念,进而用上面的重构手法分离职责。

好了,打完收工,这些类都个个清秀了吧。小伙伴们赶紧动动手,找到属于自己的成就感吧。

下期再见。

推荐

解决万恶之首“重复代码”

消除过长方法

消除巨无霸类

答读者问

你的参数列表像蚯蚓一样让人厌恶吗

职责单一原则真的简单吗

查看《大话重构》系列文章,请进入YoyaProgrammer公众号,点击 核心技术,点击 大话重构。

分类 大话重构

优雅程序员 原创 转载请注明出处

图片二维码

posted @ 2015-06-03 09:16  一码  阅读(2893)  评论(11编辑  收藏  举报