四、坏耦合的原因与解耦(三)
坏耦合的原因
1. 依赖他人
例1.
void SaveMoney(float money); void WithdrawMoney(float money);
如果有假币出现,那么存钱函数SaveMoney就提前处理了,并不会存进去,WithdrawMoney函数从来没有遇到过假钱,而它并没有处理假钱的能力,也一直没被发现!!!如果有一天WithdrawMoney因为意外把假币给了用户,将不知道怎么处理。根本原因就是,WithdrawMoney一直依赖SaveMoney函数处理假币。
2. 双向依赖,你中有我,我中有你
3. 侵占公共资源
要尽量做到公共资源是不可变的,或者操作它的途径非常有限。
4. 需求变化
需求的变化,可能使得原本是好的耦合也会变成坏耦合;
重构的时候,要尽可能解决可预见的问题;
一般项目在迭代开发的时候,要做到两分精力放在用户看不到的内部优化中。
解耦的原则
(一) 让模块逻辑独立而完整
解耦的根本目的就是拆除元素之间不必要的联系,一个核心原则就是让每个模块的逻辑独立二完整。即:
- 所依赖的外部资源尽可能是不变量;
- 对外体现的特性是“不变量”,让别人可以放心的依赖我。
函数要把自己需要的数据都明确标识在参数列表里,把自己能提供的全集中在返回值里。
案例1
void updatePerson(Array persons){ DB.open(); foreach(Person person in persons) { update(person); } DB.close(); }
//update函数
void update(Person person){
string sql = person.GenerateInsertSQL();
openedDB.ExecuteSQL(sql);
}
update里也有数据库的操作,却无须DB的open和close语句。这里是没有问题的。
但是update是一个public函数,那么就会有问题。
如何解决或优化这个问题呢?可以将数据库资源参数化。
void update(Person person,DBConnection openedDB){ string sql = person.GenerateInsertSQL(); openedDB.ExecuteSQL(sql); } //之后对update的调用 foreach(Person person in persons){ update(person,sharedDB); }
当程序员对一个类或者一个方法的使用需要额外的记忆时,这不是好代码。
案例2
一个人要看书
(1)原始版本
Person person = new Person(); person.ReadBook(book); void ReadBook(Book book){ //看书之前要先戴眼镜 WearGlasses(this.MyGlasses); Read(book); }
如果这个人没有眼镜,即 this.MyGlass 变量为null,直接调用person.ReadBook()发现异常,怎么办呢?
(2)优化版本一:通过属性注入
person.MyGlasses = new Glasses(); //虽然简单,但是专业叫法,叫属性注入 person.ReadBook(book);
但是这样写也有问题,因为 ReadBook 函数,使用上不应该有隐式的限定条件。
(3) 优化版本二:通过构造函数的注入
public Person(Glasses glasses){ this.MyGlasses = glasses; }
但是这样仍然存在一个问题:喜欢读书的人毕竟是少数,Person中的很多函数行为,如跑步、吃饭等,并不需要眼镜。
我们应该让各自的需求各自解决。
(4)优化版本三:通过普通成员函数的注入
对于二,恢复为最初的无参构造函数,并单独为ReadBook函数添加一个glasses参数。
void ReadBook(Book book, Glasses glasses){ WearGlasses(glasses); Read(book); }
对函数的调用:
person.ReadBook(bookl, new Glasses());
这样只有需要读书的人,才会被配一副眼镜了,实现资源的精确分配。
可是呢,现在每次读书都需要配一副新眼镜,太浪费了。
(4)优化版本四:封装注入
person.ReadBook(book, person.MyGlasses);
每次取自己之前的眼镜最符合现实需求。但是又出现了之前的问题:person.MyGlasses 参数可能为空,怎么办?
让person.MyGlasses封装的get 函数自己去解决这个逻辑:
public Glasses MyGlasses{ get{ if(this.myGlasses == null){ this.myGlasses = new Glassess(); return this.myGlasses; } } }
然后
void ReadBook(Book book){ WearGlasses(this.glasses); Read(book); }
总结:从优化一到优化三分别是三种依赖注入的手段:属性注入、构造函数注入,普通成员函数注入;让每一个模块独立而完整,是说各自的需求各自解决,每个模块规规矩矩,这个模块被替换掉,也不会有问题。
(二)让连接桥梁坚固而兼容
模块好比孤岛,孤岛之间需要桥梁去连接。我们需要这些桥梁坚固(具有不变性),还可以兼容各种岛屿(具有兼容性)。
需求发生变化,我们要让变化尽量落在岛屿上,而不是桥梁上,因为更换桥梁的成本更高,风险更大!!!