我们编写软件的过程中常常面临着需求变更,每一次的变更其实都是对我们代码的可重用性、可读性、可维护性、可靠性的一次考验。设计模式就是为了让我们的代码具备这些功能,并使程序呈现高内聚,低耦合的特性。

1、可重用性:相同的代码,不需要重复编写

2、可读性:代码规范,命名规范,便于理解

3、可维护性:新增功能容易,方便扩展

4、可靠性:新增的功能,不会对历史功能造成影响

 

一、单一职责原则

  单一职责原则(Single responsibility principle)规定每个类都应该有一个单一的功能,并且该功能应该由这个类完全封装起来。所有这个类的服务都应该严密的和该功能平行(功能平行,意味着没有依赖)。

案例:交通工具运行的Demo

package com.ycdhz.design;

public class SinglePrinciple {

    public static void main(String[] args) {

        Vehicle vehicle = new Vehicle();
        vehicle.run("骑车");
        vehicle.run("飞机");
    }
}

class Vehicle {

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

 飞机是在天上飞的,Vehicle 的run 方法不能满足需求,也违反了单一职责原则。我可以根据不同的交通工具拆分成不同的类。

package com.ycdhz.design;

public class SinglePrinciple {

    public static void main(String[] args) {

        RoadVehicle roadVehicle = new RoadVehicle();
        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 + "在天上飞");
    }
}

 我们将Vehicle 类拆分为RoadVehicle、AirVehicle 满足了单一职责原则,但在实际的开发中我们往往不会这么做,我们做了类分解同时还修改了客户端,这样的改动太大。

package com.ycdhz.design;

public class SinglePrinciple {

    public static void main(String[] args) {

        Vehicle vehicle = new Vehicle();
        vehicle.roadRun("公交");
        vehicle.airRun("飞机");
    }
}

class Vehicle{
    public void roadRun(String vehicle){
        System.out.println(vehicle + "在路上跑");
    }

    public void airRun(String vehicle){
        System.out.println(vehicle + "在天上飞");
    }
}

 我们没有对Vehicle 类进行拆分,而是新增了一个方法。虽然从类上来说不满足单一职责原则,但是从方法上来说依旧是满足的。

总结

1、降低类的复杂度,一个类只负责一项职责。

2、提高类的可读性,可维护性。

3、降低变更引起的风险。

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

 

二、里氏替换原则

  里氏替换原则(Liskov Substitution Principle) 子类型必须可以替换掉它们的父类。子类可以扩展父类的功能,但不能改变父类原有的功能。简单来说就是一个软件实体如果使用的是一个父类的话,那么一定适用于其子类,如果把软件中父类替换为子类,程序的行为没有任何变化。

案例:

package com.ycdhz.design;

public class LiskovPrinciple {
    
    public static void main(String[] args) {
        A a = new A();
        System.out.println("11-3=" + a.func1(11, 3));

        B b = new B();
        System.out.println("11-3=" + b.func1(11, 3));//这里本意是求出 11-3
        System.out.println("11+3+9=" + b.func2(11, 3));
    }
}

class A {

    public int func1(int num1, int num2) {
        return num1 - num2;
    }
}

class B extends A{

    @Override
    public int func1(int a, int b) {
        return a + b; // 重写了A的方法
    }

    public int func2(int a, int b) {
        return func1(a, b) + 9;
    }
}

我们发现原来运行正常的功能,因为类 B 无意中重写了父类的方法,造成功能出现错误。在实际开发过程中我们经常会通过重写来实现新功能,这样虽然开发简单,但是整个体系的复用性会比较差。我们可以定义一个更通俗的基类,让原有的父类,子类通过这个基类,去掉他们的继承关系。采用聚合、组合、依赖的方式来代替。

package com.ycdhz.design;

public class LiskovPrinciple {

    public static void main(String[] args) {
        B b = new B();
        System.out.println("11+3=" + b.func1(11, 3));
        System.out.println("11+3+9=" + b.func2(11, 3));
        System.out.println("11-3=" + b.func3(11, 3));
    }
}

class Base {

}

class A extends Base {

    public int func1(int num1, int num2) {
        return num1 - num2;
    }
}

class B extends Base {

    private A a = new A();

    public int func1(int num1, int num2) {
        return num1 + num2;
    }

