设计模式:(一)设计模式概述与原则

一、设计模式概述

1、概述

1、什么是设计模式:设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题,所提出的解决方案
2、设计模式特点:在编写软件过程中,将面临着来自耦合性内聚性以及可维护性可扩展性重用性灵活性等多方面的挑战,设计模式是为了让程序(软件),具有以下更好的特点:
  • 代码重用性(即:相同功能的代码,不用多次编写)。
  • 可读性(即:编程规范性,便于阅读和理解)。
  • 可扩展性(即:当需要增加新的功能时,非常的方便,称为可维护性)。
  • 可靠性(即:当新增加新的功能后,对原来的功能没有影响)。
  • 使程序呈现高内聚,低耦合的特性。

2、设计模式遵守的七大原则

1、设计模式原则解释:就是在编程时,应当遵守的原则,也是各种设计模式的基础。
2、设计模式常用的七大原则:
  • 单一职责原则
  • 接口隔离原则
  • 依赖倒转(倒置)原则
  • 里氏替换原则
  • 开闭原则
  • 迪米特法则
  • 合成复用原则

3、设计模式类型

1、设计模式分为三种类型共23种:创建型模式、结构模式、行为型模型
2、创建型模式
  • 单例模式、抽象工厂模式、原型模式、建造者模式、工厂方法模式。
3、结构模式
  • 适配器模式、桥接模式、装饰模式、组合模式、外观模式、享元模式、代理模式。
3、行为型模型
  • 模板方法模式、命令模式、访问者模式、迭代器模式、观察者模式、中介者模式、备忘录模式、解释器模式、状态模式、策略模式、职责链模式(责任链模式)。

二、设计模式七大原则

1、单一职责原则

1、单一职责原则(Single Responsibility Principle):是针对类来说的,即一个类应该只负责一项职责。如果类A负责两个不同职责:职责1,职责2。当职责1需求变更而改变类A时,可能会造成职责2执行错误,所以需要将类A的粒度分解为A1,A2。
2、优点:
  • 降低类的复杂度,一个类只负责一项职责。
  • 提高类的可读性,可维护性。
  • 降低变更引起的风险。
  • 通常情况下,我们应该遵守单一职责原则,只有逻辑足够简单,才可以在代码级违反单一职责原则;只有类中方法数量足够少,才可以在方法级别保持单一职责原则。
/**
 * 交通工具类
 */
public class Vehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + "在公路上运行...");
    }
}

/**
 * 单一职责原则
 */
public class responSibility {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.run("汽车");
        vehicle.run("飞机");
    }
}

/**
 * 运行结果:
 * 汽车在公路上运行...
 * 飞机在公路上运行...
 */
问题:通过运行效果可以看出,调用run方法违反了单一职责原则,可以根据交通工具不同,分解成不同类即可。

代码示例2

/**
 * 汽车工具类
 */
public class Vehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + "在公路上运行...");
    }
}

/**
 * 飞机工具类
 */
public class AirVehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + "在天空上飞行...");
    }
}

/**
 * 单一职责原则
 */
public class responSibility {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.run("汽车");
        AirVehicle airVehicle = new AirVehicle();
        airVehicle.run("飞机");
    }
}

/**
 * 运行结果:
 * 汽车在公路上运行...
 * 飞机在天空上飞行...
 */
虽然这种方案遵循了单一职责原则,但是改动是很大的,可以通过方法单一职责进行优化,对类的改动很小,只不过是新增加方法,不同的交通工具调用不同的方法。

代码示例3

/**
 * 交通工具类
 */
public class Vehicle {
    public void run(String vehicle) {
        System.out.println(vehicle + "在公路上运行...");
    }
    
    public void runAir(String vehicle) {
        System.out.println(vehicle + "在天空飞行...");
    }
}

/**
 * 单一职责原则
 */
public class responSibility {
    public static void main(String[] args) {
        Vehicle vehicle = new Vehicle();
        vehicle.run("汽车");
        vehicle.runAir("飞机");
    }
}

/**
 * 运行结果:
 * 汽车在公路上运行...
 * 飞机在天空飞行...
 */
这种方式对类的修改很小,只是增加方法,虽然没有在类级别上遵守单一职责原则,但是在方法级别上,仍然是遵守单一职责原则

2、接口隔离原则

