设计模式-六大原则

工欲善其事,必先利其器。 学习技术,贵“”,重“”,忌“”。 切不可“这山望着那山高”

在这里插入图片描述

SOLID 原则 -SRP 单一职责原则
SOLID 原则 -OCP 开闭原则
SOLID 原则 -LSP 里式替换原则
SOLID 原则 -ISP 接口隔离原则
SOLID 原则 -DIP 依赖倒置原则
DRY 原则、KISS 原则、YAGNI 原则、LOD 法则

1. 六大原则

有人问,在进行重构与架构设计的时候,我应该如何运用设计模式,我的建议是,应该先熟悉并掌握六大原则,让写的代码尽量符合这几个原则,自然代码不会差到哪里去,然后才去考虑如何用设计模式来优化自己的整体代码结构。

并不是为了设计模式而设计模式,这就变成了过渡设计。

架构应该演进而来,并不是一开始就能很全,所以一开始不要想太多,基本符合六大原则就OK了。

六大原则分别为:单一职责,里氏替换原则,依赖倒置原则,接口隔离原则,迪米特法则,开闭原则.

单一职责原则 告诉我们实现类要职责单一;
里氏替换原则 告诉我们不要破坏继承体系;
依赖倒置原则 告诉我们要面向接口编程;
接口隔离原则 告诉我们在设计接口的时候要精简单一;
迪米特法则 告诉我们要降低耦合;
而开闭原则是总纲,开闭原则 告诉我们要对扩展开放,对修改关闭。
下面细细品味一下六大原则

1.1 单一职责(Single Responsibility Principle,简称是SRP)

就一个类而言,应该仅有一个引起它变化的原因;
把各个功能独立出来,让它们满足单一职责原则;

举个栗子:

// 可以看出来,这里的接口设计的有问题,工作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();    
}

注意:
单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否有优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。

避免设计过度:
导致于类/方法/接口过多,反而不好管理,当然也要避免违背。

遵循单一职责原的优点:

  1. 提高类的可维护性,可读写性;可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;代码少了,不复杂,可读性也就好了,哈哈哈。

  2. 提高系统的可维护性;系统由类组成的,每个类的可维护性高,相对来讲整个系统的可维护性就高。可读性提高了,当然就容易维护了,嘿嘿。

  3. 降低变更的风险,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。一个类的职责越多,变更的可能性就更大,变更带来的风险也就越大,

1.2 开闭原则(Open Closed Principle,简称是OCP)

用抽象构建框架,用实现扩展细节(抽象化是开闭原则的关键)
开闭原则的核心是:对扩展开放,对修改关闭

举个栗子:

// 错误的示范,缓存的栗子
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 原则并不是说绝对不可以修改原始类的。
当我们嗅到原来的代码“腐化气味”时,应该尽早地重构,以便使代码恢复到正常的“进化”过程,而不是通过继承等方式添加新的实现,这会导致类型的膨胀以及历史遗留代码的冗余。

避免设计过度:
切忌到处都抽象,会导致系统过度设计,过度复杂。这反而是不利于系统的维护。完全的开闭原则是不可能实现的,所以请保持简单设计,在需要的时候做符合开闭原则的设计。

1.3 里氏替换原则(Liskov Substitution Principle,简称LSP)

子类继承父类后,可以扩展父类的功能,也可以实现父类抽象的方法,但不能改变父类原有已实现的功能;(子类可以对父类进行扩展,但不能改变父类现有的功能)
说白了 里氏替换原则其实就是为“良好的继承”制定一些规范;
里氏替换原则是实现开闭原则的重要方式之一;

注意几点:

  • (关键点)子类可以实现父类的抽象方法,但尽量不要覆盖父类的非抽象方法

  • 子类中可以增加自己特有的方法

  • 子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。(比如: 父类参数 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. 提高代码的可扩展性,子类可形似于父类,但异于父类,保留自我的特性;

缺点:
1. 继承是侵入性的,只要继承就必须拥有父类的所有方法和属性,在一定程度上约束了子类,降低了代码的灵活性;
2. 增加了耦合,当父类的常量、变量或者方法被修改了,需要考虑子类的修改,所以一旦父类有了变动,很可能会造成非常糟糕的结果,要重构大量的代码。

比较通用的做法是(只是建议):
原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

遵守里氏替换原则,在一定程度上增强程序的健壮性

1.4 依赖倒置原则(Dependence Inversion Principle,简称DIP)

高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。

依赖倒置原则的核心思想是面向接口编程,主要是实现解耦;

  • 高层模块不应该依赖底层模块(具体实现细节),二者都应该依赖其抽象(抽象类或接口)
  • 抽象不应该依赖细节
  • 细节应该依赖于抽象

这么去理解上面的这几句话?
抽象: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());
	}
}