    public int func2(int num1, int num2) {
        return func1(num1, num2) + 9;
    }

    public int func3(int num1, int num2) {
        return a.func1(num1, num2);
    }
}

总结:

1、里氏替换原则 所有引用基类的地方必须能透明地使用其子类的对象。
2、在使用继承时,遵循里氏替换原则,在子类中尽量不要重写父类的方法。
3、里氏替换原则告诉我们,继承实际上让两个类耦合性增强了,在适当的情况下,可以通过 聚合、组合、依赖来解决问题。

 

三、依赖倒置原则

  依赖倒转原则(Dependence Inversion Principle) 需要满足两点:高层模块不应该依赖低层模块,二者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。相对于细节(实现类)的多变性,抽象(接口/抽象类)的东西要稳定的多。以抽象为基础搭建的架构比以细节为基础的架构要稳定的多。

案例: 用户接收Email消息功能

package com.ycdhz.design;

public class DependecyPrinciple {

    public static void main(String[] args) {
        Persion persion = new Persion();
        persion.Recive(new Email());
    }
}

class Email {
    public void getInfo() {
        System.out.println("Email Msg!");
    }
}

class Persion {
    public void Recive(Email email){
        email.getInfo();
    }
}

如果这个时候需求发生了变更,我们需要接收微信、短信、QQ等等,则我们需要为每一个功能新增一个类,同时Perons 也要增加相应的接收方法。这显然不符合我们的依赖倒转原则,这时我们只引入一个IRecive 接口,让Perons 与IRecive 接口发生依赖。

package com.ycdhz.design;

public class DependecyPrinciple {

    public static void main(String[] args) {
        Persion persion = new Persion();
        persion.Recive(new Email());
    }
}

class Persion {
    public void Recive(IRecive recive){
        recive.getInfo();
    }
}

interface IRecive{
    void getInfo();
}

class Email implements IRecive {
    @Override
    public void getInfo() {
        System.out.println("Email Msg!");
    }
}

这个时候我们只需要根据需求添加具体的业务代码,而不需要改变客户端。

依赖关系的三种传递方式

1、接口传递
2、构造方法传递
3、setter 方式传递

总结:

1、低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好。
2、变量的声明类型尽量是抽象类或接口, 这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化。
3、继承时遵循里氏替换原则。

 

四、接口隔离原则

  接口隔离原则(Interface Segregation Principle) 客户端不应该依赖它不需要的接口;一个类对另一个类的依赖应该建立在最小的接口上。

 

案例

如图现在 ClientA 需要通过接口 Interface1 接口依赖ServiceA 中的Operation1、Operation2、Operation3 三个方法,ClientB 需要通过接口 Interface1 依赖ServiceB中的Operation1、Operation4、Operation5 三个方法。

package com.ycdhz.design;

public class InterfacePrinciple {

    public static void main(String[] args) {
        ClientA a = new ClientA();
        a.operation1(new ServiceA());
        a.operation2(new ServiceA());
        
        ClientB b = new ClientB();
        b.operation1(new ServiceB());
        b.operation4(new ServiceB());
    }
}

interface Interface1{
    void operation1();
    void operation2();
    void operation3();
    void operation4();
    void operation5();
}


class ServiceA implements Interface1{

    @Override
    public void operation1() {
        System.out.println("Class ServiceA operation 1");
    }

    @Override
    public void operation2() {
        System.out.println("Class ServiceA operation 2");
    }

    @Override
    public void operation3() {
        System.out.println("Class ServiceA operation 3");
    }

    @Override
    public void operation4() {
        System.out.println("Class ServiceA operation 4");
    }

    @Override
    public void operation5() {
        System.out.println("Class ServiceA operation 5");
    }
}

class ServiceB implements Interface1{

    @Override
    public void operation1() {
        System.out.println("Class ServiceB operation 1");
    }

    @Override
    public void operation2() {
        System.out.println("Class ServiceB operation 2");
    }

    @Override
    public void operation3() {
        System.out.println("Class ServiceB operation 3");
    }

    @Override
    public void operation4() {
        System.out.println("Class ServiceB operation 4");
    }

    @Override
    public void operation5() {
        System.out.println("Class ServiceB operation 5");
    }
}

