【第八章:设计模式】第28节:面试常考设计模式 (上)
大家好,从本小节开始我们一起交流常用的设计模式。设计模式是一种思想,并不是一门具体的技术,没有很多的工作积累是不可能真正理解设计模式的。本专刊中的设计模式章节,我们重点阐述面试中常见的设计模式的原理与使用。
设计模式就是在软件开发过程中所总结形成的一系列准则。当我们遇到一些场景的时候,使用恰当的设计模式可以使得软件设计更加健壮,增强程序的扩展性。在讲解面试中常考的设计模式之前,我们先来看看设计模式的六大原则吧。
设计模式的六大原则如下:
- 单一职责原则
- 里氏替换原则
- 依赖倒置原则
- 接口隔离原则
- 迪米特法则
- 开闭原则
单一职责原则(Single responsibility principle,SRP):
单一职责规定了一个类应该只有一个发生变化的原因。如果一个类承担了多个职责,则会导致多个职责耦合在一起。但部分职责发生变化的时候,可能会导致其余职责跟着受到影响,也就是说我们的程序耦合性太强,不利于变化。
举个例子,我们来看下边的一个接口IUser的类图:
IUser类中拥有设置和获取用户名字和年龄的方法,还有指挥该User去跑步,学习以及玩耍的功能。那么,IUser类其实就是违反了单一职责原则。我们可以将其拥有到职责进行划分,一类是用户User的固有属性,另一类是用户User拥有的实际能力。固有属性包括设置和获取名字和年龄,实际能力则包括User具有的跑步,学习以及玩耍功能。
所以,我们需要将该IUser类进行划分,分为多个接口,划分之后的类图如下所示:
那么,单一职责有哪些优点呢?
- 降低了类的复杂度,每一个类都有清晰明确的职责。
- 程序的可读性和可维护性都得到了提高。
- 降低业务逻辑变化导致的风险,一个接口的修改只对相应的实现类有影响,对其他接口无影响。
里氏替换原则(Liskov Substitution Principle,LSP):
里氏替换是指所有父类可以出现的地方,子类就都可以出现,使用子类来替换父类,调用方不需要关心目前传递的父类还是子类。
在前面Java基础章节中,我们介绍了继承。通过继承,实现了代码的共享和复用。但是继承是强侵入性的,子类必须拥有父类所有的非私有属性和方法,在一定程度上降低了子类的灵活性。
通过里氏替换原则,我们可以将子类对象做为父类对象来使用,屏蔽了不同子类对象之间的差异,写出通用的代码,做出通用的编程,以适应需求的不断变化。里氏替换之后,父类的对象就可以根据当前赋值给它的子类对象的特性以不同的方式运作。
接下来,我们一起看一个Demo:
package niuke; public class LiSiTest { public static void main(String[] args) { // 通过传入子类对象来替换父类 eat(new Dog()); eat(new Cat()); } private static void eat(Animals animal){ animal.eat(); } } abstract class Animals{ abstract void eat(); } class Dog extends Animals{ @Override void eat() { System.out.println("我是小狗,喜欢吃大肉"); } } class Cat extends Animals{ @Override void eat() { System.out.println("我是小猫,喜欢吃小鱼干"); } }
那么,里氏替换原则有哪些优点呢?
里氏替换原则可以增强程序的健壮性,子类可以任意增加和缩减,我们都不需要修改接口参数。在实际开发中,实现了传递不同的子类来完成不同的业务逻辑。
依赖倒置原则(Dependence Inversion Principle,DIP):
依赖倒置原则是指高层模块不应该依赖于底层模块,抽象不应该依赖细节,细节应该依赖抽象。在Java中,接口和抽象类都是抽象,而其实现类就是细节。也就是说,我们应该做到面向接口编程,而非面向实现编程。
我们来看一个案例,讲述饲养员喂养的故事。初始类图如下:
开始的时候,饲养员只需要喂养小狗即可。代码实现如下:
package niuke; public class DIPTest { public static void main(String[] args) { Dog dog = new Dog(); Feeder feeder = new Feeder(); // 饲养员喂食小狗 feeder.feed(dog); } } // 定义小狗类 class Dog{ public void eat(){ System.out.println("小狗在吃东西,,,,,,"); } } // 定义饲养员类 class Feeder{ public void feed(Dog dog){ dog.eat(); } }
这是一个面向实现编程的案例,可以看到,我们的上层模块依赖了下层模块,并没有做到面向接口编程。当饲养员想要喂食其余动物的时候,发现其并不具备相应的能力,因为feed方法中的参数是一个具体的实现,是一个细节。
然后,我们再来看下,如何做到依赖倒置,实现面向接口编程。
代码实现如下:
package niuke.mode; public class DIPTest { public static void main(String[] args) { Dog dog = new Dog(); Cat cat = new Cat(); Feeder1 zhangsan = new Feeder1(); // 饲养员zhangsan喂食小狗,小狗 zhangsan.feed(dog); zhangsan.feed(cat); // --------------------- // 接下来是另一位饲养员lisi,它们的工作方式不同 Feeder2 lisi = new Feeder2(); // 饲养员zhangsan喂食小狗,小狗 lisi.feed(dog);
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
<p> Java开发岗高频面试题全解析,专刊正文共计31节,已经全部更新完毕。专刊分9个模块来对Java岗位面试中的知识点进行解析,包括通用面试技能,Java基础,Java进阶,网络协议,常见框架以及算法,设计模式等。专刊串点成面的解析每个面试题背后的技术原理,由浅入深,循序渐进,力争让大家掌握面试题目的背后的技术原理,摒弃背题模式的陋习。 专刊详细信息,请查阅专刊大纲和开篇词的介绍。 本专刊购买后即可解锁所有章节,故不可以退换哦~ </p> <p> <br /> </p>