1、接口隔离原则(Interface Segregation Principle):客户端不应该依赖它不需要的接口;即一个类对另一个类的依赖应该建立在最小的接口上。
2、问题由来:类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。

在这里插入图片描述

3、图解:类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法。
4、解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。
public interface I {
    public void method1();
    public void method2();
    public void method3();
    public void method4();
    public void method5();
}

public class A {
    public void depend1(I i){
        i.method1();
    }
    public void depend2(I i){
        i.method2();
    }
    public void depend3(I i){
        i.method3();
    }
}

public class B implements I {
    public void method1() {
        System.out.println("类B实现接口I的方法1");
    }
    public void method2() {
        System.out.println("类B实现接口I的方法2");
    }
    public void method3() {
        System.out.println("类B实现接口I的方法3");
    }
    //对于类B来说,method4和method5不是必需的,但是由于接口A中有这两个方法,
    //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
    public void method4() {}
    public void method5() {}
}

public class C {
    public void depend1(I i){
        i.method1();
    }
    public void depend2(I i){
        i.method4();
    }
    public void depend3(I i){
        i.method5();
    }
}

public class D implements I {
    public void method1() {
        System.out.println("类D实现接口I的方法1");
    }
    //对于类D来说,method2和method3不是必需的,但是由于接口A中有这两个方法,
    //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
    public void method2() {}
    public void method3() {}
    public void method4() {
        System.out.println("类D实现接口I的方法4");
    }
    public void method5() {
        System.out.println("类D实现接口I的方法5");
    }
}

public class Test {
    public static void main(String[] args) {
        A a = new A();
        a.depend1(new B());
        a.depend2(new B());
        a.depend3(new B());

        C c = new C();
        c.depend1(new D());
        c.depend2(new D());
        c.depend3(new D());
    }
}

/**
 * 运行结果:
 * 类B实现接口I的方法1
 * 类B实现接口I的方法2
 * 类B实现接口I的方法3
 * 类D实现接口I的方法1
 * 类D实现接口I的方法4
 * 类D实现接口I的方法5
 */
可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口,拆分后的设计如下:

在这里插入图片描述

代码示例2

interface I1 {
	public void method1();
}
 
interface I2 {
	public void method2();
	public void method3();
}
 
interface I3 {
	public void method4();
	public void method5();
}
 
class A{
	public void depend1(I1 i){
		i.method1();
	}
	public void depend2(I2 i){
		i.method2();
	}
	public void depend3(I2 i){
		i.method3();
	}
}
 
class B implements I1, I2{
	public void method1() {
		System.out.println("类B实现接口I1的方法1");
	}
	public void method2() {
		System.out.println("类B实现接口I2的方法2");
	}
	public void method3() {
		System.out.println("类B实现接口I2的方法3");
	}
}
 
class C{
	public void depend1(I1 i){
		i.method1();
	}
	public void depend2(I3 i){
		i.method4();
	}
	public void depend3(I3 i){
		i.method5();
	}
}
 
class D implements I1, I3{
	public void method1() {
		System.out.println("类D实现接口I1的方法1");
	}
	public void method4() {
		System.out.println("类D实现接口I3的方法4");
	}
	public void method5() {
		System.out.println("类D实现接口I3的方法5");
	}
}
注意事项:
  • 接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不争的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。
  • 为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。
  • 提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。

3、依赖倒转原则

1、依赖倒转原则(Dependence Inversion Principle):高层模块不应该依赖底层模块,二者应该都依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象
2、依赖倒转的核心思想是:面向接口编程
3、依赖倒置原则基于这样一个事实:相对于细节的多变性,抽象的东西要稳定的多。以抽象为基础搭建起来的架构比以细节为基础搭建起来的架构要稳定的多。在java中,抽象指的是接口或者抽象类,细节就是具体的实现类,使用接口或者抽象类的目的是制定好规范和契约,而不去涉及任何具体的操作,把展现细节的任务交给他们的实现类去完成。
/**
 * 母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了
 */
public class Book {
    public String getContent(){
        return "从前山上有棵树……";
    }
}

public class Mather {
    public void narrate(Book book){
        System.out.println("妈妈开始讲故事");
        System.out.println(book.getContent());
    }
}

public class Test {
    public static void main(String[] args) {
        Mather mather = new Mather();
        mather.narrate(new Book());
    }
}

