设计模式基础
1. 设计模式的定义
在软件工程中,设计模式是对软件中普遍存在(反复出现)的各种问题,所提出的解决方案。
2. 设计模式的目的
我们在编写程序的过程中,会面临着来自耦合性、内聚性、以及可维护性、可拓展性、可重用性、灵活性等多方面的挑战,设计模式就是为了让程序拥有更好的:
- 代码重用性(即:相同功能的代码,不用多次编写)
- 可读性(即:编程规范性,便于其他程序员阅读和理解)
- 可扩展性(即:当需要增加新的功能时,非常方便,并且方便维护)
- 可靠性(即:代码需要有很好的鲁棒性,比如我们添加新功能后,对原有的代码不会产生影响)
- 使程序具有高内聚,低耦合的特性
3. 设计模式的七大原则
设计模式原则,其实就是程序员在编程时应该遵守的原则,也是各种设计模式的基础(即:设计模式为什么这么设计的依据)
1. 单一职责原则
1. 基本介绍
对类来说的,即一个类应该只负责一项原则。如类A负责两个不同的职责:职责1、职责2。当职责1需求变更而改变类A时,可能造成职责2执行错误,所以需要将类A按照职责分解为A1类和A2类,分别负责职责1和职责2。
2. 代码说明
//调用
public class MainClass{
public static void main(String[] args){
//对a的调用
//此时对Vehicle来说,它既负责汽车的职责,也负责飞机和轮船的职责
//这样就会导致飞机在公路上跑,轮船在公路上跑的错误
Vehicle vehicle = new Vehicle();
vehicle.run("汽车");
vehicle.run("飞机");
vehicle.run("轮船");
//对b的调用
RoadVehicle rv = new RoadVehicle();//只负责路上跑的
rv.run("汽车");
AirVehicle av = new AirVehicle();//只负责天上飞的
av.run("飞机");
BoatVehicle bv = new BoatVehicle();//只负责水里游的
bv.run("轮船");
//对c的调用
Vehicle vehicle = new Vehicle();
vehicle.roadRun("汽车");//该方法负责路上跑的
vehicle.airRun("飞机");//该方法负责天上飞的
vehicle.boatRun("轮船");//该方法负责水里游的
}
}
//a. 违反单一职责原则的类
class Vehicle{
public void run(String vehicle){
System.out.println(vehicle + "在公路上跑");
}
}
//b. 将Vehicle类改造成单一职责原则的类
//可以根据不同的职责将Vehicle类分解成不同的类,让每个类负责一项交通工具的职责,如下:
class RoadVehicle{
public void run(String vehicle){
System.out.println(vehicle + "在公路上跑");
}
}
class AirVehicle{
public void run(String vehicle){
System.out.println(vehicle + "在天上飞");
}
}
class BoatVehicle{
public void run(String vehicle){
System.out.println(vehicle + "在水上游");
}
}
//c. 但上面这种分解类的方法改动较大
//改进:可以直接修改Vehicle类,让其保证方法级别上的单一职责而不是类级别上的单一职责,这样改动的代码较小,如下:
class Vehicle{
public void roadRun(String vehicle){
System.out.println(vehicle + "在公路上跑");
}
public void airRun(String vehicle){
System.out.println(vehicle + "在天上飞");
}
public void boatRun(String vehicle){
System.out.println(vehicle + "在水上游");
}
}
3. 单一职责原则补充
- 降低类的复杂度,一个类只负责一项职责
- 提高类的可读性、可维护性
- 降低变更引起的风险
- 通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级别违反单一职责原则;只有类中的方法数量足够少,才可以在方法级别保持单一职责原则。
2. 接口隔离原则
1. 基本介绍
客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小的接口上
2. 代码说明
//假设:类A通过接口Interface依赖类B,类C通过接口Interface依赖类D。(暗示B和D是接口Interface的实现类)
//假设:Interface中有5个方法fun1,fun2,fun3,fun4,fun5。
//假设:类A需要其中的fun1,fun2,fun3,类C需要其中的fun1,fun4,fun5
//那么接口Interface对于类A和类C来说都不是他们的最小接口
//那么很显然,类A对类B的依赖不是建立在最小的接口上,类C对类D的依赖同理
//即使接口Interface对于类A和类C来说不是最小接口,类B和类D也必须实现接口Interface中类A和类C不需要的方法
//a. 违反接口隔离原则的代码
interface Interface{
void fun1();
void fun2();
void fun3();
void fun4();
void fun5();
}
class B implements Interface{//需要实现所有方法
public void fun1(){
System.out.println("我是B的fun1");
}
public void fun2(){
System.out.println("我是B的fun2");
}
public void fun3(){
System.out.println("我是B的fun3");
}
public void fun4(){
System.out.println("我是B的fun4");
}
public void fun5(){
System.out.println("我是B的fun5");
}
}
class D implements Interface{//需要实现所有方法
public void fun1(){
System.out.println("我是D的fun1");
}
public void fun2(){
System.out.println("我是D的fun2");
}
public void fun3(){
System.out.println("我是D的fun3");
}
public void fun4(){
System.out.println("我是D的fun4");
}
public void fun5(){
System.out.println("我是D的fun5");
}
}
class A{
public void dependB1(Interface i){
i.fun1();
}
public void dependB2(Interface i){
i.fun2();
}
public void dependB3(Interface i){
i.fun3();
}
}
class C{
public void dependD1(Interface i){
i.fun1();
}
public void dependD2(Interface i){
i.fun4();
}
public void dependD3(Interface i){
i.fun5();
}
}
//对上面进行修改,使之遵从接口隔离原则
//可以对接口进行拆分,然后让类A和类C分别与他们需要的接口建立依赖关系。
//这些接口的实现类只需要分别实现类A和类C需要的方法
//也就是说此时类A对类B、类C对类D的依赖都是建立在最小接口上
//接口拆分
interface Interface1{
void fun1();
}
interface Interface2{
void fun2();
void fun3();
}
interface Interface3{
void fun4();
void fun5();
}
class B implements Interface1,Interface2{//类B实现接口1和接口2,只需要实现这两个接口中的所有方法
public void fun1(){
System.out.println("我是B的fun1");
}
public void fun2(){
System.out.println("我是B的fun2");
}
public void fun3(){
System.out.println("我是B的fun3");
}
}
class D implements Interface1,Interface3{//类D实现接口1和接口3,只需要实现这两个接口中的所有方法
public void fun1(){
System.out.println("我是D的fun1");
}
public void fun4(){
System.out.println("我是D的fun4");
}
public void fun5(){
System.out.println("我是D的fun5");
}
}
class A{
public void dependB1(Interface1 i){//通过接口1依赖B,比如这里可以传入new B()
i.fun1();
}
public void dependB2(Interface2 i){//通过接口2依赖B,比如这里可以传入new B()
i.fun2();
}
public void dependB3(Interface2 i){
i.fun3();
}
}
class C{
public void dependD1(Interface1 i){//通过接口1依赖D,比如这里可以传入new D()
i.fun1();
}
public void dependD2(Interface3 i){//通过接口3依赖D,比如这里可以传入new D()
i.fun4();
}
public void dependD3(Interface3 i){
i.fun5();
}
}
3. 依赖倒转原则
1. 基本介绍
依赖倒转原则是指:
- 高层模块不应该依赖低层模块,二者都应该依赖其抽象
- 抽象不应该依赖细节,细节应该依赖抽象
- 依赖倒转的中心思想是面向接口编程
- 依赖倒转原则是基于这样的设计理念:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础搭建的架构要稳定的多。在java中,抽象指的是接口或抽象类,细节就是具体的实现类
- 使用接口或抽象类的目的是制定好规范,而不涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成
2. 代码说明
//比如一个Person类实现打印消息功能
//未遵守依赖倒转原则的代码
class Email{
public String getMsg(){
return "email 消息";
}
}
class Person{
//很明显,如果我们这样写的话,Person直接依赖Email类,那么以后的拓展就会大受限制
//比如我们还需要Person打印qq消息,短信消息等等,那么Person类的这个方法也需要进行修改
public void printMsg(Email email){
System.out.println(email.getMsg());
}
}
//对上面改进使之遵守依赖倒转原则
//也就是依赖抽象而不是依赖细节
interface Msg{
String getMsg();
}
class Email implements Msg{
public String getMsg(){
System.out.println("Email消息");
}
}
class QQ implements Msg{
public String getMsg(){
System.out.println("QQ消息");
}
}
class Person{
//依赖接口,这样只需要我们传入不同的实现类对象,就可以打印不同信息
public void printMsg(Msg msg){
System.out.println(msg.getMsg());
}
}
3. 依赖关系传递的三种方式以及代码说明
- 接口传递
- 构造方法传递
- setter方法传递
//1. 通过接口传递
public interface ITV {
void play();
}
public interface IOpenAndClose {
void open(ITV itv);
}
public class ChangHongTV implements ITV {
@Override
public void play() {
System.out.println("长虹电视打开了");
}
}
public class OpenAndClose implements IOpenAndClose {
@Override
public void open(ITV itv) {
itv.play();
}
}
//2. 通过构造方法传递依赖关系
public interface ITV {
void play();
}
public interface IOpenAndClose {
void open();
}
public class ChangHongTV implements ITV {
@Override
public void play() {
System.out.println("长虹电视打开了");
}
}
public class OpenAndClose implements IOpenAndClose{
private ITV itv;
public OpenAndClose(ITV itv){
this.itv = itv;
}
public void open(){
itv.play();
}
}
//2. 通过setter方法传递依赖关系
public interface ITV {
void play();
}
public interface IOpenAndClose {
void open();
void setITV(ITV itv);
}
public class ChangHongTV implements ITV {
@Override
public void play() {
System.out.println("长虹电视打开了");
}
}
public class OpenAndClose implements IOpenAndClose{
private ITV itv;
public void setITV(ITV itv){
this.itv = itv;
}
public void open(){
itv.play();
}
}
//测试
public class MainTest {
@Test
public void t1() {//测试通过接口传递依赖关系
ITV changhong = new ChangHongTV();
OpenAndClose oc = new OpenAndClose();
oc.open(changhong);
}
@Test
public void t2() {//测试通过构造方法传递依赖关系
ITV changhong = new ChangHongTV();
OpenAndClose oc = new OpenAndClose(changhong);
oc.open();
}
@Test
public void t2() {//测试通过setter方法传递依赖关系
ITV changhong = new ChangHongTV();
OpenAndClose oc = new OpenAndClose();
oc.setITV(changhong);
oc.open();
}
}
4. 依赖倒转原则的补充
- 低层模块尽量都要有抽象类或接口,或者二者都有,程序稳定性更好。
- 变量的声明类型尽量是接口或抽象类,这样我们的变量引用和实际对象之间,就存在一个缓冲层,利于程序拓展和优化
- 继承时遵循里氏替换原则
4. 里氏替换原则
1. OO中的继承性的思考和说明
- 继承包含这样一层含义:父类中凡是已经实现好的方法,实际上是在设定规范和契约,虽然它不强制要求所有的子类必须遵守这些契约,但是如果子类对这些已经实现的方法任意修改,就会对整个继承体系造成破坏。
- 继承在给程序设计带来便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加对象间的耦合性,如果一个类被其他类继承,则当这个类修改时,必须考虑到所有的子类,并且父类修改之后,所有涉及到子类的功能都有可能产生故障。
- 问题提出:如何在编程中正确使用继承?–>里氏替换原则
2. 介绍与理解
- 如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。换句话说,所有引用基类的地方必须能透明的使用其子类的对象。
- 在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法
- 里氏替换原则告诉我们,继承实际上使两个类耦合性增强了,在适当的情况下,可以通过聚合、组合、依赖来解决问题
- 里氏替换原则是针对继承而言的,如果继承是为了实现代码重用,也就是为了共享方法,那么共享的父类方法就应该保持不变,不能被子类重新定义。子类只能通过新添加方法来扩展功能,父类和子类都可以实例化,而子类继承的方法和父类是一样的,父类调用方法的地方,子类也可以调用同一个继承得来的,逻辑和父类一致的方法,这时用子类对象将父类对象替换掉时,当然逻辑一致,相安无事
- 如果继承的目的是为了多态,而多态的前提就是子类覆盖并重新定义父类的方法,为了符合LSP,我们应该将父类定义为抽象类,并定义抽象方法,让子类重新定义这些方法,当父类是抽象类时,父类就是不能实例化,所以也不存在可实例化的父类对象在程序里。也就不存在子类替换父类实例(根本不存在父类实例了)时逻辑不一致的可能。
- 不符合LSP的最常见的情况是,父类和子类都是可实例化的非抽象类,且父类的方法被子类重新定义,这一类的实现继承会造成父类和子类间的强耦合,也就是实际上并不相关的属性和方法牵强附会在一起,不利于程序扩展和维护。
- 如何符合LSP?总结一句话 ——就是尽量不要从可实例化的父类中继承,而是要使用基于抽象类和接口的继承。
5. 开闭原则
1. 基本介绍
- 开闭原则是编程中最基础、最重要的设计原则
- 一个软件实体如类,模块和函数应该对扩展开放(提供方),对修改关闭(使用方)。也就是说尽量在不修改原有代码的基础上实现扩展。用抽象构建框架,用实现拓展细节
- 当软件需要变化时,尽量通过拓展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
- 编程中遵循其他原则,以及使用设计模式的目的就是遵循开闭原则
2. 代码说明
//1. 违反开闭原则的代码
//基类
class Shape{
int type;
}
//用于绘图的类【使用方】
class GraphEditor{
//接受Shape对象,然后根据对象的type来绘制不同的图形
public void drawShape(Shape s){
if(s.type == 1) drawRec(s);
else if(s.type == 2) drawCir(s);
}
//绘制矩形
public void drawRec(Shape s){
System.out.println("绘制矩形");
}
//绘制圆形
public void drawCir(Shape s){
System.out.println("绘制圆形");
}
}
//矩形类【提供方】
class Rec extends Shape{
Rec(){
super.type = 1;
}
}
//圆形类【提供方】
class Cir extends Shape{
Cir(){
super.type = 2;
}
}
//假如我们现在需要实现绘制三角形,通过上面的代码1,需要:
//1.添加三角形类
//2.修改GraphEditor,添加绘制三角形方法和修改drawShape,很明显,修改了使用方,违背了ocp原则
//2. 修改上面代码使之遵循ocp原则
//基类
abstract class Shape{
int type;
public abstract void draw();
}
//用于绘图的类【使用方】
class GraphEditor{
//接受Shape对象,根据不同的对象调用其draw方法
public void drawShape(Shape s){
s.draw();
}
}
//矩形类
class Rec extends Shape{
Rec(){
super.type = 1;
}
public void draw(){
System.out.println("绘制矩形")
}
}
//圆形类
class Cir extends Shape{
Cir(){
super.type = 2;
}
public void draw(){
System.out.println("绘制圆形")
}
}
//很明显,上面代码2如果想增加绘制三角形功能,只需要添加三角形类,即只需要拓展提供方
//不需要修改GraphEditor类,即不需要修改使用方,符合ocp原则
6. 迪米特法则
1. 基本介绍
- 一个对象应该对其他对象保持最少的了解
- 类与类关系越密切,耦合度越大
- 迪米特法则又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多复杂,都尽量将逻辑封装在类的内部。对外除了提供的public方法,不对外泄露任何信息。
- 迪米特法则还有个更简单的定义:只与直接的朋友通信
- 直接的朋友:每个对象都会与其他对象有耦合关系,只要俩个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友,也就是说,陌生的类最好不要以局部变量的形式出现在当前类中
2. 代码说明
//学校总部员工类
class Employee{
private int id;
public void setId(int id){
this.id = id;
}
public int getId(){
return this.id;
}
}
//学院的员工类
class CollegeEmployee{
private int id;
public void setId(int id){
this.id = id;
}
public int getId(){
return this.id;
}
}
//管理学院员工的管理类
class CollegeManager{
//返回学院所有员工的
public List<CollegeEmployee> getAll(){
List<CollegeEmployee> res = new ArrayList<>();
for(int i = 0; i < 10; i++){
CollegeEmployee c = new CollegeEmployee();
c.setId(i);
res.add(c);
}
return res;
}
}
//学校管理类
class SchoolManager{
//返回学校总部的所有员工
public List<Employee> getAll(){
List<Employee> res = new ArrayList<>();
for(int i = 0; i < 10; i++){
Employee c = new Employee();
c.setId(i);
res.add(c);
}
return res;
}
//打印
public void print(CollegeManager cm){
//打印学院员工
List<CollegeEmployee> list1 = cm.getAll();
//打印逻辑
//打印学校总部所有员工
List<Employee> list2 = this.getAll();
//打印逻辑
}
}
//分析:SchoolManager类的直接朋友类有Employee,CollegeManager
//而CollegeEmployee对SchoolManager来说是陌生类,因为它是以局部变量的形式出现在SchoolManager中的
//违反了迪米特法则
//改进:将不是直接朋友的CollegeEmployee放到CollegeManager中。
//即List<CollegeEmployee> list1 = cm.getAll();这个逻辑封装到CollegeManager类中
//管理学院员工的管理类
class CollegeManager{
//返回学院所有员工的id
public List<CollegeEmployee> getAll(){
List<CollegeEmployee> res = new ArrayList<>();
for(int i = 0; i < 10; i++){
CollegeEmployee c = new CollegeEmployee();
c.setId(i);
res.add(c);
}
return res;
}
//打印学院员工信息
public void print(){
List<CollegeEmployee> res = getAll();
//打印逻辑
}
}
//然后在学校管理类中打印方法调用CollegeManager的打印方法即可
//学校管理类
class SchoolManager{
//返回学校总部的所有员工
public List<Employee> getAll(){
List<Employee> res = new ArrayList<>();
for(int i = 0; i < 10; i++){
Employee c = new Employee();
c.setId(i);
res.add(c);
}
return res;
}
//打印
public void print(CollegeManager cm){
//打印学院员工
cm.print();
//打印学校总部所有员工
List<Employee> list2 = this.getAll();
//打印逻辑
}
}
3. 米特法则补充
- 迪米特法则的核心是降低类之间的耦合度
- 但是注意:由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系
7. 合成复用原则
尽量使用组合、聚合和依赖关系,少用继承关系
8. 七大设计原则总结
- 找出应用中可能需要变化之处,将它们独立出来,不要和那些不需要的变化的代码混在一起
- 针对接口编程,而不是针对实现编程
- 为了交互对象之间的低耦合设计而努力