工欲善其事,必先利其器。 学习技术,贵“
恒
”,重“精
”,忌“浮
”。 切不可“这山望着那山高”
有人问,在进行重构与架构设计的时候,我应该如何运用设计模式,我的建议是,应该先熟悉并掌握六大原则,让写的代码尽量符合这几个原则,自然代码不会差到哪里去,然后才去考虑如何用设计模式
来优化自己的整体代码结构。
并不是为了设计模式而设计模式,这就变成了过渡设计。
架构应该演进而来,并不是一开始就能很全,所以一开始不要想太多,基本符合六大原则就OK了。
六大原则分别为:单一职责,里氏替换原则,依赖倒置原则,接口隔离原则,迪米特法则,开闭原则.
单一职责原则 告诉我们实现类要职责单一;
里氏替换原则 告诉我们不要破坏继承体系;
依赖倒置原则 告诉我们要面向接口编程;
接口隔离原则 告诉我们在设计接口的时候要精简单一;
迪米特法则 告诉我们要降低耦合;
而开闭原则是总纲,开闭原则 告诉我们要对扩展开放,对修改关闭。
下面细细品味一下六大原则
就一个类而言,应该仅有一个引起它变化的原因;
把各个功能独立出来,让它们满足单一职责原则;
举个栗子:
// 可以看出来,这里的接口设计的有问题,工作Work 和 生活Lift 没有分开,这是一个严重的错误;
// 接口里展示了两个职责,工作 与 生活;
// 接口设计的一团糟,我们下面修改下;
public interface Human {
public void work相关1();
public void work相关2();
public void work相关3();
public void work相关4();
... ...
public void life相关1();
public void life相关2();
public void life相关3();
public void life相关4();
... ...
}
修改后的栗子,将 Human 的 工作 抽取为 Work,生活抽取为 Life;(下面的类图应该是虚线的箭头)
当然除了在类中,函数的定义也可以尽量的单一职责,颗粒度多细,取决于实际的场景;
比如 初始化数据 initAllDatas,初始化界面 initAllViews 等等
// Activity onCreate demo 处理演示
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
loginTimes = -1;
Bundle bundle = getIntent().getExtras();
String strEmail = bundle.getString(AppConstants.Email);
etEmail = (EditText) f indViewById(R.id.email);
etEmail.setText(strEmail);
etPassword = (EditText) f indViewById(R.id.password); // 登录事件
Button btnLogin = (Button) findViewById(R.id.sign_in_button);
btnLogin.setOnClickListener(this);
// 获取2个MobileAPI,获取天气数据,获取城市数据
loadWeatherData();
loadCityData();
}
// 更改后的代码效果
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initVariables();
initViews(savedInstanceState);
loadData();
}
注意:
单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否有优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。
避免设计过度:
导致于类/方法/接口过多,反而不好管理,当然也要避免违背。
遵循单一职责原的优点:
提高类的可维护性,可读写性;可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;代码少了,不复杂,可读性也就好了,哈哈哈。
提高系统的可维护性;系统由类组成的,每个类的可维护性高,相对来讲整个系统的可维护性就高。可读性提高了,当然就容易维护了,嘿嘿。
降低变更的风险,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。一个类的职责越多,变更的可能性就更大,变更带来的风险也就越大,
用抽象构建框架,用实现扩展细节(抽象化是开闭原则的关键)
开闭原则的核心是:对扩展开放,对修改关闭
举个栗子:
// 错误的示范,缓存的栗子
public class OpenCloseTest {
// 这里存在的问题就是,如果有一天又增加其它缓存方式,
// 又要修改 OpenCloseTest 的loadImage代码
// 整个loadImage 不仅越来越复杂,脆弱,也会越来越臃肿,可扩展性也很差
// 这里就违背了开闭原则,扩展不是开放的,修改不是封闭的
public void loadImage(String type) {
if ("内存缓存".equals(type)) { // 内存缓存
... ...
} else if ("硬盘缓存".equals(type)) { // 硬盘缓存
... ...
} else { // 双缓存
... ...
}
}
}
// 修改后的栗子 1. 增加抽象
public interface ICache {
public Bitmap get(String url);
}
// 内存缓存
public class MemoryCache implements ICache {
}
// 硬盘缓存
public class DiskCache implements ICache {
}
// ... ...
public class OpenCloseTest {
// 外部传入各种缓存,可扩展性,灵活性高;
// 符合开闭原则,扩展是开放的,修改是封闭的
public void loadImage(ICache cache) {
Bitmap b = cache.get("http://test");
}
}
注意:
开闭原则指导我们,当软件需要变化时,应该尽量通过扩展的方式来实现变化,而不是通过修改己有的代码来实现。
这里的“应该尽量”4个字说明 OCP 原则并不是说绝对不可以修改原始类的。
当我们嗅到原来的代码“腐化气味”时,应该尽早地重构,以便使代码恢复到正常的“进化”过程,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。
避免设计过度:
切忌到处都抽象,会导致系统过度设计,过度复杂。这反而是不利于系统的维护。完全的开闭原则是不可能实现的,所以请保持简单设计,在需要的时候做符合开闭原则的设计。
子类可以扩展父类的功能,但不能改变父类原有的功能;
说白了 里氏替换原则其实就是为“良好的继承”制定一些规范;
里氏替换原则是实现开闭原则的重要方式之一;
注意几点:
(关键点)子类可以实现父类的抽象方法,但尽量不要覆盖父类的非抽象方法。
子类中可以增加自己特有的方法。
当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。(比如: 父类参数 T,子类 参数 S,要求 S 大于 T。要么同一个类型,要么 T 是 S 的子类;比如 父类参数 HashMap,子类就是 Map)
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。(比如:父类 返回值为类型T,子类返回值为S,要求S必须小于等于T。要么 S 和 T 是同一个类型,要么 S 是 T 的子类;如父类要求返回List,那么子类就应该返回List的实现ArrayList,父类是采用泛型,那么子类则不能采用泛型,而是具体的返回)
举个栗子:
public interfaceclass I {
public List testMapRes2();
}
// 父类
public class A implements I {
public int add(int num1, int num2) {
return num1 + num2;
}
// 父类 输入参数是 HashMap 类型
public String testMap(HashMap map) {
return "父类 A testMap 函数 测试>>>";
}
// 实现父类的抽象方法
@Override
public ArrayList testMapRes2() { // 方法的返回值 ArrayList 比父类的更严格
return null;
}
}
// 子类 继承 父类A,覆盖了父类的非抽象方法
public class B extends A {
// 错误的方法!!!
@Override
public int add(int num1, int num2) {
return num1 - num2;
}
// 子类的输入参数是 Map 类型
// 子类的输入参数类型的范围扩大了
// @Override 是失效的,因为现在是 重载(Overload)
public String testMap(Map map/*子类的形参比父类的更宽松*/) {
return "子类 B testMap 函数 测试<<<";
}
// 添加自己的特有的方法
... ...
}
... ...
public static void main(... ...
B b = new B();
// 结果为5,覆盖重写父类的非抽象方法,导致计算结果有误
// 可以实现父类的抽象方法,但是不要覆盖父类的非抽象方法.
b.add(10, 5);
// 结果为 "父类 A testMap 函数 测试>>>" OK,正确的.
// 如果反过来,父类 Map,子类HashMap,子类被执行,可能会导致业务逻辑混乱,因为父类实现好的方法,一般是继承使用就好,.
// 所以子类中方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松
HashMap map = new HashMap();
b.testMap(map);
// 如果颠倒过来,子类为 HashMap,父类为 Map,那么就导致父类调不到...这是错误的!!!
// 实现I的抽象方法
A a = new A();
a.testMapRes2();
注意:
继承的优缺点:
优点:
1. 提高代码的重用性,子类拥有父类的方法和属性;
2. 提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性;
缺点:
1. 继承是侵入性的,只要继承就必须拥有父类的所有方法和属性,在一定程度上约束了子类,降低了代码的灵活性;
2. 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能会造成非常糟糕的结果,要重构大量的代码。
比较通用的做法是(只是建议):
原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。
遵守里氏替换原则,在一定程度上增强程序的健壮性
高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。
依赖倒置原则的核心思想是面向接口编程,主要是实现解耦;
这么去理解上面的这几句话?
抽象:Java代码中,抽象就是指 接口(interface)
或者 抽象类(abstract)
高层模块:负责复杂的业务逻辑
底层模块:具体实现细节,
举个栗子:
问题由来:类A直接依赖类B,假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。这种场景下,类A一般是高层模块,负责复杂的业务逻辑;类B和类C是低层模块,负责基本的原子操作;假如修改类A,会给程序带来不必要的风险。
class B { // 类B和类C是低层模块,负责基本的原子操作
public String getTestContent() {
return "获取A测试内容";
}
}
class C { // 类B和类C是低层模块,负责基本的原子操作
public String getTestContent() {
return "获取C测试内容";
}
}
// 类A直接依赖类B
class A { // 类A一般是高层模块,负责复杂的业务逻辑
// 假如要将类A改为依赖类C,则必须通过修改类A的代码来达成。
// 假如修改类A,会给程序带来不必要的风险。
public void test(B b) {
System.out.println("===依赖倒置测试===");
System.out.println(b.getTestContent());
}
}
public class Client {
public static void main(String[] args) {
A aa = new A();
aa.test(new B());
}
}
解决方案:将类A修改为依赖接口I,类B和类C各自实现接口I,类A通过接口I间接与类B或者类C发生联系,则会大大降低修改类A的几率
interface I {
public String getTestContent();
}
class B implements I { // 类B和类C各自实现接口I
@Override
public String getTestContent() {
return "获取A测试内容";
}
}
class C implements I { // 类B和类C各自实现接口I
... ...
// 类A直接依赖类B
class A { // 类A一般是高层模块,负责复杂的业务逻辑
// 类A通过接口I间接与类B或者类C发生联系,
// 则会大大降低修改类A的几率
public void test(I i) {
System.out.println("===依赖倒置测试===");
System.out.println(i.getTestContent());
}
}
public class Client{
public static void main(String[] args){
A a = new A();
a.test(new A());
a.test(new C());
}
}
注意:
低层模块尽量都要有抽象类或接口,或者两者都有。
变量的声明类型尽量是抽象类或接口。
使用继承时遵循里氏替换原则。
类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。
接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署。
客户端不应该依赖它不需要的接口。
Bob大叔(Robert C Martin)在21世纪早期将单一职责、开闭原则、里氏替换、接口隔离以 及依赖倒置(也称为依赖反转) 5个原则定义为SOLID 原则,作为面向对象编程的5个基本原则。
举个栗子:
// 错误示范,比如 定义了 改变世界 的接口
public interface 改变世界 {
void 写代码();
void 代码审查();
void APK提测();
void APK发布();
}
// 小公司 XXOO 刚开始的时候,一步到位
// 程序员A 实现了所有的方法,没办法,小公司,身兼多职
public 程序员A implements Travel {
}
'随着公司发展壮大,流程越来越规范'
依赖它不需要的接口,只是极少实现了部分接口的方法;
'程序员-A' 发现它是需要 写代码 就可以了;
'某Leader-B' 只是 代码审查;
'测试同学-C' 只是 APK测试;
'运营同学-D' 只是 APK发布;
这就导致接口隔离不是很少,其它人还是实现了多余的3个接口,这么办?
修改后:
interface 程序员相关 {
void 写代码();
... ...
}
interface 运营相关 {
void APK发布();
... ...
}
... ...
这样对应的人,只需要实现对应的接口就OK了
注意:
根据实际情况,具体业务具体分析,合理使用接口隔离原则;
在特定的场景下,如果很多类实现了同一个接口,并且都只实现了接口的极少部分方法,这时候很有可能就是接口隔离性不好,就要去分析能不能把方法拆分到不同的接口。
避免设计过度:
设计接口的粒度越小,系统越灵活是肯定的。但是过度把接口设计粒度锁到很小,这样会增加系统阅读代码的复杂度。接口设计尽量使其能完成一个特有的功能,而不能把一个功能再进行拆分,拆分出好多接口来,这就过度设计了,度很难把握。
再举一个栗子:
邮件APP,设计之初,收件箱与发件箱 共同实现了同一个邮箱接口(MailBox),接口具有(start 星标邮件,delete 删除邮件,reply 回复邮件,retry 发送重试 四 个 方法);这里的问题在于,发件箱, 只需要 邮件发送失败进行重试的操作,对于收件箱来说,这个方法是多余的,这样的设计不满足接口分离原则。
改进后,将 retry 方法 提取到新接口 MailError 中去,收件箱不会再有 retry 空实现。
也称为最少知识原则(Least Knowledge Principle),
通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,类的内部如何实现与调用者或者依赖者没关系,调用者或者依赖者只需要知道它需要的方法即可,其他的可一概不用管。
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
少暴露细节.
举个栗子:
// 女朋友 让你给 她 数星星
public class GirlFriend {
// GirlFriend 类 除了 和 My 这个朋友类 有交流,还和 Star 有了交流;
// 迪米特法则说是一个类只和朋友类交流,这里是不对的!!!
public void commond(My my) {
List<Star> starList = new ArrayList();
for (int i=0;i<1000;i++) {
starList.add(new Star())
}
my.数星星(starList)
}
}
// 修改后的例子
// GirlFriend 避开了 对陌生类 star 的访问,减少耦合;
public class GirlFriend {
public void commond(My my) {
my.数星星()
}
}
注意:
记住,类只和朋友交流,不要和陌生类交流,有危险,妈妈说的;
避免设计过度:
过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。
迪米特法则 的优点:
类之间解耦,弱耦合。
写的代码 应该 保持 高可扩展性
、高内聚
、低耦合
;
在经历了各版本的变更之后依然保持清晰、灵活、稳定的系统架构。
虽然 实际情况下,会因为时间,项目需求经常变更,场景的各种原因,但是我们还是朝这个方向努力,努力遵守六大原则 就是 让代码更稳定,灵活,清晰的 第一步。
六大原则是一把“双刃剑”,使用得当是一把神兵利器,使用过度那么就是自杀行为。
遵守的差,点将会在小圆内部;
过渡遵守,点将会落在大圆外部;
设计1、设计2属于良好的设计;
设计3、设计4设计虽然有些不足,但也基本可以接受;
设计5则严重不足,对各项原则都没有很好的遵守;
设计6则遵守过渡了,
设计5和设计6都是迫切需要重构的设计。
对象池模式
规格模式
雇工模式
黑板模式
空对象模式
其实说白了就是实现了一个空类,没有任何操作,这样的做法可以避免外部代码进行非NULL检查判断。
在Kotlin,Swifth 普及的今天,可能很多代码已经不需要做非空判断检查了,但是建议可以使用这种空对象模式返回一个空对象,代码更健壮,容错更高,也做了防呆.
空对象模式资料1
空对象模式资料2
组件化设计可以理解为 更细粒度的系统拆分
如何去绘制UML类图,这里推荐我之前写的文章
这里再推荐几本不错的书籍《设计模式之禅》,《大话设计模式》,《Android 源码设计模式解析与实战》,《图解设计模式》,《大话重构》
多阅读下别人的源码,比如 Android 的同学,可以看看 Glide,Retrofit 等 的源码。
http://linbinghe.com/2017/1646c9f3.html 谈谈23种设计模式在Android源码及项目中的应用
http://blog.csdn.net/zhengzhb/article/details/7296944 设计模式六大原则(6):开闭原则
http://www.cnblogs.com/yanghuahui/p/3308487.html 用枚举实现工厂方法模式更简洁?
https://zhuanlan.zhihu.com/p/33607390 快速理解 设计模式六大原则
https://wenku.baidu.com/view/59684b4326d3240c844769eae009581b6bd9bd15.html 里氏替换原则