/**
 * 运行结果:
 * 妈妈开始讲故事
 * 从前山上有棵树……
 */
假如有一天,需求变更:不是给书对象,而是给一个报纸,让母亲讲报纸上内容,发现办不到(需要修改Mather中代码才能读),假如换成其他的,则每次都需要修改Mather对象,这显然不是好的设计。原因就是Mather与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。
解决方案:可以引入一个抽象接口IReader,让Mather类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了。
public interface IReader {
    public String getContent();
}

public class Book implements IReader {
    @Override
    public String getContent() {
        return "从前山上有棵树……";
    }
}

public class Newspaper implements IReader {
    @Override
    public String getContent() {
        return "中国战胜了新冠!!!";
    }
}

public class Mather {
    public void narrate(IReader iReader){
        System.out.println("妈妈开始讲故事");
        System.out.println(iReader.getContent());
    }
}

public class Test { 
    public static void main(String[] args) {
        Mather mather = new Mather();
        mather.narrate(new Book());
        mather.narrate(new Newspaper());
    }
}

/**
 * 运行结果:
 * 妈妈开始讲故事
 * 从前山上有棵树……
 * 妈妈开始讲故事
 * 中国战胜了新冠!!!
 */
注意事项
  • 低层模块尽量都要有抽象类或接口,或者两者都有,程序稳定性更好。
  • 变量的声明类型尽量是抽象类或接口,这样我们的变量引用和实际对象间,就存在一个缓冲层,利于程序扩展和优化。
  • 继承时遵循里氏替换原则。

4、里氏替换原则

1、里氏替换原则(Likov Substitution Principle):这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。
2、定义:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象 o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。换句话说,所有引用基类的地方必须能透明地使用其子类的对象
3、问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。
4、解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。
5、继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。
6、继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。
/**
 * 负责两数相减
 */
public class A {
    public int func1(int a, int b){
        return a-b;
    }
}

public class Test {
    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-50=50
 * 100-80=20
 */
/**
 * 增加一个新功能,完成两数相加,然后和100求和,由类B负责,即类B需要完成两个功能
 * 1、两数相减
 * 2、两数相加,再与100求和
 * 由于类A实现了第一个功能,因此类B直接继承类A后,只用开发第二个功能即可
 */
public class B extends A {
    /**
     * 这里可能重写了类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 Test {
    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));
    }
}

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

/**
 * 负责两数相减
 */
public class A extends Base {
    public int func1(int a, int b){
        return a-b;
    }
}

public class B extends Base {
    /**
     * 如果B需要使用A类的方法,使用组合关系
     */
    private A a = new A();
    
    public int func1(int a, int b) {
        return a + b;
    }

    public int func2(int a, int b) {
        return func1(a, b) + 100;
    }
    
     public int func3(int a, int b) {
        return this.a.func1(a, b);
    }
}

public class Test {
    public static void main(String[] args) {
        B b = new B();
        //因为B类不再继承A类,因此调用者,不会再认为func1是求减法
        //调用完成的功能就会很明确
        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));
        //使用组合仍然可以使用到A类相关方法
        System.out.println("100-20="+b.func3(100, 20));
    }
}

/**
 * 运行结果:
 * 100+50=150
 * 100+80=180
 * 100+20+100=220
 * 100-20=80
 */
1、总结:里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。包含以下4种含义
  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法(已经实现的方法)。
  • 子类中可以增加自己特有的方法。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

5、迪米特法则

1、定义:一个对象应该对其他对象保持最少的了解。
2、问题由来:类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大。
3、解决方案:尽量降低类与类之间的耦合。
4、迪米特法则(Demeter Principle)又叫最少知道原则,即一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类不管多么复杂,都尽量将逻辑封装在类的内部,对外除了提供的public方法,不对外泄露任何信息。
5、迪米特法则还有一个更简单的定义:只与直接的朋友通信。
6、直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

代码示例1

有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看一下违反迪米特法则的设计。
/**
 * 总公司员工类
 */
@Data
public class Employee {
    private String id;
}

/**
 * 分公司员工类
 */
@Data
public class SubEmployee {
    private String id;
}

/**
 * 直接朋友有Employee、SubCompanyManager
 * SubEmployee不是直接朋友,而是一个陌生类,这样就违背了迪米特法则
 */
