美文网首页
Java日记之设计模式初探

Java日记之设计模式初探

作者: 居居居居居居x | 来源:发表于2019-10-25 21:35 被阅读0次

前言

设计模式是在开发中必须要掌握的一种技能,包括各个项目的结构设计以及一些框架源码的解读,里面都包含有设计模式,设计模式也是面试中经常问到,从这篇文章开始记录全部23种设计模式的解析,23种设计模式根据准则来分类总共分为三大类,创建型、结构形以及行为型。此篇文章的大部分源代码出自于Java设计模式精讲 Debug方式+内存分析,并结合我自己的理解,已获得课程老师同意。


感谢geely老师

设计模式的七大原则

在讲设计模式之前,首先就要讲设计模式的七大原则:

  1. 开闭原则(open closed principle)
  2. 依赖倒置原则(dependence inversion principle)
  3. 单一职责原则(Single Responsibility Principle)
  4. 接口隔离原则(interface segregation principle)
  5. 迪米特原则(law of demeter LOD)
  6. 里氏替换原则(LSP liskov substitution principle)
  7. 合成复用原则(Composite Reuse Principle)

1. 开闭原则(open closed principle)

  • 定义:一个软件的实体比如类、模块和函数应该对扩展开放,修改和关闭,所谓的开闭也正是对扩展和修改这两个行为的原则,强调的是用抽象构建框架,用实现来扩展细节,它是面向设计模式中最基础的原则,它知道我们如何建立稳定灵活的系统,例如版本更新,尽量不修改源代码,但是可以增加新功能,实现开闭原则的思想就是面向抽象编程。

  • 优点: 提高软件的可复用性及可维护性。

代码举例:

public interface ICourse {
    Integer getId();
    String getName();
    Double getPrice();
}

//实现类
public class JavaCourse implements ICourse{
    private Integer Id;
    private String name;
    private Double price;

    public JavaCourse(Integer id, String name, Double price) {
        this.Id = id;
        this.name = name;
        this.price = price;
    }

    public Integer getId() {
        return this.Id;
    }

    public String getName() {
        return this.name;
    }

    public Double getPrice() {
        return this.price;
    }

}

首先创建一个接口和它的实现类,这时候假如说要对JavaCourse这个类进行拓展,但是不修改原来的代码,该怎么做呢?很简单,继承这个类,并进行拓展就行,以下的例子:

//拓展类
public class JavaDiscountCourse extends JavaCourse {

    public JavaDiscountCourse(Integer id, String name, Double price) {
        super(id, name, price);
    }

    public Double getDiscountPrice(){
        return super.getPrice()*0.8;
    }

}

//测试类
public class Test {
    public static void main(String[] args) {
        ICourse iCourse = new JavaDiscountCourse(96, "Java从零到企业级电商开发", 348d);
        JavaDiscountCourse javaCourse = (JavaDiscountCourse) iCourse;
        System.out.println("课程ID:" + javaCourse.getId() + " 课程名称:" + javaCourse.getName() 
        + " 课程原价:" + javaCourse.getPrice() + " 课程折后价格:" + javaCourse.getDiscountPrice() + "元");

    }
}
开闭原则代码运行结果

从代码就可以看出来了,这就很符合开闭原则,我们只对类进行拓展和修改,如果是修改方法的话,通过接口去进行抽象出来,然后去实现就可以,这就是面向接口编程。

2.依赖倒置原则(dependence inversion principle)

  • 定义:高层模块不应该依赖底层模块,两者都应该依赖其抽象。意思就是抽象不应该依赖细节,细节应该依赖抽象,还是最主要的,就是针对接口来进行编程,不要针对实现来进行编程。 通过抽象可以使各个类或者模块的实现彼此独立互不影响,从而实现模块之间的松耦合,降低模块的耦合性。使用这个原则也有一些注意的点,就是每个类都尽量继承接口或者抽象类。

  • 优点:可以减少类间的耦合,提高系统的而稳定性,提高代码的可读性和可维护性,也降低了修改程序所造成的的风险。

代码举例:

定义接口
public interface ICourse {
    void studyCourse();
}

实现类
public class JavaCourse implements ICourse {

    @Override
    public void studyCourse() {
        System.out.println("学习Java课程");
    }
}

public class PythonCourse implements ICourse {

    @Override
    public void studyCourse() {
        System.out.println("学习Python课程");
    }
}

首先定义接口和它的实现类,接着我们面向接口编程呢,我只需要要创建一个传递接口实现的类就好了。

public class Ju {

    public void setiCourse(ICourse iCourse) {
        this.iCourse = iCourse;
    }

    private ICourse iCourse;



