设计模式就该这么学:要走心才能遵循设计模式五大原则(第二篇)
摘要:设计模式是基于一定问题解决规律产生,自然也会有些公用的东西,这个公用的东西就是我今天要讲的设计模式的五大原则,当然原则只是战略层面的指导,没有代码能完全遵守着五大原则,要根据实际(zou)情况(xin)合理取舍
本文作为《设计模式就该这么学》系列文章的第二篇,今天我们来聊一下设计模式中的五大原则!
《设计模式就该这么学》系列文章:
一、到底是哪五大原则
设计模式(面向对象)五大原则可以简称为SOLID,SOLID是面向对象设计和编程(OOD&OOP)中几个重要编码原则(Programming Priciple)的首字母缩写,它目的就是为了写出可复用、可扩展、高内聚、低耦合的代码。
当然原则只是战略层面的指导,没有代码能完全遵守着五大原则,要根据实际(zou)情况(xin)合理取舍。
下面我们来看看SOLID的具体描述:
SRP | The Single Responsibility Principle | 单一责任原则 |
OCP | The Open Closed Principle | 开放封闭原则 |
LSP | The Liskov Substitution Principle | 里氏替换原则 |
DIP | The Dependency Inversion Principle | 依赖倒置原则 |
ISP | The Interface Segregation Principle | 接口分离原则 |
1、单一职责
一个类只做一种类型责任,当这个类需要承当其他类型的责任的时候,就需要分解这个类。不过在现实开发中,这个原则是最不可能遵守的,因为每个人对一个类的哪些功能算是同一类型的职责判断都不相同。
2、开放封闭原则
软件实体应该是可扩展,而不可修改的。也就是说,你写完一个类,要想添加功能,不能修改原有类,而是想办法扩展该类。即对扩展开放,对修改关闭。
3、里氏替换原则
当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。也就是说接口或父类出现的地方,实现接口的类或子类可以代入,这主要依赖于多态和继承。
4、 接口分离原则
(1)不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。
(2)不要提供一个大的接口包括所有功能,应该根据功能把这些接口分割,减少依赖。
5、依赖倒置原则
(1)高层模块不应该依赖于低层模块,二者都应该依赖于抽象
(2)抽象不应该依赖于细节,细节应该依赖于抽象
二、举个例子来介绍下五大原则
以动物爬树来讲单一原则,用一个类描述这个场景
class Animal { public void breathe(string animal) { System.out.println(animal+"爬树"); } } class Program { static void Main(string[] args) { Animal animal = new Animal(); animal.breathe("猫"); animal.breathe("蛇"); } }
我们发现不是所有动物都会爬树的,比如鸟就不会爬树,根据单一职责原则,我们将Animal类细分为爬树动物类和飞翔动物类,如下所示:
class SpeelAnimal { public void pashu(string animal) { System.out.println(animal+"爬树"); } } class FlyAnimal { public void fly(string animal) { System.out.println(animal + "飞翔"); } } class Program { static void Main(string[] args) { SpeelAnimal speelAnimal = new SpeelAnimal(); speelAnimal.pashu("蛇"); FlyAnimal flyAnimal = new FlyAnimal(); flyAnimal.breathe("麻雀"); } }
我们发现这样修改的花销很大,既要将原来的类分解,又要修改客户端。而直接修改Animal类虽然违背了单一职责原则,但花销小的多,如下所示
class Animal { public void action(string animal) { if ("麻雀".Equals(animal)) { System.out.println(animal + "飞翔"); } else { System.out.println(animal + "攀爬"); } } } class Program { static void Main(string[] args) { Animal animal = new Animal(); animal.action("蛇"); animal.action("麻雀"); } }
可以看到,这样代码量少了很多,可还有一个问题,鸡会打鸣,而蛇不会,有一天需要增加鸡打鸣的方法,会发现又要直接修改上面的方法,这种修改直接在代码级别违背了单一职责原则,虽然修改起来最简单,但隐患最大。还有一种修改方式:
Animal { public void pashu(string animal) { System.out.println(animal+"爬树"); } public void fly(string animal) { System.out.println(animal + "飞翔"); } public call() { System.out.println(animal + "鸣叫"); } } class Program { static void Main(string[] args) { Animal animal = new Animal(); animal.pashu("蛇"); animal.call("麻雀"); } }
这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然违背了单一职责原则,但在方法级别上却是符合单一职责原则的。那么在实际编程中,采用哪一种呢?我的原则是,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,才可以在方法级别违反单一职责原则。
遵循单一职责的优点:
1)降低类的复杂度,一个类只负责一项职责。
2)提高类的可读性,可维护性
3)降低变更引起的风险。
开放封闭原则这个没什么好说的,再来说说里氏替换原则,它其实有两种定义,
第一种定义:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换为o2,程序P的行为没有发生变化,那么类型S是类型T的子类型。
第二种定义:所有引用基类的地方必须透明的使用其子类的对象。第二种定义明确的说,只要父类能出现的地方子类也可以出现,而且替换为子类不会产生任何错误或异常,但是反过来就不行,有子类出现的地方,父类未必就能适应。由定义可知,在使用继承时,会遵循里氏替换原则,集成的好处自不必多说,代码共享,重用性高,但是在子类中尽量不要重写和重载父类的方法。为什么呢?
- 继承是侵入性的,只要继承,就必须拥有父类的所有方法和属性
- 降低了代码的灵活性,子类必须拥有父类的属性和方法,让子类有了一些约束
- 增加了耦合性,当父类的常量,变量和方法被修改了,需要考虑子类的修改,这种修改可能带来非常糟糕的结果,要重构大量的代码。
接下来以一个两数相减的小李子说明一下:
class A{ public int func1(int a,int b){ return a-b; } } public class Client{ public static void main(string[] args){ A a=new A(); System.out.println("100-50="+a.func1(100,50)); System.out.println("100-80="+a.func1(100,80)); } }
运行结果:
100-50=50
100-80=20
后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。
Class B extends A{ public int func1(int a,int b){ return a+b; } public int func2(int a,int b){ return func1(a,b)+100; } } public class Client{ public static void main(string[] args){ B a=new B(); System.out.println("100-50="+b.func1(100,50)); System.out.println("100-80="+b.func1(100,80)); System.out.println("100+20+100="+b.func2(100,20)); } }
运行结果:
100-50=150
100-80=180
100+20+100=220
我们发现原来运行正常的相减功能发生了错误,这就是因为重写A类中的方法导致。
所以如果非要重写父类的方法,通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖,聚合,组合等关系代替
接下来说说依赖导致原则,它的定义是:
- 高层模块不应该依赖低层模块,两者都应该依赖抽象
- 抽象不应该依赖细节
- 细节应该依赖抽象
依赖倒置原则可以说是这里面最难用的一个原则了,那为什么我们还要要遵循依赖倒置原则?接下来以一个看视频的例子说明下问题,张三喜欢看douyin小视频
//Bibili类 public class Bibili类{ //阅读文学经典 public void watch(){ System.out.println("看douyin视频"); } }
再来看张三类
//张三类 public class Zhangsan{ //阅读文学经典 public void watch(douyin bili){ bili.watch(); } }
场景类
public class Client{ public static void main(Strings[] args){ Zhangsan zhangsan = new Zhangsan(); douyin bili = new douyin (); //zhangsan看douyin视频 zhangsan.watch(bili); } }
张三看了一段时间的小视频时候,又想看电视机剧《白夜追凶》,发现这个只在优酷才能看,于是实现一个优酷类
public class Youku{ //看电视剧 public void watch(){ System.out.println("看白夜追凶"); } }
现在我们再来看代码,发现张三类的read方法只与Bilibi类是强依赖,紧耦合关系,张三竟然阅读不了小说类。这与现实明显的是不符合的,代码设计的是有问题的。那么问题在那里呢?
我们看张三类,此类是一个高层模块,并且是一个细节实现类,此类依赖的是一个Bilibi类,而Bilibi类也是一个细节实现类。这是不是就与我们说的依赖倒置原则相违背呢?依赖倒置原则是说我们的高层模块,实现类,细节类都应该是依赖与抽象,依赖与接口和抽象类。
为了解决张三看电影的问题,我们根据依赖倒置原则先抽象一个阅读者接口,下面是完整的uml类图:
观看者接口
public interface Watcher{ //阅读 public void watch(Watcher watch){ watch.watch(); } }
视频接口
public interface Video{ //被观看 public void video(); }
再定义douyin类和Youku类
//douyin类 public class douyin implements Watcher{ public void watcher(){ System.out.println("看douyin小视频"); } }
//Youku类 public class Youku implements Watcher{ public void watcher(){ System.out.println("看白夜追凶"); } }
然后实现张三类
//张三类 public class Zhasan implements Video{ //观看 public void watch(Video video){ video.watch(); } }
然后再让张三来看douyin视频和优酷电视剧《白夜追凶》
public class Client{ public static void main(Strings[] args){ Watcher zhansan = new Zhansan(); Video douyin = new douyin(); //张三看douyin小视频 zhangsan.watch(douyin); Video youku = new Youku(); //张三看douyin小视频 zhangsan.watch(youku); } }
至此,张三是可以看douyin小视频,又可以看优酷电视剧《白夜追凶》了,目的达到了。
为什么依赖抽象的接口可以适应变化的需求?这就要从接口的本质来说,接口就是把一些公司的方法和属性声明,然后具体的业务逻辑是可以在实现接口的具体类中实现的。所以我们当依赖对象是接口时,就可以适应所有的实现此接口的具体类变化。由此也可以看出依赖倒置原则用好可以减少类间的耦合性,提高系统的稳定,降低并行开发引起的风险,提高代码的可读性和可维护性。
至此,设计模式的五大原则介绍完毕,接下来一篇文章我会以微信订阅号来讲观察者模式,同时会讲到实际应用的一个例子!
学习本就是一个不断模仿、练习、再到最后面自己原创的过程。
虽然可能从来不能写出超越网上通类型同主题博文,但为什么还是要写?
于自己而言,博文主要是自己总结。假设自己有观众,毕竟讲是最好的学(见下图)。于读者而言,笔者能在这个过程get到知识点,那就是双赢了。
当然由于笔者能力有限,或许文中存在描述不正确,欢迎指正、补充!
感谢您的阅读。如果本文对您有用,那么请点赞鼓励。