public 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){
        //这里的SubEmployee不是CompanyManager的直接朋友
        //SubEmployee是以局部变量的方式出现在CompanyManager的,违反了迪米特法则
        List<SubEmployee> list1 = sub.getAllEmployee();
        for(SubEmployee e:list1){
            System.out.println(e.getId());
        }

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

public 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 class Test {
    public static void main(String[] args){
          e = new CompanyManager();
        e.printAllEmployee(new SubCompanyManager());
    }
}
现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。

代码示例2

public 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> list2 = this.getAllEmployee();
        for(Employee e:list2){
            System.out.println(e.getId());
        }
    }
}

public 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());
        }
    }
}

public class Test {
    public static void main(String[] args){
        CompanyManager e = new CompanyManager();
        e.printAllEmployee(new SubCompanyManager());
    }
}
总结:
  • 迪米特法则的初衷是降低类之间的耦合。
  • 由于每个类都减少了不必要的依赖,因此迪米特法则只是要求降低类间(对象间)耦合关系,并不是要求完全没有依赖关系。

6、开闭原则

1、开闭原则(Open Closed Principle):是编程中最基础、最重要的设计原则,一个软件实体如类、模块和函数应该对扩展开放(对提供方),对修改关闭(对使用方)。用抽象构建框架,用实现扩展细节。因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节,我们用从抽象派生的实现类来进行扩展,当软件需要发生变化时,我们只需要根据需求重新派生一个实现类来扩展就可以了。当然前提是我们的抽象要合理,要对需求的变更有前瞻性和预见性才行。
2、问题由来:在软件的生命周期内,因为变化、升级和维护等原因需要对软件原有代码进行修改时,可能会给旧代码中引入错误,也可能会使我们不得不对整个功能进行重构,并且需要原有代码经过重新测试。
3、解决方案:当软件需要变化时,尽量通过扩展软件实体的行为来实现变化,而不是通过修改已有的代码来实现变化。
4、其他说明:
  1. 开闭原则是面向对象设计中最基础的设计原则,它指导我们如何建立稳定灵活的系统。开闭原则可能是设计模式原则中定义最模糊的一个了,它只告诉我们对扩展开放,对修改关闭,可是到底如何才能做到对扩展开放,对修改关闭,并没有明确的告诉我们。
  2. 其实,我们遵循设计模式前面5大原则,以及使用23种设计模式的目的就是遵循开闭原则。也就是说,只要我们对前面5项原则遵守的好了,设计出的软件自然是符合开闭原则的,这个开闭原则更像是前面五项原则遵守程度的“平均得分”,前面5项原则遵守的好,平均分自然就高,说明软件设计开闭原则遵守的好;如果前面5项原则遵守的不好,则说明开闭原则遵守的不好。
  3. 再回想一下前面说的5项原则,恰恰是告诉我们用抽象构建框架,用实现扩展细节的注意事项而已:单一职责原则告诉我们实现类要职责单一;里氏替换原则告诉我们不要破坏继承体系;依赖倒置原则告诉我们要面向接口编程;接口隔离原则告诉我们在设计接口的时候要精简单一;迪米特法则告诉我们要降低耦合。而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。

7、合成复用原则

1、合成复用原则(Composite Reuse Principle):又叫组合/聚合复用原则。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。
2、通常类的复用方式分为两种:
  • 继承复用
  • 合成复用
3、继承复用特点:
  • 简单易实现(优点)
  • 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。
  • 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。
  • 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。
4、合成复用原则:是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用;有如下特点:
  • 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  • 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。
  • 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。
5、举例说明:汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。
  • 继承关系实现的汽车分类的类图

在这里插入图片描述

  • 从上图可以看出用继承关系实现会产生很多子类,而且增加新的“动力源”或者增加新的“颜色”都要修改源代码,这违背了开闭原则,显然不可取。但如果改用组合关系实现就能很好地解决以上问题,其类图如下所示。

在这里插入图片描述

8、总结

在这里插入图片描述

原则的最终目的:降低对象之间的耦合,增加程序的可复用性、可扩展性和可维护性

三、UML类图

1、UML基本介绍

1、UML——Unified Modeling Language(UML)又称统一建模语言或标准建模语言,是一种用于软件系统分析和设计的语言工具,它用于帮助软件开发人员进行思考和记录思路的工具。
2、UML本身是一套符号的规定,就像数学符号和化学符号一样,这些符号用于描述软件模型中各个元素和他们之间的关系,比如类、接口、实现、泛化(继承)、依赖、组合、聚合等。

