设计模式二:七大设计原则(下)

余生长醉 提交于 2020-01-02 23:52:11

4.接口隔离原则

  • 接口隔离原则(Interface Segregation Principle,ISP)是指用多个专门的接口,而不是使用单一的总接口,客户端不要依赖自己不需要的接口。这个原则指导我们在设计时注意以下几点:
    • 1、一个类对另一个类的依赖应该建立在最小的接口上
    • 2、建立单一的接口,不要建立复杂臃肿的接口
    • 3、尽量细化接口,接口内方法功能尽量不要重复
  • 接口隔离原则符合我们常说的"高内聚、低耦合"的设计思想,从而提高了类的可扩展性、可维护性和可读性。所以我们在设计接口的时候,要多考虑业务模型和未来可能产生的情况,请看示例:
  • 定义一个动物的接口
public interface IAnimal {
    void eat();
    void swimming();
}

  • 定义一个GuangTouQiang,实现动物接口:
public class GuangTouQiang implements IAnimal {
    @Override
    public void eat() {

    }
    @Override
    public void swimming() {

    }
}
  • 定义一个XiongDa类,实现动物接口:
public class XiongDa implements IAnimal {
    @Override
    public void eat() {

    }
    @Override
    public void swimming() {

    }
}
  • 可以思考一下,光头强可是不会爬树的哦(管他会不会,说他不会他就不会{{{(>_<)}}}),那这个方法只能空着,冗余在这,这样的设计就太不合理了,我们不妨改下设计思路:分别设计IEatAnimal和ISwimmingAnimal两个接口,然后实现类分别实现需要的功能所在的接口就可以了:
  • IEatAnimal接口
public interface IEatAnimal {
    void eat();
}
  • ISwimmingAnimal接口
public interface ISwimmingAnimal {
    void swimming();
}

  • 光头强只需要实现IEatAnimal 接口就可以了:
public class GuangTouQiang implements IEatAnimal {
    @Override
    public void eat() {

    }
}
  • 熊大实现俩接口:
public class XiongDa implements IEatAnimal, ISwimmingAnimal {
    @Override
    public void eat() {

    }
    @Override
    public void swimming() {

    }
}

5.迪米特法则

  • 迪米特法则(Law of Demeter LoD)是指一个对象对其他的对象保持最少的了解,又叫最少知道原则(Least Knowledge Principle, LKP),尽量降低类与类之间的耦合。迪米特法则主要强调:只和朋友说话,不搭理陌生人。其中出现在成员变量、方法参数输入输出的类都可以称为朋友类,而出现在方法体内部的类都属于陌生人类。
  • 有一天,吉吉国王安排熊大和熊二去采果子,准备过冬,到完成的时候,熊大和熊二向吉吉国王报告采了多少果子:
  • 果子类:
public class Fruit {
}
  • 熊大熊二类:
public class XiongDaXiongEr {
    public void reportFruit(List<Fruit> fruitList) {
        System.out.println("哥俩今天采的果子数量是:" + fruitList.size());
    }
}
  • 吉吉国王类:
public class JiJiKing {
    public void checkFruit(XiongDaXiongEr brothers) {
        //一个一个地检查水果
        List<Fruit> fruitList = new ArrayList<Fruit>();
        for(int i = 0; i < 30; i++) {
            fruitList.add(new Fruit());
        }
        brothers.reportFruit(fruitList);
    }
}
  • 接下来看一下具体工作:
public class StartWork {
    public static void main(String[] args) {
        JiJiKing jiJiKing = new JiJiKing();
        XiongDaXiongEr brothers = new XiongDaXiongEr();
        jiJiKing.checkFruit(brothers);
    }
}
  • 这时,其实功能都已经实现,看着也没什么问题,根据迪米特法则,吉吉国王和果子类之间不需要产生直接的交流,而熊大熊二报告需要引用果子类,这时候只需要稍加改造就可以了:
  • 熊大熊二类:
public class XiongDaXiongEr {
    public void reportFruit() {
        List<Fruit> fruitList = new ArrayList<>();
        //一个一个地检查水果
        for(int i = 0; i < 30; i++) {
            fruitList.add(new Fruit());
        }
        System.out.println("哥俩今天采的果子数量是:" + fruitList.size());
    }
}
  • 吉吉国王类:
public class JiJiKing {
    public void checkFruit(XiongDaXiongEr brothers) {
        brothers.reportFruit();
    }
}

这样,吉吉国王和水果之间就没有直接的依赖关系了。

