Loading

设计模式学习(一):设计原则

设计模式学习(一):设计原则

作者:Grey

原文地址:

博客园:设计模式学习(一):设计原则

CSDN:设计模式学习(一):设计原则

UML 类图关系

可以用下面图来表示类的各种关系

img

  • 聚合关系是非强依赖的,而组合关系是强依赖的。

  • 继承关系中,实现关系是继承的抽象类,泛化关系是继承的非抽象类。

  • 依赖关系是动态关系,而关联关系是静态关系。

开闭原则

对扩展开放,对修改关闭,降低维护带来的新风险

先看一个不符合开闭原则的代码,每次新增一种类型,都需要增加一个if条件判断(修改),如下代码

public class GraphicEditor {
    public void draw(Shape shape) {
        if (shape.mType == 1) {
            drawRectangle();
        } else if (shape.mType == 2) {
            drawCircle();
        }
        // ... 每次增加一种类型,这里的代码就要加一个if分支。不符合开闭原则
    }

    public void drawRectangle() {
        System.out.println("画长方形");
    }

    public void drawCircle() {
        System.out.println("画圆形");
    }

    class Shape {
        int mType;
    }

    class Rectangle extends Shape {
        Rectangle() {
            super.mType = 1;
        }
    }

    class Circle extends Shape {
        Circle() {
            super.mType = 2;
        }
    }
}

要调整为符合开闭原则的代码,需要先将 Shape 抽象为接口,GraphicEditor 中的 draw() 方法中直接传入 Shape 这个接口类型,并调用其 draw() 方法,每次增加一种 Shape 类型,只需要新增一个类,并实现 Shape 接口即可,这样就实现了对扩展开放,而且也不需要修改 GraphicEditor 中的 draw() 方法内容,实现了对修改关闭,修改后的代码如下。

public class GraphicEditor1 {
    public void draw(Shape shape) {
        shape.draw();
    }

    interface Shape {
        void draw();
    }

    class Rectangle implements Shape {

        @Override
        public void draw() {
            System.out.println("画矩形");
        }
    }

    class Circle implements Shape {

        @Override
        public void draw() {
            System.out.println("画圆形");
        }
    }
}

依赖倒置

高层不应该依赖低层,这样更利于代码结构的升级扩展

先看一段不符合依赖倒置的代码,driver方法参数里面写了具体的车的实现,如果以后要换一种车,只能修改代码实现。

public class CarTest {
    private static class Benz {
        public void run() {
            System.out.println("奔驰跑起来了!");
        }
    }

    private static class Driver {
        private String name;

        public Driver(String name) {
            this.name = name;
        }

        // driver方法参数里面写了具体的车的实现,如果以后要换一种车,只能修改代码实现。
        // 不符合依赖倒置原则
        public void driver(Benz benz) {
            benz.run();
        }
    }

    public static void main(String[] args) {
        Benz benz = new Benz();
        Driver driver = new Driver("张三");
        driver.driver(benz);
    }
}

调整后的代码如下,因为 driver() 方法的参数变成了抽象的 ICar 类型,所以,所有实现 ICar 类型的车都可以调用这个方法,这样就实现了高层不依赖低层。

public class CarTest1 {
    public static void main(String[] args) {
        IDriver driver = new Driver();
        driver.driver(new Benz());
        driver.driver(new BMW());
    }

    private interface ICar {
        void run();
    }

    public static class Benz implements ICar {
        public void run() {
            System.out.println("奔驰跑起来了!");
        }
    }

    public static class BMW implements ICar {
        public void run() {
            System.out.println("宝马跑起来了!");
        }
    }

    private interface IDriver {
        void driver(ICar car);
    }

    public static class Driver implements IDriver {

        @Override
        public void driver(ICar car) {
            car.run();
        }
    }
}

单一职责

一个类只干一件事 便于理解,提高代码的可读性

举个例子,一般来说,系统有登录,注册,注销的功能,可以写成如下形式

public interface UserOperate {
    void login(UserInfo userInfo);
    void register(UserInfo userInfo);
    void logout(UserInfo userInfo);
}

public class UserOperateImpl implements UserOperate{
    @Override
    public void login(UserInfo userInfo) {
        // 用户登录
    }

    @Override
    public void register(UserInfo userInfo) {
        // 用户注册
    }

    @Override
    public void logout(UserInfo userInfo) {
        // 用户登出
    }
}

以上设计针对各个方法都不是很复杂的情况,如果每个方法都比较复杂,可以考虑独立成类来做,示例如下

public interface Register {
    void register();
}

public interface Login {
    void login();
}

public interface Logout {
    void logout();
}


public class RegisterImpl implements Register{

    @Override
    public void register() {
        // 用户注册
    }
}

public class LoginImpl implements Login{
    @Override
    public void login() {
        // 用户登录
    }
}

public class LogoutImpl implements Logout{

    @Override
    public void logout() {
        // 登出
    }
}

这就实现了单一职责的原则。

接口隔离

一个接口只干一件事,功能解耦,高聚合,低耦合

举个例子,学生成绩管理程序一般包含查询成绩、新增成绩、删除成绩、修改成绩、计算总分、计算平均分、打印成绩信息等功能,如果都写在一个类里面,就会变成如下形式

public interface IStudentScore {
    // 查询成绩
    public void queryScore();

    // 修改成绩
    public void updateScore();

    // 添加成绩
    public void saveScore();