2、UML图分类

1、分类:
  • 用例图(use case)
  • 静态结构图:类图、对象图、包图、组件图、部署图
  • 动态行为图:交互图(时序图与协作图)、状态图、活动图
2、UML类图
  • 类图是描述类与类之间的关系的,是UML图中最核心的。
  • 用于描述系统中的类(对象)本身组成和类(对象)之间的各种静态关系
3、类之间关系:依赖、泛化(继承)、实现、关联、聚合、组合

3、依赖关系(Dependence)

1、说明:只要在类中用到了对方(类的成员属性、方法的返回类型、方法接收的参数类型、方法中使用到等),那么他们之间就存在依赖关系。如果没有对方,连编译都通过不了。
2、表示方式:依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。

在这里插入图片描述

/**
 * 人类要想打电话必须依赖手机
 */
public class Person {
    private String name;

    public void call(Phone phone) {
        phone.transfer();
    }
}

public class Phone {
    public void transfer(){
        System.out.println("打电话");
    }
}

5、泛化关系(Generalization)

1、说明:对象之间耦合度最大的一种关系,是父类与子类之间的关系,是一种继承关系,是 is-a 的关系,是依赖关系的特例。
2、表示方式:泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。

在这里插入图片描述

/**
 * Student 类和 Teacher 类都是 Person 类的子类
 */
public class Person {
    private String name;

    private Integer age;

    public void speak() {
        System.out.println("都会说中文");
    }
}

public class Teacher extends Person {
    private long teacherNo;

    public void teaching() {
        System.out.println("传道受业解惑");
    }
}

public class Student extends Person {
    private long studentNo;

    public void study() {
        System.out.println("学习");
    }
}

6、实现关系(Realization)

1、说明:接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作,是依赖关系的特例。
2、表示方式:使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。

在这里插入图片描述

/**
 * 汽车和船实现了交通工具类
 */
public interface Vehicle {
    public void move();
}

public class Car implements Vehicle {
    @Override
    public void move() {
        System.out.println("汽车在地面上移动");
    }
}

public class Ship implements Vehicle {
    @Override
    public void move() {
        System.out.println("船在水中移动");
    }
}

7、关联关系(Association)

1、说明:是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。在代码中通常将一个类的对象作为另一个类的成员变量来实现关联关系。
2、表示方式:关联可以是双向的,也可以是单向的。双向的关联可以用带两个箭头或者没有箭头的实线来表示,单向的关联用带一个箭头的实线来表示,箭头从使用类指向被关联的类。也可以在关联线的两端标注角色名,代表两种不同的角色。

在这里插入图片描述

/**
 * 在代码中通常将一个类的对象作为另一个类的成员变量来实现关联关系。
 * 如下是老师和学生的关系图,每个老师可以教多个学生,每个学生也可向
 * 多个老师学,他们是双向关联。
 */
public class Teacher {
    private String name;

    private List<Student> stus;

    public void teaching() {
        System.out.println("传道受业解惑");
    }
}

public class Student {
    private String name;

    private List<Teacher> teas;

    public void study() {
        System.out.println("学习");
    }
}

8、聚合关系(Aggregation)

1、说明:关系是关联关系的一种,是强关联关系,是整体和部分之间的关系,整体与部分可以分开,是has-a的关系。聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。
2、表示方式:聚合关系可以用带空心菱形的实线来表示,菱形指向整体。

在这里插入图片描述

/**
 * 学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
 */
public class Teacher {
    private String name;

    public void teaching() {
        System.out.println("传道受业解惑");
    }
}

public class University {
    private List<Teacher> teas;
}

9、组合关系(Composition)

1、说明:也是整体与部分的关系,但是整体与部分不可以分开。是一种更强烈的聚合关系,是cxmtains-a关系。在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。
2、表示方式:组合关系用带实心菱形的实线来表示,菱形指向整体。

在这里插入图片描述

代码示例

/**
 * 头和嘴的关系,没有了头,嘴也就不存在了。
 */
public class Head {
    private Mouth mouth = new Mouth();
}

public class Mouth {
    public void eat() {
        System.out.println("吃");
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值