设计模式七大原则

设计模式七大原则

设计模式原则,其实是程序员编程时,应当遵守的原则,也是各个设计模式设计的理论依据。

可以这么说,设计模式原则是我们的目标,而设计模式是我们的做法。

1. 开闭原则(ocp)

定义

开闭原则要求,程序应该对扩展开发,对修改关闭。即当需求发生变更的时候,可以不改动已有代码,而是增加新的代码以满足新的需求。

作用

开闭原则是面向对象设计的最终目标,它使软件具有一定的适应性和灵活性的同时具备稳定性和延续性。

作用如下:

  1. 便于测试。对于测试来说,只需要对扩展的代码进行测试即可,因为原有的测试代码仍然可以正常运行。
  2. 提高代码的复用性。 粒度越小,被复用的可能性就越大;在面向对象的程序设计中,根据原子和抽象编程可以提高代码的可复用性。
  3. 提高软件的可维护性。遵守开闭原则的软件,其稳定性和延续性都更强,从而易于扩展和维护。

开闭原则的实现方式

可以通过"抽象约束、封装变化"来实现,即通过接口或者抽象类为软件实体定义一个相对稳定的抽象层,而将相同的可变的因素封装在具体实现类中。

因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。对于易变的细节可以从抽象派生出来的实现类进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

上面俩段话,第一次接触可能有点懵,但相信随着设计模式的不断学习,你在回过头来看这句话一定会深有体会。

案例

方案一

现要实现一个画图功能,类图如下:
在这里插入图片描述

public class Shap {
    int type;
}

class Retangle extends Shap{
    public Retangle() {
        this.type = 1;
    }
}

class Circle extends Shap{
    public Circle() {
        this.type = 2;
    }
}

public class GraphicEditor {
    public void draw(Shap shap){
        if (shap.type == 1){
            System.out.println("绘制矩形");
        }else if (shap.type == 2){
            System.out.println("绘制圆形");
        }
    }
}

// 客户端测试
public class Client {
    public static void main(String[] args) {
        GraphicEditor editor = new GraphicEditor();
        editor.draw(new Retangle());
        editor.draw(new Circle());
    }
}

在上面的代码中,虽然我们实现目标功能,但是存在什么问题呢?
假设,现在需求改变了,增加了一个三角形。

class Triangle extends Shap{
    public Triangle() {
        this.type = 3;
    }
}

这时候,我们会发现我们原本的GraphicEditor无法满足我们的新功能了,因此需要改动代码:

public class GraphicEditor {
    public void draw(Shap shap){
        if (shap.type == 1){
            System.out.println("绘制矩形");
        }else if (shap.type == 2){
            System.out.println("绘制圆形");
        }else if (shap.type == 3){
            System.out.println("绘制三角形");
        }
    }
}

发现了没有,由于我们需求的增加,导致我们原本的代码发生了变动,违反了开闭原则,不利于功能的扩展。

方案二

那么如何进行改进? 通过上面的分析,我们发生GraphicEditor类中draw方法的代码是属于可变因素,根据单一原则的实现方式,我们可以将这可变因素封装在具体的实现类中,即由实现类自己实现绘制功能。所以我们在Shap抽象一个draw方法,让具体的实现类自己去实现。

类图如下:
在这里插入图片描述
注意观察,与方案一相比,GraphicEditor并没有依赖具体的实现类,而是依赖了他们的抽象

改进后的代码如下:

public abstract class Shap {
    public abstract void draw();
}

class Retangle extends Shap {
    @Override
    public void draw() {
        System.out.println("绘制矩形");
    }
}

class Circle extends Shap {
    @Override
    public void draw() {
        System.out.println("绘制原型");
    }
}

public class GraphicEditor {
    public void draw(Shap shap){
    	//由具体的实现类自己实现
        shap.draw();
    }
}

public class Client {
    public static void main(String[] args) {
        GraphicEditor editor = new GraphicEditor();
        editor.draw(new Retangle());
        editor.draw(new Circle());
    }
}

这时候,如果我们新增了一个绘制三角形的需求,我们只需要添加一个三角形的类:

class Triangle extends Shap {
    @Override
    public void draw() {
        System.out.println("绘制三角形");
    }
}

即可,并不需要修改原先GraphicEditor类中的代码,符合了开闭原则!!!

2. 单一职责原则

什么是单一职责原则

对于一个类来说,一个类应该只负责一项职责。

案例

public class SingleResponsibility {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.run("摩托车");
        vehicle.run("汽车");
        vehicle.run("飞机");
    }
}

class Vehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + " 在公路上运行的....");
    }
}

运行代码后,我们会发现摩托车和汽车是在公路上运行的没错,但飞机也在公路上运行,这就不符合常理了。原因是run方法既负责了公路的交通工具,又负责了天空的交通工具,不符合单一原则。

为了符合单一原则,我们可以将Vehicle拆分成RoadVehicle和AirVehicle,在类级别上保持单一原则。

类级别单一原则

public class SingleResponsibility {
    public static void main(String[] args) {
        RoadVehicle roadVehicle = new RoadVehicle();
        roadVehicle.run("摩托车");
        roadVehicle.run("汽车");

        AirVehicle airVehicle = new AirVehicle();
        airVehicle.run("飞机");
    }
}

class RoadVehicle{
    public void run(String vehicle) {
        System.out.println(vehicle + " .在公路上跑的...");
    }
}

class AirVehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + " 在天上飞的....");
    }
}

这样的话就遵守了单一原则,但是你可以看到,我们做了很大的代码改动。既将类进行了拆分又修改了客户端代码。

除了类级别上保持单一原则,其实我们还可以在方法级别上保持单一原则。

方法级别单一原则

public class SingleResponsibility1 {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.runRoad("摩托车");
        vehicle.runRoad("汽车");
        vehicle.runAir("飞机");
    }
}

class Vehicle {
	//方法级别上遵守单一原则
    public void runRoad(String vehicle) {
        System.out.println(vehicle + " 在公路上运行....");
    }

	//方法级别上遵守单一原则
    public void runAir(String vehicle) {
        System.out.println(vehicle + " 在天上运行....");
    }
}

这样做呢,虽然需要修改客户端代码,但是省去了拆分类的步骤,会相对简单一点。

类级别和方法级别单一原则如何选择

通常情况下,我们应当遵守单一职责原则,只有逻辑足够简单,才可以在代码级别违反单一职责;只有类方法数量足够少,才可以在方法级别保持单一职责。

单一职责原则的好处

  1. 降低类的复杂度,一个类只负责一项职责。
  2. 类的复杂度降低了,可读性和可维护性自然也就上升了。
  3. 可读性和可维护性上升了,发生风险的可能性也就降低了。

3. 接口隔离原则

接口隔离原则定义

客户端不应该依赖它不需要的接口,即一个类对另一个类的依赖应该建立在最小接口上。

接口隔离原则的实现方式

  • 接口尽量小,但是要有限度。一个接口只服务一个子模块或业务逻辑
  • 为了依赖接口的类定制服务。只提供调用者需要的方法,屏蔽不需要的方法
  • 了解环境,拒绝盲从。每个项目或产品都有选定的环境因素,环境不同,接口拆分的标准就不同深入了解业务逻辑。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情

案例

【例1】学生成绩管理程序。

分析:学生成绩管理程序一般包含插入成绩、删除成绩、修改成绩、计算总分、计算均分、打印成绩信息、査询成绩信息等功能,如果将这些功能全部放到一个接口中显然不太合理,正确的做法是将它们分别放在输入模块、统计模块和打印模块等 3 个模块中,其类图如图 1 所示。

在这里插入图片描述
程序代码如下:

public class Client {
    public static void main(String[] args) {
        InputModule inputModule = StuScoreList.getInputModule();
        CountModule countModule = StuScoreList.getCountModule();
        PrintModule printModule = StuScoreList.getPrintModule();

        inputModule.insert();
        countModule.countAverage();
        printModule.printStuInfo();
    }
}

public interface CountModule {
    void countTotalScore();
    void countAverage();
}

public interface InputModule {
    void insert();
    void delete();
    void modify();
}

public interface PrintModule {
    void printStuInfo();
    void queryStuInfo();
}

public class StuScoreList implements InputModule,CountModule,PrintModule{


    private StuScoreList(){}

    public static InputModule getInputModule(){
        return new StuScoreList();
    }

    public static CountModule getCountModule()
    {
        return new StuScoreList();
    }

    public static PrintModule getPrintModule()
    {
        return new StuScoreList();
    }

    @Override
    public void countTotalScore() {
        System.out.println("统计模块countTotalScore被调用");
    }

    @Override
    public void countAverage() {
        System.out.println("统计模块countAverage被调用");
    }

    @Override
    public void insert() {
        System.out.println("输入模块insert被调用");
    }

    @Override
    public void delete() {
        System.out.println("输入模块delete被调用");
    }