    public void studyImoocCourse(){
        iCourse.studyCourse();
    }

    
}

//测试类实现
public class Test {

    public static void main(String[] args) {
        Ju ju = new Ju();
        ju.setiCourse(new JavaCourse());
        ju.studyImoocCourse();

        ju.setiCourse(new FECourse());
        ju.studyImoocCourse();

    }
}
依赖倒置原则代码运行结果
从代码就可以看出来,这就是面向接口编程,你需要实现接口什么样的方法,就只要把实现这个接口的类实例化就可以轻松的进行调用,这样就降低了耦合度,也就不用如果需要替换类,就需要从核心逻辑上替换实现的类了,也提高了代码的可维护性。

3.单一职责原则(Single Responsibility Principle)

  • 定义:不要存在多于一个导致类变更的原因。假设我们有一个类负责两个职责, 但是一旦有其中一个需求变更,就要修改里面的代码,但是修改的话有可能就导致原版运行正常的另一个职责发生问题。也就是说这个类有两个职责,对于这样子的类的话,我们就需要分别建立两个类,分别负责职责,这样就不会发生故障或者变更。一个类/接口/方法只负责一项原则。

  • 优点:降低类的复杂度,提高类的可读性,提高系统的可维护性和降低变更引起的风险。

  • 缺点:有可能类会非常的多,可读性就差。
    代码举例:

private void updateUserInfo(String userName,String address,boolean bool){
    if(bool){
        //todo something1
    }else{
        //todo something2
    }
}

比如说一个有新的业务需求(如上代码),你可能就需要在代码上面直接更改,这样子如此长期改下去可能会发生问题。我们就推荐各创建一个类并负责各自的职责。

//各自类的实现
public class FlyBird {
    public void mainMoveMode(String birdName){
        System.out.println(birdName+"用翅膀飞");
    }
}

public class WalkBird {
    public void mainMoveMode(String birdName){
        System.out.println(birdName+"用脚走");
    }
}

//测试类
public class Test {
    public static void main(String[] args) {
        FlyBird flyBird = new FlyBird();
        flyBird.mainMoveMode("大雁");

        WalkBird walkBird = new WalkBird();
        walkBird.mainMoveMode("鸵鸟");

    }
}

一些公共的方法可以使用抽象类来进行实现,不同的业务,就创建各自的类来进行负责其职责就好。

4.接口隔离原则(interface segregation principle)

  • 定义:用多个专门的接口,而不使用单一的接口,客户端不应该依赖它不需要的接口。一个类对一个类的依赖应该建立在最小的接口上。建立单一接口,不要建立庞大臃肿的接口,尽量细化接口,接口的方法,尽量少,太少也不行,就是一定要适度。

  • 优点:符合我们常说的高内聚低耦合的设计思想,从而使得类具有很好的可读性,可扩展性和可维护性。

代码举例:

//接口定义
public interface IAnimalAction {
    void eat();
    void fly();
    void swim();

}

public interface IEatAnimalAction {
    void eat();
}

public interface IFlyAnimalAction {
    void fly();
}

public interface ISwimAnimalAction {
    void swim();
}

//实现类
public class Bird implements IAnimalAction {
    @Override
    public void eat() {

    }

    @Override
    public void fly() {

    }

    @Override
    public void swim() {

    }
}

public class Dog implements ISwimAnimalAction,IEatAnimalAction {

    @Override
    public void eat() {

    }

    @Override
    public void swim() {

    }
}

这就是一个接口隔离原则的设计了,因为比较简单所有没什么好说的,就是通过一个接口来对应一个类以及使用多继承。

5.迪米特原则(law of demeter LOD)

  • 定义:一个对象应该对其他对象保持最少的了解,又叫最少知道原则,尽量降低类的耦合,就是尽量不要对外公开过多的方法和变量,多使用private权限来约束。

  • 优点:降低类之间的耦合。

代码举例:
比如Boss说有一个需求就是查询现在有多少门线上课程,这就关系了三个类,TeamLeader、Course和Boss。

public class Boss {

    public void commandCheckNumber(TeamLeader teamLeader){
        teamLeader.checkNumberOfCourses();
    }

}

public class Course {
}

public class TeamLeader {
    public void checkNumberOfCourses(){
        List<Course> courseList = new ArrayList<Course>();
        //模拟查询课程逻辑
        for(int i = 0 ;i < 20;i++){
            courseList.add(new Course());
        }
        System.out.println("在线课程的数量是:"+courseList.size());
    }

}

//测试类
public class Test {
    public static void main(String[] args) {
        Boss boss = new Boss();
        TeamLeader teamLeader = new TeamLeader();
        boss.commandCheckNumber(teamLeader);

    }
}
迪米特原则代码运行结果

