Open levy5307 opened 2 years ago
https://levy5307.github.io/blog/code-refactor/
过去几年里做了很多重构相关的工作,所以一直想写一篇关于代码重构的文章。但是国内互联网公司环境下,重构工作其实是不太被老板们重视的。因为不管重构做不做,功能一直都是有的,重构本身并没有带来什么比较亮眼的功能点,反而容易带来bug(除非重构工作带来很多性能的提升,当然绝大多数重构工作并不会带来这种功效)。所以一度对于重构这一块是很灰心的,颇有种老顽童想要拼命忘记九阴真经的感觉,因此文章也一直放着没写。现在我想明白了,人嘛,更重要的是要为自己负责,所以看到不顺眼的代码还是会去改。而且代码重构本身是非常有意义的,比如之前Pegasus的load balance模块,那块真的是太乱了,如果不重构后面完全无法添加新的负载均衡策略。
所以下面主要针对本人的重构经验进行总结和反思。
设计模式
其实提到重构,不得不说的就是设计模式。之前有同事跟我讲:设计模式这个东西没什么用,因为很少有机会能够套用上这些所谓的模式。我个人认为,设计模式这个东西本来就不是用来生搬硬套的,学习设计模式更多的是学习其中的思想和精髓,就如同武侠小说中的武功心法。比如郭靖常年研习九阴真经里的内功心法,并融入到降龙十八掌里,使其刚中有柔,因此在大战金轮法王+蒙古三杰时能够愈战愈勇并逐渐占据上风。与乔峰在少室山中一味刚猛而无法久战相比,自然是高明的多了。我在工作中也经常遇到一个场景,即在重构某部分代码时,压根就没有想过去用什么设计模式。等设计完、画完类图之后才恍然大悟,原来早已在无形中用上了。
单例模式
单例模式是一个非常常用的设计模式,主要用于一个类只有一个对象的场景。它有两种实现方式:
饿汉式
懒汉式是指,对于该类的单例对象,不管日后是否使用,都事先创建一个对象,其代码如下:
class Singleton { public: static Singleton& instance() { return instance; }
private: Singleton() = default; Singleton(const singleton &) = delete; Singleton &operator=(const singleton &) = delete;
private Singleton instance;
};
该方式的缺点在于,对于某个类,即使永远没有调用过instance()函数,该单例对象也早已创建完,造成资源的浪费。
其优点也很明显,即线程安全。
懒汉式
懒汉式是指,对于该类的单例对象,只有在获取的时候才去创建,其常见写法如下:
class Singleton { public: static Singleton *instance() { if (nullptr == instance) { instance.reset(new Singleton()); } return instance.get(); }
private std::unique_ptr<Singleton> instance;
这样做的好处是,当某个类的instance()从没有调用过时,便可以不用创建该类的对象,减少不必要的资源浪费。其缺点也很明显,该方式不是线程安全的,原因在于instance()函数的if (nullptr == instance)这一行。
对于非线程安全这一点,可以通过以下几种方法来解决:
加锁。对if语句进行加锁可以实现线程安全,但是这样对性能的开销很大 利用”C++11中静态变量的初始化时线程安全的”这一条特性。具体实现代码如下:
class Singleton { public: static Singleton& instance() { static Singleton instance; return instance; }
private: Singleton() = default; Singleton(const singleton &) = delete; Singleton &operator=(const singleton &) = delete; };
上述实现集合了不浪费资源、又线程安全两大优点。
另外,仔细看上面的代码,不管是懒汉式还是饿汉式,都使构造函数对外不可见。这是为了防止有代码直接new Singleton,从而破坏了一个类只有一个对象的约定。
工厂模式
工厂模式也是普遍采用的一种设计模式,其优点主要有如下几个:
解耦。假如class A要获取Class B的各种不同子类的对象,只需要通过传参给工厂,工厂根据参数返回具体的子类对象,并使用Class B类型指针返回。此时Class A无需知道Class B的子类情况,达到了屏蔽的效果。 降低代码重复。如果创建对象的过程很复杂,需要大量的代码,那么将创建对象的过程封装到工厂中,可以显著减少重复代码。
简单工厂
最常用、也最简单的工厂模式就是简单工厂模式,其类图如下所示:
上图中的Product只有一种,因此类图很简单。当Product有多种,且是固定数量时,同样也可以使用简单工厂模式。只不过是Factory类针对不同的产品,提供不同的接口。
那么当Product数量不固定时,简单工厂还能满足需求吗?
答案是否定的。因为由于Product数量不固定,所以每当增加一个Product时,都需要侵入式修改Factory代码(不符合开闭原则),非常不优雅。
工厂方法模式
对于Product数量不固定的这种情况,需要使用工厂方法模式。其思路也很简单,就是避免侵入式修改(符合开闭原则)。那么避免侵入式修改的最简单的方法就是,通过增加工厂子类来应对增加的Product。其类图如下:
如图中所示,Factory是一个接口类,其有不同的子类,每个子类对应一个具体的Product。当增加Product时,只需要增加Factory的子类即可。
抽象工厂模式
有时产品会有两个维度,例如猫狗老鼠、公和母。此时使用简单工厂模式和工厂方法模式都不能满足需求,此时就需要使用抽象工厂模式了。
其原理在于,对于产品的两个维度,首先区分出哪个容易变化、哪个不容易变化。如前面举得例子,公和母是不会发生改变的,自然界中只有这么两种可能。然而动物种类则是可能发生变化的,最开始系统中只有猫狗和老鼠,后面可能会增加兔子、鸵鸟等等。
其次,对于选取出的不易变化变化的维度,采用简单工厂中的方法,即增加创建函数的方法;而对于容易发生变化的维度,前面讲过,如果采用增加创建函数的方法的话,很容易带来侵入式修改,因此需要采用工厂方法中的方式,即增加子类的方式。
具体类图如下:
Builder模式
Builder模式也是一种创建型设计模式,其主要应用场景有如下两个:
类的构造函数参数特别多,其中一部分是必要的,另外还有很多参数是可选的。这样会导致需要创建很多个构造函数。例如:
class Example { public: Example(int a); Example(int a, int optional1); Example(int a, std::string& optional2); Example(int a, int optional1, std::string& optional2);
private: int a; int optional1; std::string optional2; };
在上面的例子中,有1个必要参数,2个可选参数,导致构造函数需要创建4个。如果参数数量再增多的话,其复杂程度可想而知。因此需要采用Builder模式,其大概实现如下:
class Example { public: Example(int a, int optional1, std::string& optional2);
class Builder { public: Builder(int a); Builder& setOptional1(int optional1); Builder& setOptional2(std::string optional2); Example* build() { return Example(a, optional1, optional2); } private: int a; int optional1; std::string optional2; };
这里有很多同学会问,就只使用必选参数来实现一个构造函数Example(int a),其他使用set函数来实现不可以吗?
当然是不可以的,这里有两个原因:
这些参数有可能是const类型的,不支持set函数 即使支持set函数,当调用完new Example(int a)之后,会获取一个“半成品” 对象。这样需要用户代码逻辑来保证不会错误的使用这种“半成品”对象,导致系统不够健壮的。
对象的创建需要对给出的参数进行合法性检查,当检查失败时不进行创建。
这种情况可以参考本人之前做Pegasus load balance重构时的实现
其大致代码如下:
std::unique_ptr build() { // do some caculate ...
if (0 == higher_count && 0 == lower_count) { return nullptr; } return dsn::make_unique<ford_fulkerson>(higher_count, lower_count);
}
如上所示,在build()函数中对一些参数进行了校验,当校验失败时不进行创建。而这些校验无法在构造函数中进行。很显然,在这种情况下使用set函数也是很不合理的,因为这会导致一些本不应该创建的对象,作为“半成品”被创建出来。
原型模式
最近在读西游记,里面有一个片段让人印象深刻:
拔一把毫毛,丢在口中嚼碎,望空中喷去,叫一声“变”,即变做三二百个小猴,周围攒簇。
原型模式就有类似的功效,即克隆复制。下面为代码示例:
class Monkey { public: Monkey(uint32_t height, uint32_t weight) { this->height = height; this->weight = weight; } Monkey(const Monkey& monkey) { this->height = monkey.height; this->weight = monkey.weight; }
Monkey* clone() { return new Monkey(*this); }
private: uint32_t height; uint32_t weight; };
通过调用monkey->clone(),即可复制出与monkey一模一样的小猴子:
void doBussiness(Monkey *monkey) { auto monkey1 = new Monkey(180, 80); auto monkey2 = monkey1->clone(); auto monkey3 = monkey1->clone(); }
可能有人会问,直接用new创建不行吗?比如下面这样:
void doBussiness(Monkey *monkey) { auto monkey1 = new Monkey(180, 80); auto monkey2 = new Monkey(180, 80); auto monkey3 = new Monkey(180, 80); }
这样当然是不好的,因为有大量的重复代码,重复代码往往意味着bad smell,因为一旦修改,就要修改很多处地方,非常不优雅。
另外clone函数最终调用的也是拷贝构造函数,那直接使用拷贝构造函数不可以吗?
当然是不可以的。有两个原因:
因为clone函数可以实现多态,而拷贝构造函数不可以。使用多态可以自动识别出其具体类型,调用实际子类的clone函数 使用拷贝构造函数需要知道具体类的类型,这样会带来耦合(不符合开闭原则)。比如Monkey有多个子类,需要知道其具体是属于哪个子类,然后去调用其拷贝构造函数,带来了耦合性。
举个例子:
猴子其实是一个大类,具体可以分为猕猴、金丝猴等等。
class Monkey { public: Monkey(uint32_t height, uint32_t weight) { this->height = height; this->weight = weight; } Monkey(const Monkey& monkey) { this->height = monkey.height; this->weight = monkey.weight; } virtual ~Monkey() = 0;
virtual std::unique_ptr<Monkey> clone() = 0;
protected: uint32_t height; uint32_t weight; };
猕猴有一个特性,即有些猕猴是没有尾巴的,所以其多一个参数hasTail
class Macaque : public Monkey { public: Macaque(uint32_t height, uint32_t weight, bool hasTail) : Monkey(height, weight) { this->hasTail = hasTail; } Macaque(const Macaque& monkey) : Monkey(monkey) { this->hasTail = monkey.hasTail; }
std::unique_ptr<Monkey> clone() { return std::make_unique<Macaque>(*this); }
private: bool hasTail; };
而金丝猴在某些公司特指比较重要的员工,一般都是领导层,所以有一个参数officialRank
class GoldenMonkey : public Monkey { public: GoldenMonkey(uint32_t height, uint32_t weight, uint16_t officalRank) : Monkey(height, weight) { this->officalRank = officalRank; } GoldenMonkey(const GoldenMonkey& monkey) : Monkey(monkey) { this->officalRank = monkey.officalRank; }
std::unique_ptr<Monkey> clone() { return std::make_unique<GoldenMonkey>(*this); }
private: uint16_t officalRank; };
当我们使用拷贝构造函数进行复制时:
void doBussiness(Monkey *monkey) { // monkey实际是猕猴 // How to copy? // 1. 根本不知道monkey的类型,不知道该调GoldenMonkey拷贝构造函数还是Macaque的拷贝构造函数。 // 2. 即使知道是猕猴,也会带来对Macque的耦合 }
而使用clone函数就比较简单了,它支持多态,可以自动根据实际类型来调用具体子类的clone函数,并且完全不会带来耦合(符合开闭原则):
void doBussiness(Monkey *monkey) { auto monkey1 = monkey->clone(); auto monkey2 = monkey->clone(); auto monkey3 = monkey->clone(); }
享元模式
享元,故名思议,就是分享元数据的意思。在讲享元模式前,还是先举一个西游记中的例子:
玉帝大恼,即差四大天王,协同李天王并哪吒太子,点二十八宿、九曜星官、十二元辰、五方揭谛、四值功曹、东西星斗、南北二神、五岳四渎、普天星相,共十万天兵,布一十八架天罗地网,下界去花果山围困,定捉获那厮处治
这里的十万天兵天将,不是先从老百姓中征兵、训练,然后再出征,等出征完后各回各家各找各妈,因为这样效率实在太低了。而是事先准备好的军队,等用的时候直接从军队中调遣。正所谓养兵千日用兵一时。
这就是享元模式的基本思想。下面看一下具体的代码示例:
class Solider { public: Solider(std::string name, std::string desc) { this->name = name; this->desc = desc; } virtual ~Solider() = 0;
virtual void fight() = 0; const std::string& getName() const { return this->name; }
private: std::string name; std::string desc; };
// 天王 class HeavenlyKing : public Solider { public: HeavenlyKing(std::string name, std::string desc) : Solider(name, desc) {}
void fight() { // 天王战斗逻辑 }
// 哪吒 class Nezha : public Solider { public: Nezha(std::string name, std::string desc) : Solider(name, desc) {}
void fight() { // 哪吒战斗逻辑 }
上面代码中实现了一个Solider的虚基类,其有天王和哪吒等几个子类。
class Army { public: Army() { Solider *liudehua = new HeavenlyKing(
https://levy5307.github.io/blog/code-refactor/
过去几年里做了很多重构相关的工作,所以一直想写一篇关于代码重构的文章。但是国内互联网公司环境下,重构工作其实是不太被老板们重视的。因为不管重构做不做,功能一直都是有的,重构本身并没有带来什么比较亮眼的功能点,反而容易带来bug(除非重构工作带来很多性能的提升,当然绝大多数重构工作并不会带来这种功效)。所以一度对于重构这一块是很灰心的,颇有种老顽童想要拼命忘记九阴真经的感觉,因此文章也一直放着没写。现在我想明白了,人嘛,更重要的是要为自己负责,所以看到不顺眼的代码还是会去改。而且代码重构本身是非常有意义的,比如之前Pegasus的load balance模块,那块真的是太乱了,如果不重构后面完全无法添加新的负载均衡策略。
所以下面主要针对本人的重构经验进行总结和反思。
设计模式
其实提到重构,不得不说的就是设计模式。之前有同事跟我讲:设计模式这个东西没什么用,因为很少有机会能够套用上这些所谓的模式。我个人认为,设计模式这个东西本来就不是用来生搬硬套的,学习设计模式更多的是学习其中的思想和精髓,就如同武侠小说中的武功心法。比如郭靖常年研习九阴真经里的内功心法,并融入到降龙十八掌里,使其刚中有柔,因此在大战金轮法王+蒙古三杰时能够愈战愈勇并逐渐占据上风。与乔峰在少室山中一味刚猛而无法久战相比,自然是高明的多了。我在工作中也经常遇到一个场景,即在重构某部分代码时,压根就没有想过去用什么设计模式。等设计完、画完类图之后才恍然大悟,原来早已在无形中用上了。
单例模式
单例模式是一个非常常用的设计模式,主要用于一个类只有一个对象的场景。它有两种实现方式:
饿汉式
懒汉式是指,对于该类的单例对象,不管日后是否使用,都事先创建一个对象,其代码如下:
class Singleton { public: static Singleton& instance() { return instance; }
private: Singleton() = default; Singleton(const singleton &) = delete; Singleton &operator=(const singleton &) = delete;
};
该方式的缺点在于,对于某个类,即使永远没有调用过instance()函数,该单例对象也早已创建完,造成资源的浪费。
其优点也很明显,即线程安全。
懒汉式
懒汉式是指,对于该类的单例对象,只有在获取的时候才去创建,其常见写法如下:
class Singleton { public: static Singleton *instance() { if (nullptr == instance) { instance.reset(new Singleton()); } return instance.get(); }
private: Singleton() = default; Singleton(const singleton &) = delete; Singleton &operator=(const singleton &) = delete;
};
这样做的好处是,当某个类的instance()从没有调用过时,便可以不用创建该类的对象,减少不必要的资源浪费。其缺点也很明显,该方式不是线程安全的,原因在于instance()函数的if (nullptr == instance)这一行。
对于非线程安全这一点,可以通过以下几种方法来解决:
class Singleton { public: static Singleton& instance() { static Singleton instance; return instance; }
private: Singleton() = default; Singleton(const singleton &) = delete; Singleton &operator=(const singleton &) = delete; };
上述实现集合了不浪费资源、又线程安全两大优点。
另外,仔细看上面的代码,不管是懒汉式还是饿汉式,都使构造函数对外不可见。这是为了防止有代码直接new Singleton,从而破坏了一个类只有一个对象的约定。
工厂模式
工厂模式也是普遍采用的一种设计模式,其优点主要有如下几个:
简单工厂
最常用、也最简单的工厂模式就是简单工厂模式,其类图如下所示:
上图中的Product只有一种,因此类图很简单。当Product有多种,且是固定数量时,同样也可以使用简单工厂模式。只不过是Factory类针对不同的产品,提供不同的接口。
那么当Product数量不固定时,简单工厂还能满足需求吗?
答案是否定的。因为由于Product数量不固定,所以每当增加一个Product时,都需要侵入式修改Factory代码(不符合开闭原则),非常不优雅。
工厂方法模式
对于Product数量不固定的这种情况,需要使用工厂方法模式。其思路也很简单,就是避免侵入式修改(符合开闭原则)。那么避免侵入式修改的最简单的方法就是,通过增加工厂子类来应对增加的Product。其类图如下:
如图中所示,Factory是一个接口类,其有不同的子类,每个子类对应一个具体的Product。当增加Product时,只需要增加Factory的子类即可。
抽象工厂模式
有时产品会有两个维度,例如猫狗老鼠、公和母。此时使用简单工厂模式和工厂方法模式都不能满足需求,此时就需要使用抽象工厂模式了。
其原理在于,对于产品的两个维度,首先区分出哪个容易变化、哪个不容易变化。如前面举得例子,公和母是不会发生改变的,自然界中只有这么两种可能。然而动物种类则是可能发生变化的,最开始系统中只有猫狗和老鼠,后面可能会增加兔子、鸵鸟等等。
其次,对于选取出的不易变化变化的维度,采用简单工厂中的方法,即增加创建函数的方法;而对于容易发生变化的维度,前面讲过,如果采用增加创建函数的方法的话,很容易带来侵入式修改,因此需要采用工厂方法中的方式,即增加子类的方式。
具体类图如下:
Builder模式
Builder模式也是一种创建型设计模式,其主要应用场景有如下两个:
类的构造函数参数特别多,其中一部分是必要的,另外还有很多参数是可选的。这样会导致需要创建很多个构造函数。例如:
class Example { public: Example(int a); Example(int a, int optional1); Example(int a, std::string& optional2); Example(int a, int optional1, std::string& optional2);
private: int a; int optional1; std::string optional2; };
在上面的例子中,有1个必要参数,2个可选参数,导致构造函数需要创建4个。如果参数数量再增多的话,其复杂程度可想而知。因此需要采用Builder模式,其大概实现如下:
class Example { public: Example(int a, int optional1, std::string& optional2);
private: int a; int optional1; std::string optional2; };
这里有很多同学会问,就只使用必选参数来实现一个构造函数Example(int a),其他使用set函数来实现不可以吗?
当然是不可以的,这里有两个原因:
对象的创建需要对给出的参数进行合法性检查,当检查失败时不进行创建。
这种情况可以参考本人之前做Pegasus load balance重构时的实现
其大致代码如下:
std::unique_ptr build()
{
// do some caculate
...
}
如上所示,在build()函数中对一些参数进行了校验,当校验失败时不进行创建。而这些校验无法在构造函数中进行。很显然,在这种情况下使用set函数也是很不合理的,因为这会导致一些本不应该创建的对象,作为“半成品”被创建出来。
原型模式
最近在读西游记,里面有一个片段让人印象深刻:
拔一把毫毛,丢在口中嚼碎,望空中喷去,叫一声“变”,即变做三二百个小猴,周围攒簇。
原型模式就有类似的功效,即克隆复制。下面为代码示例:
class Monkey { public: Monkey(uint32_t height, uint32_t weight) { this->height = height; this->weight = weight; } Monkey(const Monkey& monkey) { this->height = monkey.height; this->weight = monkey.weight; }
private: uint32_t height; uint32_t weight; };
通过调用monkey->clone(),即可复制出与monkey一模一样的小猴子:
void doBussiness(Monkey *monkey) { auto monkey1 = new Monkey(180, 80); auto monkey2 = monkey1->clone(); auto monkey3 = monkey1->clone(); }
可能有人会问,直接用new创建不行吗?比如下面这样:
void doBussiness(Monkey *monkey) { auto monkey1 = new Monkey(180, 80); auto monkey2 = new Monkey(180, 80); auto monkey3 = new Monkey(180, 80); }
这样当然是不好的,因为有大量的重复代码,重复代码往往意味着bad smell,因为一旦修改,就要修改很多处地方,非常不优雅。
另外clone函数最终调用的也是拷贝构造函数,那直接使用拷贝构造函数不可以吗?
当然是不可以的。有两个原因:
举个例子:
猴子其实是一个大类,具体可以分为猕猴、金丝猴等等。
class Monkey { public: Monkey(uint32_t height, uint32_t weight) { this->height = height; this->weight = weight; } Monkey(const Monkey& monkey) { this->height = monkey.height; this->weight = monkey.weight; } virtual ~Monkey() = 0;
protected: uint32_t height; uint32_t weight; };
猕猴有一个特性,即有些猕猴是没有尾巴的,所以其多一个参数hasTail
class Macaque : public Monkey { public: Macaque(uint32_t height, uint32_t weight, bool hasTail) : Monkey(height, weight) { this->hasTail = hasTail; } Macaque(const Macaque& monkey) : Monkey(monkey) { this->hasTail = monkey.hasTail; }
private: bool hasTail; };
而金丝猴在某些公司特指比较重要的员工,一般都是领导层,所以有一个参数officialRank
class GoldenMonkey : public Monkey { public: GoldenMonkey(uint32_t height, uint32_t weight, uint16_t officalRank) : Monkey(height, weight) { this->officalRank = officalRank; } GoldenMonkey(const GoldenMonkey& monkey) : Monkey(monkey) { this->officalRank = monkey.officalRank; }
private: uint16_t officalRank; };
当我们使用拷贝构造函数进行复制时:
void doBussiness(Monkey *monkey) { // monkey实际是猕猴 // How to copy? // 1. 根本不知道monkey的类型,不知道该调GoldenMonkey拷贝构造函数还是Macaque的拷贝构造函数。 // 2. 即使知道是猕猴,也会带来对Macque的耦合 }
而使用clone函数就比较简单了,它支持多态,可以自动根据实际类型来调用具体子类的clone函数,并且完全不会带来耦合(符合开闭原则):
void doBussiness(Monkey *monkey) { auto monkey1 = monkey->clone(); auto monkey2 = monkey->clone(); auto monkey3 = monkey->clone(); }
享元模式
享元,故名思议,就是分享元数据的意思。在讲享元模式前,还是先举一个西游记中的例子:
玉帝大恼,即差四大天王,协同李天王并哪吒太子,点二十八宿、九曜星官、十二元辰、五方揭谛、四值功曹、东西星斗、南北二神、五岳四渎、普天星相,共十万天兵,布一十八架天罗地网,下界去花果山围困,定捉获那厮处治
这里的十万天兵天将,不是先从老百姓中征兵、训练,然后再出征,等出征完后各回各家各找各妈,因为这样效率实在太低了。而是事先准备好的军队,等用的时候直接从军队中调遣。正所谓养兵千日用兵一时。
这就是享元模式的基本思想。下面看一下具体的代码示例:
class Solider { public: Solider(std::string name, std::string desc) { this->name = name; this->desc = desc; } virtual ~Solider() = 0;
private: std::string name; std::string desc; };
// 天王 class HeavenlyKing : public Solider { public: HeavenlyKing(std::string name, std::string desc) : Solider(name, desc) {}
};
// 哪吒 class Nezha : public Solider { public: Nezha(std::string name, std::string desc) : Solider(name, desc) {}
};
上面代码中实现了一个Solider的虚基类,其有天王和哪吒等几个子类。
class Army { public: Army() { Solider *liudehua = new HeavenlyKing(