class ClientA {
    public void operation1(Interface1 interface1){
        interface1.operation1();
    }

    public void operation2(Interface1 interface1){
        interface1.operation2();
    }

    public void operation3(Interface1 interface1){
        interface1.operation3();
    }
}

class ClientB {
    public void operation1(Interface1 interface1){
        interface1.operation1();
    }

    public void operation4(Interface1 interface1){
        interface1.operation4();
    }

    public void operation5(Interface1 interface1){
        interface1.operation5();
    }
}

我们发现Interface1 接口对于ServiceA、ServiceB 来说都不是最小接口,明显违背了接口隔离原则。我可以根据不同的需要对接口进行拆分,将接口 Interface1 拆分为独立的几个接口( Interface1、Interface2、Interface3),类 ServiceA 和类ServiceB 分别与他们需要的接口建立依赖关系。

package com.ycdhz.design;

public class InterfacePrinciple {

    public static void main(String[] args) {
        ClientA a = new ClientA();
        a.operation1(new ServiceA());
        a.operation2(new ServiceA());
        a.operation3(new ServiceA());
        
        ClientB b = new ClientB();
        b.operation1(new ServiceB());
        b.operation4(new ServiceB());
        b.operation5(new ServiceB());
    }
}

interface Interface1{
    void operation1();
}

interface Interface2{
    void operation2();
    void operation3();
}

interface Interface3{
    void operation4();
    void operation5();
}

class ServiceA implements Interface1, Interface2{

    @Override
    public void operation1() {
        System.out.println("Class ServiceA operation 1");
    }

    @Override
    public void operation2() {
        System.out.println("Class ServiceA operation 2");
    }

    @Override
    public void operation3() {
        System.out.println("Class ServiceA operation 3");
    }
}

class ServiceB implements Interface1, Interface3 {

    @Override
    public void operation1() {
        System.out.println("Class ServiceB operation 1");
    }

    @Override
    public void operation4() {
        System.out.println("Class ServiceB operation 4");
    }

    @Override
    public void operation5() {
        System.out.println("Class ServiceB operation 5");
    }
}

class ClientA {
    public void operation1(Interface1 interface1){
        interface1.operation1();
    }

    public void operation2(Interface2 interface1){
        interface1.operation2();
    }

    public void operation3(Interface2 interface1){
        interface1.operation3();
    }
}

class ClientB {
    public void operation1(Interface1 interface1){
        interface1.operation1();
    }

    public void operation4(Interface3 interface1){
        interface1.operation4();
    }

    public void operation5(Interface3 interface1){
        interface1.operation5();
    }
}

总结

1、将接口 Interface1 进行合理的拆分,让类ClientA 和类ClientB 分别与他们需要的接口建立依赖关系,这样就满足了接口隔离原则。

 

五、迪米特法则

  迪米特法则(Demeter Principle)又叫 最少知道原则,即一个类 对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部。对外除了提供的 public 方法,不对外泄露任何信息。

  迪米特法则还有个更简单的定义:只与直接的朋友通信。每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多依赖、关联、组合、聚合等。其中我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类不是直接的朋友。

package com.ycdhz.design;

import java.util.ArrayList;
import java.util.List;

public class DemeterPrinciple {

    public static void main(String[] args) {
        CompanyManager manager = new CompanyManager();
        manager.printAllEmployee(new SubCompanyManager());
    }
}

//总公司员工
class Employee {
    private String id;

    public void setId(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }
}

//分公司员工
class SubEmployee {
    private String id;

    public void setId(String id) {
        this.id = id;
    }

    public String getId() {
        return id;
    }
}

class SubCompanyManager {
    public List<SubEmployee> getAllEmployee() {
        List<SubEmployee> list = new ArrayList<SubEmployee>();
        for (int i = 0; i < 100; i++) {
            SubEmployee emp = new SubEmployee();
            //为分公司人员按顺序分配一个ID
            emp.setId("分公司" + i);
            list.add(emp);
        }
        return list;
    }
}

class CompanyManager {
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<Employee>();
        for (int i = 0; i < 30; i++) {
            Employee emp = new Employee();
            //为总公司人员按顺序分配一个ID
            emp.setId("总公司" + i);
            list.add(emp);
        }
        return list;
    }

    public void printAllEmployee(SubCompanyManager sub) {
        List<SubEmployee> list1 = sub.getAllEmployee();
        for(SubEmployee e:list1){
            System.out.println(e.getId());
        }

        List<Employee> list = this.getAllEmployee();
        for (Employee e : list) {
            System.out.println(e.getId());
        }
    }
}