从这段代码我们看到Course是有TeamLeader来生成的,Course是不和Boss发生接触的,Boss也不需要知道Course,这就是迪米特原则,Boss不需要关心那么多细节,只要知道结果就行。

6. 里氏替换原则(LSP liskov substitution principle)

  • 定义:如果对每个类型为T1的对象o1,都有类型为T2的对象o2,使得T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。这个原则相比其他几个是比较难理解的,简单来说就是T1这个类生成了o1对象,T2生成了o2对象,我们现在写一个程序,里面使用T1的类型,同时创建了o1,当我们把所有程序中的o1都替换成T2生成的o2时,这时程序的行为没有发生变化,那么就可以认为T2是T1的子类型。有没有发现跟开闭原则有点类似,它其实就是开闭原则的补充。

  • 定义扩展:一个软件实体如果适用一个父类的话,那一定适用于其子类,所有引用父类的地方必须能透明的适用其子类的对象,子类对象能够替换父类对象,而程序逻辑不变。子类可以扩展父类的功能,但不能改变父类原有的功能。子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。子类可以增加自己特有的方法。当子类的方法重载父类的方法,方法的前置条件要比父类的输入参数更宽松。当子类的方法实现父类的方法时,方法的后置条件要比父类更加严格或者相等。

  • 优点:约束继承泛滥,开闭原则的一种体现。也加强了程序的健壮性,同时变更时也可以做到非常好的兼容性,提高程序的维护性,扩展性,降低需求变更时引入的风险。

代码举例:
举个例子说明下继承的风险(以下例子来源其他博客,侵删)

public class Father {
    public int subtraction(int a, int b) {
        return a - b;
    }

}

public class Test {

    public static void main(String[] args) {
        Father father = new Father();
        System.out.println("100-50=" + father.subtraction(100, 50));
        System.out.println("100-80=" + father.subtraction(100, 80));
    }
}
里氏替换原则代码运行结果1

上面就是一个很简单减法运算,现在我们需要增加一个新的功能,完成两数相加,然后再与100求和,由类Sun来负责。

public class Sun extends Father {

    @Override
    public int subtraction(int a, int b) {
        return a + b;
    }

    public int addition(int a, int b) {
        //重写后subtraction(a,b)改为a+b
        return subtraction(a, b) + 100;
    }

}

public class Test {

    public static void main(String[] args) {
        Sun sun = new Sun();
        System.out.println("100-50="+sun.subtraction(100, 50));
        System.out.println("100-80="+sun.subtraction(100, 80));
        System.out.println("100+20+100="+sun.addition(100, 20));
    }
}
里氏替换原则代码运行结果2

我们发现原本运行正常的相减功能发生了错误。原因就是类Sun在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类Sun重写后的方法,造成原本运行正常的功能出现了错误。在本例中,引用基类Father完成的功能,换成子类Sun之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。如果非要重写父类的方法,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用依赖、聚合,组合等关系代替。

7. 合成复用原则(Composite Reuse Principle)

  • 定义:尽量使用对象组合/聚合,而不是使用继承关系来达到软件复用的目的。

  • 优点:可以使系统更加的灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少。

代码举例:

//定义接口
public abstract class DBConnection {

    public abstract String getConnection();
}

//实现接口类
public class MySQLConnection extends DBConnection {
    @Override
    public String getConnection() {
        return "MySQL数据库连接";
    }
}

public class PostgreSQLConnection extends DBConnection {
    @Override
    public String getConnection() {
        return "PostgreSQL数据库连接";
    }
}
public class ProductDao{

    private DBConnection dbConnection;

    public void setDbConnection(DBConnection dbConnection) {
        this.dbConnection = dbConnection;
    }

    public void addProduct(){
        String conn = dbConnection.getConnection();
        System.out.println("使用"+conn+"增加产品");
    }
}

public class Test {
    public static void main(String[] args) {
        ProductDao productDao = new ProductDao();
        productDao.setDbConnection(new PostgreSQLConnection());
        productDao.addProduct();
    }
}

从代码就可以看出,通过ProductDao类注入你需要的对象,就能实现不同的业务逻辑,通过setDbConnection()就可以实现对象的组合和复用,当然也可以通过构造方法来进行,而以后我们如果要增加产品的话,只需要写一个和MySQLConnection类一样平级的类,继承DBConnection接口,具体的选择就可以额交给客户端就好了。

参考

相关文章

网友评论

      本文标题:Java日记之设计模式初探

      本文链接:https://www.haomeiwen.com/subject/ylexvctx.html