6.里氏替换原则

  • 里氏替换原则(Liskov Substitution Principle,LSP)是指如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1替换为o2时,程序P的行为没有变化,那么类型T2就是T1的子类型。好绕哦,其实简单点说就是:一个软件实体,如果适用父类的话,那么一定也适用其子类,所有引用父类的地方,必须透明地使用其子类的对象,子类可以替换父类的对象,而逻辑不变。
  • 可以总结以下几点:
    • 1、子类可以实现父类的抽象方法,而不能覆盖父类的非抽象方法
    • 2、子类中可以增加自己特有的方法
    • 3、当子类的方法重载父类的方法时,方法的前置条件(方法的输入/入参)要别比父类的输入参数更加宽松
    • 4、当子类的方法实现父类的方法时(重载/重写,或实现),方法的后置条件(输出/返回值)要比父类更加严格或相等
  • 所以在之前的开闭原则示例中,因为修改了重写方法,而违背了里氏替换原则,此时我们应该在子类GoodHopPeople中定义一个新的方法来完成新的任务。
  • 使用里氏替换原则有以下优点:
    • 1、约束继承泛滥,开闭原则的一种体现
    • 2、加强程序的健壮性,同时提高可维护性、扩展性和兼容性,降低了银需求变更带来的风险。
  • 还是用一个非常经典的场景来描述一下,那就是正方形、矩形和四边形,我们先创建一个长方形:
public class Rectangle {
    private Long height;
    private Long width;

    public Long getHeight() {
        return height;
    }

    public void setHeight(Long height) {
        this.height = height;
    }

    public Long getWidth() {
        return width;
    }

    public void setWidth(Long width) {
        this.width = width;
    }
}
  • 创建一个正方形继承自长方形:
public class Square extends Rectangle {
    private Long length;

    public Long getLength() {
        return length;
    }

    public void setLength(Long length) {
        this.length = length;
    }
    @Override
    public Long getHeight() {
        return getLength();
    }

    @Override
    public void setHeight(Long height) {
        setLength(height);
    }
    @Override
    public Long getWidth() {
        return getLength();
    }

    @Override
    public void setWidth(Long width) {
        setLength(width);
    }
}
  • 现在写一个测试类,让长方形的高一直自增,直到等于宽,变成正方形
public class ChangeLength {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle();
        rectangle.setWidth(10L);
        rectangle.setHeight(5L);
        change(rectangle);
    }
    public static void change(Rectangle rectangle) {
        while (rectangle.getWidth()  rectangle.getHeight()) {
            rectangle.setHeight(rectangle.getHeight() + 1);
            System.out.println("width:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());
        }
        System.out.println("change 方法结束" + "\nwidth:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());
    }
}
  • 看下运行结果:
    在这里插入图片描述
  • 最后得到了一个10*10的正方形。那么如果现在把长方形改为正方形,又会怎样呢,我们看下:
 public static void change(Rectangle rectangle) {
        while (rectangle.getWidth() > rectangle.getHeight()) {
            rectangle.setHeight(rectangle.getHeight() + 1);
            System.out.println("width:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());
        }
        System.out.println("change 方法结束" + "\nwidth:"+rectangle.getWidth() + ",height:"+rectangle.getHeight());
    }


    public static void main(String[] args) {
        Square square = new Square();
        square.setLength(10L);
        change(square);
    }
  • 程序陷入了死循环,违背了里氏替换原则,用子类替换父类后,并没有打到我们语气的结果,里氏替换原则只存在于父类与子类之间,约束继承泛滥。我们现在再定义一个四边形:
public interface Quadrangle {
    long getWidth();
    long getHeight();
}
  • 修改长方形:
public class Rectangle implements Quadrangle {
    private Long height;
    private Long width;

    @Override
    public long getHeight() {
        return height;
    }

    public void setHeight(Long height) {
        this.height = height;
    }

    @Override
    public long getWidth() {
        return width;
    }

    public void setWidth(Long width) {
        this.width = width;
    }
}
  • 修改正方形:
public class Square extends Rectangle implements Quadrangle {
    private Long length;

    public Long getLength() {
        return length;
    }

    public void setLength(Long length) {
        this.length = length;
    }
    @Override
    public long getHeight() {
        return getLength();
    }
    @Override
    public long getWidth() {
        return getLength();
    }

}
  • 此时把change方法的参数改为Quadrangle 类,方法就会报错,因为正方形内部已经没有了setWidth()和 setHeight()方法了。为了约束继承泛滥,change方法的参数只能用Rectangle 长方形。

7.合成复用原则

  • 合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)/聚合(contains-a),而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类对另一个类的影响相对较小。继承可以称为白箱复用,相当于把所有的实现细节暴露给子类,组合/聚合相当于黑箱复用,对类以外的对象无法得知具体功能的实现细节。
  • 以数据库连接操作为例,首先定义一个MySQL连接方法类:
public class MySqlConnection {
    public String getConnection() {
        return "成功连接MySQL";
    }
}
  • 在DAO层调用,并操作数据库
public class OperateDao {
    private MySqlConnection connection;
    public void setConnection(MySqlConnection connection) {
        this.connection = connection;
    }
    public void doTask() {
        String conn = connection.getConnection();
        System.out.println(conn + "并成功操作");
    }
}

  • 调用的代码只管调用连接方法就可以了,不用去关心到底是怎么连接的。

总结:

  • 在实际开发过程中,并不是一定要求所有代码都 遵循设计原则,我们要考虑人力、时间、成本、质量,不是刻意追求完美,要在适当的 场景遵循设计原则,体现的是一种平衡取舍,帮助我们设计出更加优雅的代码结构。
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!