我们发现在CompanyManager 类的printAllEmployee 方法中与子公司的员工发生了耦合,按照迪米特法则只与直接的朋友发生通信,而SubEmployee 类并不是CompanyManager 类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。

class SubCompanyManager {
    public List<SubEmployee> getAllEmployee() {
        List<SubEmployee> list = new ArrayList<SubEmployee>();
        for (int i = 0; i < 100; i++) {
            SubEmployee emp = new SubEmployee();
            //为分公司人员按顺序分配一个ID
            emp.setId("分公司" + i);
            list.add(emp);
        }
        return list;
    }

    public void printEmployee() {
        List<SubEmployee> list = this.getAllEmployee();
        for (SubEmployee e : list) {
            System.out.println(e.getId());
        }
    }
}

class CompanyManager {
    public List<Employee> getAllEmployee() {
        List<Employee> list = new ArrayList<Employee>();
        for (int i = 0; i < 30; i++) {
            Employee emp = new Employee();
            //为总公司人员按顺序分配一个ID
            emp.setId("总公司" + i);
            list.add(emp);
        }
        return list;
    }

    public void printAllEmployee(SubCompanyManager sub) {
        sub.printEmployee();

        List<Employee> list = this.getAllEmployee();
        for (Employee e : list) {
            System.out.println(e.getId());
        }
    }
}

 修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。

总结:

1、迪米特法则的核心是降低类之间的耦合。
2、由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系, 并不是要求完全没有依赖关系。

 

六、开闭原则

开闭原则(Open Closed Principle)是编程中最基础、最重要的设计原则。软件实体(类,模块,函数)应该可以扩展,但是不可以修改。即对于扩展是开放的,对于更改是封闭的。面对需求,对程序的改动是通过增加新代码进行的,而不是更改现有的代码。开放-封闭原则是面向对象设计的核心所在,遵循这个原则可以使系统可维护,可扩展,可复用,灵活性好。

package com.ycdhz.design;

public class OpenClosedPrinciple {

    public static void main(String[] args) {
        GraphicEditor editor = new GraphicEditor();
        editor.drawCircle(new Circle());
        editor.drawRectangle(new Rectangle());
    }
}

//Shape 类,基类
class Shape {
    int m_type;
}

class Rectangle extends Shape {
    Rectangle() {
        super.m_type = 1;
    }
}
class Circle extends Shape {
    Circle() {
        super.m_type = 2;
    }
}

class GraphicEditor {
    public void drawShape(Shape s) {
        if (s.m_type == 1) {
            drawRectangle(s);
        } else if (s.m_type == 2){
            drawCircle(s);
        }
    }

    public void drawRectangle(Shape r) {
        System.out.println("绘制矩形");
    }

    public void drawCircle(Shape r) {
        System.out.println("绘制圆形");
    }
}

这个时候如果我们需要绘制一个三角形,我们发现需要做很多的修改,违背了开闭原则。其实我们可以将Shape 类做成抽象类,并提供一个抽象的 draw 方法,让子类去实现,这样我们有新的图形种类时,只需要让新的图形类继承 Shape,并实现 draw 方法即可。

package com.ycdhz.design;

public class OpenClosedPrinciple {

    public static void main(String[] args) {
        GraphicEditor editor = new GraphicEditor();
        editor.drawShape(new Circle());
        editor.drawShape(new Rectangle());
    }
}

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

class Rectangle extends Shape {

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

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

class GraphicEditor{
    public void drawShape(Shape s) {
        s.draw();
    }
}

总结

1、开闭原则(Open Closed Principle)是编程中 最基础、最重要的设计原则。
2、一个软件实体如类,模块和函数应该 对扩展开放( 对提供方),对 修改关闭( 对使用方)。用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。
3、当软件需要变化时,尽量 通过扩展软件实体的行为来实现变化,而不是 通过修改已有的代码来实现变化。

 

 

 

 

posted on 2020-08-06 10:59  愚蠢的猴子  阅读(364)  评论(0编辑  收藏  举报