    @Override
    public void modify() {
        System.out.println("输入模块modify被调用");
    }

    @Override
    public void printStuInfo() {
        System.out.println("打印模块printStuInfo被调用");
    }

    @Override
    public void queryStuInfo() {
        System.out.println("打印模块queryStuInfo被调用");
    }
}

程序运行结果如下:

输入模块insert被调用
统计模块countAverage被调用
打印模块printStuInfo被调用

4. 依赖倒置原则

定义

1、高层模块不应该依赖低层模块,二者都应该依赖其抽象
2、抽象不应该依赖细节,细节应该依赖抽象

其实依赖倒置的中心思想就是面向接口编程

依赖倒置基于这样的涉及理想:相对于细节的多变性,抽象的东西要稳定得多。以抽象为基础搭建的架构比以细节为基础的架构要稳定得多。(在JAVA中,抽象指的使接口或者抽象类,细节就是具体的实现类)。

看到这些概念,你可能会有点懵逼,没事的,这确实是个抽象的东西,先有个印象,继续往下看,相信看完你就会有较清晰的认识了。

应用示例

编写一个Person类,完成接受消息的功能。

按照我们正常思维去实现
public class DependecyInversion {
    public static void main(String[] args) {
        Person person = new Person();
        person.receive(new Email());
    }
}

class Person{
    public void receive(Email email) {
        System.out.println(email.getInfo());
    }
}

class Email {
    public String getInfo() {
        return "电子邮件信息: hello,world";
    }
}

将上述代码转换成类图,如下:
在这里插入图片描述

这么做确实完成了我们的需求,但是有什么问题呢?
如果Person类不止能接受短信,还能接受微信信息,我们增加一个微信的同时,我们是不是也需要为Person类增加新的接受方法?

那怎么解决这个问题呢?下面让我们使用依赖倒置的原则来实现这个功能。

根据依赖倒置原则修改类图:
在这里插入图片描述

//定义接口
interface IReceiver { 
    public String getInfo();
}

class Email implements IReceiver { 
    public String getInfo() {
		return "电子邮件信息: hello,world";
	}
}

//增加微信
class WeiXin implements IReceiver { 
    public String getInfo() {
		return "微信信息: hello,ok";
	}
}

//方式 2
class Person {
	//这里我们是对接口的依赖
	public void receive(IReceiver receiver ) { 
        System.out.println(receiver.getInfo());
	}
}

public class DependecyInversion {
    public static void main(String[] args) {
        Person person = new Person();
        person.receive(new Email());
    }
}

发生这个时候,当我们添加一个新的功能时,只需要添加一个IReceiver的实现类即可,Person类的代码并不需要修改。这样代码的扩展性和维护性就会很好。

那依赖倒置是怎么体现的呢?我们将上面俩种方案的类图放在一起看,我们就会更清楚了。
在这里插入图片描述
我们可以直观的看到,相比方案二,底层类Person并没有直接依赖实现,而是依赖了抽象IReceive,而细节也只依赖了抽象。

这时候在回过头看依赖倒置的定义,相信会更加清晰的。

遵从依赖倒置原则实现

几个指导方针帮助你遵循此原则

  • 变量不可以持有具体对象的引用。如果使用new,就会持有具体类的引用。可以通过工厂模式避免
  • 不要让类派生自具体类。如果派生具体类,就会依赖一个具体的类。请派生自一个抽象(接口或抽象类)
  • 不要覆盖基类中已实现的方法

但是,请注意,我们应该尽量达到这个原则,并不是随时都要遵守这个原则。如果说有一个不像是会改变的类,那么在代码中直接实例化具体的类也没有什么大碍。比如我们平时程序中,经常不假思索的实例化了字符串对象,很明显违反了这条原则,但是我们依旧可以这么做,因为字符串不可能改变。

可以去读读Head First设计模式中工厂模式部分,里面用实例讲解了这个原则,相信你会有所收获的

5. 里氏替换原则

定义

里氏替换原则是由里斯科夫女士提出:继承必须确保超类所拥有的性质在子类中仍然成立。
换句话说,里氏替换原则规定:子类可以扩展类的功能,但不能改变父类原有的功能。即子类继承父类时,除了添加新的方法完成新增功能外,尽量不要重写父类的方法。

案例

里氏替换原则告诉了我们什么

里氏替换原则其实阐述的是什么时候应该使用继承,什么时候不应该使用继承,以及其中蕴含的道理。