    // 删除成绩
    public void delete();

    // 计算总分
    public double sum();

    // 计算平均分
    public double avg();

    // 打印成绩单
    public void printScore();
}

这样的方式不利于扩展. 比如:

学生只有查看成绩,打印成绩单的权限, 没有增删改的权限;

老师拥有所有的权限。

采用接口隔离原则设计,可以做如下改进

public interface IQueryScore {
    // 查询成绩
    public void queryScore();

    // 打印成绩单
    public void printScore();
}

public interface IOperateScore {

    // 修改成绩
    public void updateScore();

    // 添加成绩
    public void saveScore();

    // 删除成绩
    public void delete();

    // 计算总分
    public double sum();

    // 计算平均分
    public double avg();
}

分为查询接口和操作接口,这样学生端就不需要重写和他不相关的接口了。

迪米特法则

不该知道的不要知道,减少代码臃肿

示例:一个人用咖啡机煮咖啡的过程,例子中只有两个类,一个是人,一个是咖啡机。

首先是咖啡机类 CoffeeMachine ,咖啡机制作咖啡只需要三个方法

第一步:加咖啡豆;

第二步:加水;

第三步:制作咖啡。

如果把咖啡机的所有方法暴露给人调用,就会出现如下代码

/**
 * 咖啡机抽象接口
 */
public interface ICoffeeMachine {

    //加咖啡豆
    void addCoffeeBean();

    //加水
    void addWater();

    //制作咖啡
    void makeCoffee();
}


/**
 * 咖啡机实现类
 */
public class CoffeeMachine implements ICoffeeMachine{

    //加咖啡豆
    public void addCoffeeBean() {
        System.out.println("放咖啡豆");
    }

    //加水
    public void addWater() {
        System.out.println("加水");
    }

    //制作咖啡
    public void makeCoffee() {
        System.out.println("制作咖啡");
    }
}


/**
 * 人, 制作咖啡
 */
public interface IMan {
    /**
     * 制作咖啡
     */
    void makeCoffee();
}

/**
 * 人制作咖啡
 */
public class Man implements IMan {
    private ICoffeeMachine coffeeMachine;

    public Man(ICoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;
    }

    /**
     * 制作咖啡
     */
    public void makeCoffee() {
        coffeeMachine.addWater();
        coffeeMachine.addCoffeeBean();
        coffeeMachine.makeCoffee();
    }
}

/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        ICoffeeMachine coffeeMachine = new CoffeeMachine();

        IMan man = new Man(coffeeMachine);
        man.makeCoffee();

    }
}

其实人根本不关心咖啡机具体制作咖啡的过程。所以我们可以作如下优化:

优化后的咖啡机类,只暴露一个 work() 方法,把制作咖啡的三个具体的方法addCoffeeBean()addWater()makeCoffee()设为私有,work()方法封装了这三个具体方法的实现。

/**
 * 咖啡机抽象接口
 */
public interface ICoffeeMachine {

    //咖啡机工作
    void work();

}

/**
 * 咖啡机实现类
 */
public class CoffeeMachine implements ICoffeeMachine {

    //加咖啡豆
    private void addCoffeeBean() {
        System.out.println("放咖啡豆");
    }

    //加水
    private void addWater() {
        System.out.println("加水");
    }

    //制作咖啡
    private void makeCoffee() {
        System.out.println("制作咖啡");
    }

    @Override
    public void work() {
        addCoffeeBean();
        addWater();
        makeCoffee();
    }
}

/**
 * 人, 制作咖啡
 */
public interface IMan {
    /**
     * 制作咖啡
     */
    void makeCoffee();
}


/**
 * 人制作咖啡
 */
public class Man implements IMan {
    private ICoffeeMachine coffeeMachine;

    public Man(ICoffeeMachine coffeeMachine) {
        this.coffeeMachine = coffeeMachine;
    }

    /**
     * 制作咖啡
     */
    public void makeCoffee() {
        coffeeMachine.work();
    }
}

/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        ICoffeeMachine coffeeMachine = new CoffeeMachine();

        IMan man = new Man(coffeeMachine);
        man.makeCoffee();

    }
}

通过减少 CoffeeMachine 对外暴露的方法,减少 Man 对 CoffeeMachine 的了解,从而降低了它们之间的耦合。

里氏替换原则

子类重写方法功能发生改变,不应该影响父类方法的含义,防止继承泛滥。

比如下述代码,就不符合里氏替换原则

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 求和,由类 B 来负责。即类 B 需要完成两个功能:

  1. 两数相减。

  2. 两数相加,然后再加100。

由于类 A 已经实现了第一个功能,所以类 B 继承类 A 后,只需要再完成第二个功能就可以了,代码如下:

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 b = 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));
 }
}

类B完成后,运行结果:

100-50=150
100-80=180
100+20+100=220

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

UML 和 代码

UML 图

代码

更多

设计模式学习专栏

参考资料

《设计模式-可复用面向对象软件的基础》读书笔记

设计模式六大原则(六)----开闭原则

设计模式六大原则(三)----依赖倒置原则

设计模式六大原则(一)----单一职责原则

设计模式六大原则(四)----接口隔离原则

设计模式六大原则(五)----迪米特法则

设计模式六大原则(二)----里式替换原则

posted @ 2022-01-07 16:34  Grey Zeng  阅读(394)  评论(1编辑  收藏  举报