注意:
低层模块尽量都要有抽象类或接口,或者两者都有。
变量的声明类型尽量是抽象类或接口。
使用继承时遵循里氏替换原则。

1.5 接口隔离原则(Interface Segregation Principle,简称ISP)

类间的依赖关系应该建立在最小的接口上。接口隔离原则将非常庞大、臃肿的接口拆分成更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。
接口隔离原则的目的是系统解开耦合,从而容易重构、更改和重新部署。
客户端不应该依赖它不需要的接口。

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 空实现。

在这里插入图片描述

1.6 迪米特法则(Law Of Demeter,简称LOD)

也称为最少知识原则(Least Knowledge Principle),
通俗地讲,一个类应该对自己需要耦合或调用的类知道得最少,类的内部如何实现与调用者或者依赖者没关系,调用者或者依赖者只需要知道它需要的方法即可,其他的可一概不用管。
类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
少暴露细节.

表现如下:

  • private 方法 主要实现类本身的逻辑功能
  • protected 方法主要暴露给子类用
  • public 方法主要是给其它类提供接口

举个栗子:

// 女朋友 让你给 她 数星星
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.7 总结

写的代码 应该 保持 高可扩展性高内聚低耦合
在经历了各版本的变更之后依然保持清晰、灵活、稳定的系统架构。
虽然 实际情况下,会因为时间,项目需求经常变更,场景的各种原因,但是我们还是朝这个方向努力,努力遵守六大原则 就是 让代码更稳定,灵活,清晰的 第一步。

六大原则是一把“双刃剑”,使用得当是一把神兵利器,使用过度那么就是自杀行为。
在这里插入图片描述
遵守的差,点将会在小圆内部;
过渡遵守,点将会落在大圆外部;
设计1、设计2属于良好的设计;
设计3、设计4设计虽然有些不足,但也基本可以接受;
设计5则严重不足,对各项原则都没有很好的遵守;
设计6则遵守过渡了,
设计5和设计6都是迫切需要重构的设计。
在这里插入图片描述

2. 各种设计模式了解(下个章节讲解部分常用的)

  • 创建型
  1. Factory Method(工厂方法)
  2. Abstract Factory(抽象工厂)
  3. Builder(建造者)
  4. Prototype(原型)
  5. Singleton(单例)
  • 结构型
  1. Adapter Class/Object(适配器)
  2. Bridge (桥接)
  3. Composite(组合)
  4. Decorator(装饰) Android源码中的ContextWrapper
  5. Facade(外观)
  6. Flyweight(享元)
  7. Proxy(代理)
  • 行为型
  1. Interpreter(解释器)
  2. Template Method(模板方法)
  3. Chain of Responsibility(责任链) Android的事件分发机制
  4. Command(命令)
  5. Iterator(迭代器)
  6. Mediator(中介者)
  7. Memento(备忘录) 类似游戏的保存与恢复
  8. Observer(观察者)
  9. Iterator(迭代器)
  10. Strategy(策略)
  11. Visitor(访问者)

3. 新知识扩展

对象池模式

规格模式

雇工模式
黑板模式

空对象模式
其实说白了就是实现了一个空类,没有任何操作,这样的做法可以避免外部代码进行非NULL检查判断。
在Kotlin,Swifth 普及的今天,可能很多代码已经不需要做非空判断检查了,但是建议可以使用这种空对象模式返回一个空对象,代码更健壮,容错更高,也做了防呆.

空对象模式资料1

空对象模式资料2

4. 架构了解

4.1 组件化架构

组件化设计可以理解为 更细粒度的系统拆分

5. 推荐资料

如何去绘制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 里氏替换原则

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值