在实际编程中,我们常常通过会通过重写父类的方法完成新的功能,这样写起来虽然简单,但整个继承体系的复杂性会提升,复用性会比较差。特别是运行多态比较频繁的时候。

继承实际上是让俩个类的耦合性增强了,在适当的情况下,可以通过将父类和子类继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合或组合的关系替代。

里氏替换原则的作用

  1. 里氏替换原则是实现开闭原则的重要方式之一
  2. 它克服了继承中重写父类造成的可复用性变差的缺点
  3. 它是动作正确性的保证。即类的扩展不会给已有的系统引新的错误,降低代码出错的可能性
  4. 增加程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性,可扩展性,降低需求变更引发的风险

6. 迪米特法则

定义

迪米特法则也叫最少知道原则。

迪米特法则规定:只与直接朋友交谈,不跟陌生人说话。其含义是:如果俩个软件实体无须直接通信,那么不应当发生直接互相调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的独立性。

这里出现了个直接朋友,那什么是直接朋友呢?
每个对象都会与其他对象有耦合关系,只要俩个对象之间有耦合关系,我们就说这俩个对象之间是朋友关系。耦合的方式有很多,依赖、关联、聚合等。其中,我们称成员变量、方法参数、方法返回值中的类为直接朋友,而出现在局部变量中的类不是直接朋友。也就是说陌生的人最好不要以局部变量的形式出现在类的内部。

虽然对迪米特法则还不是很理解,但是这种通第三方转发而达到解耦效果的,我们应该都会想到消息中间件吧o( ̄▽ ̄)o

案例

明星与经纪人的关系实例。

分析:明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如与粉丝的见面会,与媒体公司的业务洽淡等。这里的经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则,其类图如下:
在这里插入图片描述
程序代码如下:

@AllArgsConstructor
@Getter
public class Star {
    private String name;
}

@AllArgsConstructor
@Getter
public class Fans {
    private String name;
}

@AllArgsConstructor
@Getter
public class Company {
    private String name;
}

@Setter
public class Agent {
    private Star star;
    private Fans myFans;
    private Company myCompany;

    public void metting(){
        System.out.println(myFans.getName()+"与"+star.getName()+"见面了");
    }

    public void business(){
        System.out.println(myCompany.getName()+"与明星"+star.getName()+"洽谈业务");
    }
}

public class Clent {
    public static void main(String[] args) {
        Agent agent = new Agent();
        agent.setMyCompany(new Company("小米"));
        agent.setMyFans(new Fans("小王"));
        agent.setStar(new Star("王源"));
        agent.business();
        agent.metting();
    }
}

程序运行结果如下:

小米与明星王源洽谈业务
小王与王源见面了

7. 合成复用原则

定义

合成复合要求软件在复用时,尽量先使用组合和聚合等关联关系来实现,其次才考虑使用继承关系来实现。因为继承关系会使俩个类的耦合性增强。

如果使用继承关系,则必须严格遵守里氏替换原则。合成复用原则同里氏替换原则相辅相成,俩者都是开闭原则的具体实现规范。

合成服用原则的重要性

通常类的复用非为继承复用和合成复用俩种,继承复用虽然简单易实现,但它存在一下缺点:

  1. 继承复用破环了类的封装性。因为继承会将类的实现细节暴露给子类,父类对子类是透明的,所以这种服用又称为"白箱复用"
  2. 子类与父类的耦合度高。父类实现的任何改变都会导致子类的实现发生变化,不利于类的扩展与维护
  3. 限制服用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以运行时不可能发生变化。

采用组合或聚合服用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,拥有如下优点:

  1. 他维持了类的封装性,因为成分对象的内部细节是新对象不可见的,所以这种复用又称为"黑箱复用"
  2. 新旧对象之间的耦合度低
  3. 复用的灵活度高。这种复用可以在运行时动态进行,新对象可以动态的引入与成分对象类型相同的对象。

案例

复用原则通过将已有对象纳入新对象中,新对象通过调用已有对象的功能,从而达到复用。

下面已汽车分类管理程序为例介绍合成复用原则的应用

分析:汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。如下图 所示是用继承关系实现的汽车分类的类图。
在这里插入图片描述
从上图可以看出用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。但如果改用组合关系实现就能很好地解决以上问题,其类图如下所示。
在这里插入图片描述

博客中有多处引自设计模式,因为我觉得他写得就很好了,所以就引用了过来。 O(∩_∩)O。但是我并不是CV大法,而是手敲了一下,又加深了点印象(。・∀・)ノ

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值