设计模式第一篇之设计模式原则

为什么需要使用设计模式?(使用设计模式解决了什么问题?)

  • 在编写软件的过程中,随着项目的代码越来越多,项目越来越难以维护,每当需要对原有的代码进行修改时都会带来一些新的问题,使得系统拓展和维护会非常不易,而当我们在初始设计软件,编写代码的过程中如果能够遵循某种规范,按照一种原则进行设计和编码,可以大大的提高系统的可拓展性,这就是学习设计模式解决的问题
  • 设计模式的整体原则是使程序呈现高内聚,低耦合的特性,即类的内部联系紧密,而类与类之间的联系尽可能少,七大设计原则也是遵循这个整体原则,同时使得系统易于拓展

按目的来区分23种设计模式

  • 创建型模式:按照某种原则创建对象,是生产对象有关的设计模式,可以极大的提高程序的拓展性,以及运行效率(单例模式)。
  • 结构型模式:如何将类或者对 象结合在一起形成更大的结构,就像搭积木,可以通过 简单积木的组合形成复杂的、功能更为强大的结构。重点在于如何通过一种组织形式,让原本不相关的类产生联接,产生一种组织结构发挥作用。
  • 行为型模式:用于描述类或对象之间怎样相互协作共同完成单个对象都无法单独完成的任务,以及怎样分配职责。解决类与类之间的协作,每个类如何进行功能划分,使得组织更加高效

UML类图学习

类图是什么:类图是用一种图形化的方式来描述类与类之间的关系以及类所具有的信息的图,类与类之间一共有以下几种关系,类之间联系的紧密程度依次增加
  • 依赖关系
    • 表示一个类里面使用到了另一个类,可能是返回值,函数参数,亦或是局部变量使用到了这个类,是一种使用关系
  • 关联关系
    • 表示一个类里面的成员变量有另一个类,是一种组合关系,就如同汽车和轮子,汽车类里面会有轮子这一个对象。
    • 根据关系的不同,又分为聚合和组合(被关联类是否可以单独存在),自关联,双向关联。
  • 继承关系(泛化关系)
    • 继承关系是最紧密的一种关系,一个类拥有另一个的所有方法和属性。
几种关系的表示方法

在这里插入图片描述

  • 接口和类之间是实现关系,但是接口里面的方法是虚的,所以使用虚线,泛化关系指的是类与类的继承关系,继承是实实在在的继承的一个实体类,所以使用实线。上面的三角形可以理解为关系紧密包含了父类的所有信息。

  • 关联关系,表示一个类里面有另一个类,使用菱形表示某个类被镶嵌到了这个类里面。

  • 依赖关系,表示此类使用到了另一个类,使用虚线指向被使用的类,表示某个类被使用了,没有真正的拥有这个类,只能暂时使用,所有是虚线。
    理解类之间的关系

七大设计原则

单一职责原则
  • 单一职责原则的含义是,每一个类或者每一个函数只负责一项功能,负责微信转账和负责支付宝转账的两个功能不要用同一个类实现,或者同一个方法实现。

我们假设一个场景:

有一个动物类,它会呼吸空气,用一个类描述动物呼吸这个场景:

class Animal{  
    public void breathe(String animal){  
        System.out.println(animal + "呼吸空气");  
    }  
}  
public class Client{  
    public static void main(String[] args){  
        Animal animal = new Animal();  
        animal.breathe("牛");  
        animal.breathe("羊");  
        animal.breathe("猪");  
    }  
}  

在后来发现新问题,并不是所有的动物都需要呼吸空气,比如鱼需要呼吸水,修改时如果遵循单一职责原则的话,那么需要将 Animal 类进行拆分为陆生类和水生动物类,代码如下:

class Terrestrial{  
    public void breathe(String animal){  
        System.out.println(animal + "呼吸空气");  
    }  
}  
class Aquatic{  
    public void breathe(String animal){  
        System.out.println(animal + "呼吸水");  
    }  
}  
  
public class Client{  
    public static void main(String[] args){  
        Terrestrial terrestrial = new Terrestrial();  
        terrestrial.breathe("牛");  
        terrestrial.breathe("羊");  
        terrestrial.breathe("猪");  
          
        Aquatic aquatic = new Aquatic();  
        aquatic.breathe("鱼");  
    }  
}  

在实际工作中,如果这样修改的话开销是很大的,除了将原来的 Animal 类分解为 Terrestrial 类和 Aquatic 类以外还需要修改客户端,而直接修改类 Animal 类来达到目的虽然违背了单一职责原则,但是花销却小的多,代码如下:

class Animal{  
    public void breathe(String animal){  
        if("鱼".equals(animal)){  
            System.out.println(animal + "呼吸水");  
        }else{  
            System.out.println(animal + "呼吸空气");  
        }  
    }  
}  

public class Client{  
    public static void main(String[] args){  
        Animal animal = new Animal();  
        animal.breathe("牛");  
        animal.breathe("羊");  
        animal.breathe("猪");  
        animal.breathe("鱼");  
    }  
}

可以看得出,这样的修改显然简便了许多,但是却存在着隐患,如果有一天有需要加入某类动物不需要呼吸,那么就要修改 Animal 类的 breathe 方法,而对原有代码的修改可能会对其他相关功能带来风险,也许有一天你会发现输出结果变成了:“牛呼吸水” 了,这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却最大的。

另外还有一种修改方式:

class Animal{  
    public void breathe(String animal){  
        System.out.println(animal + "呼吸空气");  
    }  
  
    public void breathe2(String animal){  
        System.out.println(animal + "呼吸水");  
    }  
}  
  
public class Client{  
    public static void main(String[] args){  
        Animal animal = new Animal();  
        animal.breathe("牛");  
        animal.breathe("羊");  
        animal.breathe("猪");  
        animal.breathe2("鱼");  
    }  
}  

可以看出,这种修改方式没有改动原来的代码,而是在类中新加了一个方法,这样虽然违背了单一职责原则,但是它并没有修改原来已存在的代码,不会对原本已存在的功能造成影响。

那么在实际编程中,需要根据实际情况来确定使用哪种方式,只有逻辑足够简单,才可以在代码级别上违背单一职责原则。

总结:

  1. SRP 是一个简单又直观的原则,但是在实际编码的过程中很难将它恰当地运用,需要结合实际情况进行运用。
  2. 单一职责原则可以降低类的复杂度,一个类仅负责一项职责,其逻辑肯定要比负责多项职责简单。
  3. 提高了代码的可读性,提高系统的可维护性。
开放-关闭原则
  • 开放关闭原则的含义是,对拓展开放,对修改关闭,就是可以拓展存在的系统,增加新的功能,而不修改已经写好的代码
  • 对使用方(具体功能调用者)的修改关闭, 对提供方(提供新功能的,比如对接口新的实现类)的功能扩展开放
  1. 能够扩展已存在的系统,能够提供新的功能满足新的需求,因此该软件有着很强的适应性和灵活性。
  2. 已存在的模块,特别是那些重要的抽象模块,不需要被修改,那么该软件就有很强的稳定性和持久性。
例子:此例子是通过接口实现的功能拓展,开放关闭原则不只是只有这一种实现方法,主要说的是设计实现,实现形式多种多样,遵循了这样的原则我们不会修改原先的代码,也就不会因为修改代码而带来新的bug

以下是一个违背 OCP 的例子。 它实现了绘制各种图形的 graph editor。 显然,GraphicEditor 没有遵循 OCP ,因为每次添加一个 shape 子类,都要对其进行修改。这有以下几个缺点:

  • 每次添加新的 shape 子类, GraphicEditor 的单元测试都要重新做
  • 每次添加新的 shape 子类所需时间都比较多,因为开发人员需要理解 GraphicEditor 的内部逻辑
  • 尽管新添加的 shape 能很好的工作,但它可能会意外地影响现有功能
// Open-Close Principle - Bad Example
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(Rectangle r){...}
    public void drawCircle(Circle r){...}
}

class Shape{
    int m_type;
}

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

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

以下是一个遵循 OCP 要求的例子。 在新的设计中,我们在 GraphicEditor 中使用抽象的 draw() 方法,而将具体实现移至派生类实例中。通过使用 OCP ,我们就避免了上述例子中的问题,当添加新 shape 子类的时候, 不需要对GraphicEditor 进行修改。

  • 不需要额外的单元测试
  • 不需要知晓 GraphicEditor 的源码
  • 由于具体的绘图代码移到具体的 shape 实现子类中,新功能添加进来的时候免除了影响有功能的风险
// Open-Close Principle - Good example
class GraphicEditor{
    public void drawShape(Shape s){
        s.draw();
    }
}

class Shape{
    abstract void draw();
}

class Rectangle extends Shape{
    @Override
    public void draw(){
        // draw Rectangle
    }
}

class Circle extends Shape{
    @Override
    public void draw(){
        // draw Circle;
    }
}
里氏替换原则
  • 里氏替换原则主要指的是与继承有关的一个原则,就是如果我们使用了继承,就尽量不要去修改我们父类中的内容,也就是在某段代码中调用父类的方法能够起作用,我们将父类可以替换成子类,同时它也是开闭原则的一种实现形式,不修改父类中的方法,但是可以对父类中没有的功能,通过子类进行拓展。
例子:里氏替换原则在“几维鸟不是鸟”实例中的应用。

分析:鸟一般都会飞行,如燕子的飞行速度大概是每小时 120 千米。但是新西兰的几维鸟由于翅膀退化无法飞行。假如要设计一个实例,计算这两种鸟飞行 300 千米要花费的时间。显然,拿燕子来测试这段代码,结果正确,能计算出所需要的时间;但拿几维鸟来测试,结果会发生“除零异常”或是“无穷大”,明显不符合预期,其类图如图 1 所示。
在这里插入图片描述

图1 “几维鸟不是鸟”实例的类图

程序代码如下:

package principle;

public class LSPtest {
    public static void main(String[] args) {
        Bird bird1 = new Swallow();
        Bird bird2 = new BrownKiwi();
        bird1.setSpeed(120);
        bird2.setSpeed(120);
        System.out.println("如果飞行300公里:");
        try {
            System.out.println("燕子将飞行" + bird1.getFlyTime(300) + "小时.");
            System.out.println("几维鸟将飞行" + bird2.getFlyTime(300) + "小时。");
        } catch (Exception err) {
            System.out.println("发生错误了!");
        }
    }
}

//鸟类
class Bird {
    double flySpeed;

    public void setSpeed(double speed) {
        flySpeed = speed;
    }

    public double getFlyTime(double distance) {
        return (distance / flySpeed);
    }
}

//燕子类
class Swallow extends Bird {
}

//几维鸟类
class BrownKiwi extends Bird {//继承了父类,但是却修改了父类中的方法,而我们还以为我们调用的是父类的方法,成功的给它赋了速度
    public void setSpeed(double speed) {
        flySpeed = 0;
    }
}

程序的运行结果如下:

如果飞行300公里:
燕子将飞行2.5小时.
几维鸟将飞行Infinity小时。

程序运行错误的原因是:几维鸟类重写了鸟类的 setSpeed(double speed) 方法,这违背了里氏替换原则。正确的做法是:取消几维鸟原来的继承关系,定义鸟和几维鸟的更一般的父类,如动物类,它们都有奔跑的能力。几维鸟的飞行速度虽然为 0,但奔跑速度不为 0,可以计算出其奔跑 300 千米所要花费的时间。其类图如图 2 所示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WLtQxEsI-1663996041497)(/Users/liuzhendong/Library/Application Support/typora-user-images/image-20211227200728366.png)]

图2 “几维鸟是动物”实例的类图

依赖倒转原则
  • 依赖倒转原则要求我们尽可能的依赖抽象类或者接口,而不要依赖于某个具体的类,同时依赖倒转原则也是开闭原则的一种重要实现形式,开放关闭原则的例子就是同时采用了依赖倒转原则
  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象(High level modules shouldnot depend upon low level modules.Both should depend upon abstractions.Abstractions should not depend upon details. Details should depend upon abstractions)。其核心思想是:要面向接口编程,不要面向实现编程(调用接口方法,而不是具体类的方法)。

依赖倒置原则的目的是通过要面向接口的编程来降低类间的耦合性,所以我们在实际编程中只要遵循以下4点,就能在项目中满足这个规则。

  1. 每个类尽量提供接口或抽象类,或者两者都具备。
  2. 变量的声明类型尽量是接口或者是抽象类。
  3. 任何类都不应该从具体类派生。
  4. 使用继承时尽量遵循里氏替换原则。

例1】依赖倒置原则在“顾客购物程序”中的应用。

分析:本程序反映了 “顾客类”与“商店类”的关系。商店类中有 sell() 方法,顾客类通过该方法购物以下代码定义了顾客类通过韶关网店 ShaoguanShop 购物:

class Customer {
    public void shopping(ShaoguanShop shop) {
        //购物
        System.out.println(shop.sell());
    }
}

但是,这种设计存在缺点,如果该顾客想从另外一家商店(如婺源网店 WuyuanShop)购物,就要将该顾客的代码修改如下:

class Customer {
    public void shopping(WuyuanShop shop) {
        //购物
        System.out.println(shop.sell());
    }
}

顾客每更换一家商店,都要修改一次代码,这明显违背了开闭原则。存在以上缺点的原因是:顾客类设计时同具体的商店类绑定了,这违背了依赖倒置原则。解决方法是:定义“婺源网店”和“韶关网店”的共同接口 Shop,顾客类面向该接口编程,其代码修改如下:

class Customer {
    public void shopping(Shop shop) {
        //购物
        System.out.println(shop.sell());
    }
}

这样,不管顾客类 Customer 访问什么商店,或者增加新的商店,都不需要修改原有代码了,其类图如图 1 所示。
在这里插入图片描述

图1 顾客购物程序的类图
程序代码如下:

package principle;

public class DIPtest {
    public static void main(String[] args) {
        Customer wang = new Customer();
        System.out.println("顾客购买以下商品:");
        wang.shopping(new ShaoguanShop());
        wang.shopping(new WuyuanShop());
    }
}

//商店
interface Shop {
    public String sell(); //卖
}

//韶关网店
class ShaoguanShop implements Shop {
    public String sell() {
        return "韶关土特产:香菇、木耳……";
    }
}

//婺源网店
class WuyuanShop implements Shop {
    public String sell() {
        return "婺源土特产:绿茶、酒糟鱼……";
    }
}

//顾客
class Customer {
    public void shopping(Shop shop) {
        //购物
        System.out.println(shop.sell());
    }
}

程序的运行结果如下:

顾客购买以下商品:
韶关土特产:香菇、木耳……
婺源土特产:绿茶、酒糟鱼……
接口隔离原则
  • 接口隔离原则描述的是,如何定义接口的规范,如果一个某个类只使用到了某个接口里面的部分方法,那么就没有必要实现这个接口的所有方法,而是应该对接口进行拆分,也就是如何定义接口的规范。也称为最小接口原则。

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

解决方案:将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则。

举例来说明接口隔离原则:
在这里插入图片描述

这个图的意思是:类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法。对类图不熟悉的可以参照程序代码来理解,代码如下:
interface I {
	public void method1();
	public void method2();
	public void method3();
	public void method4();
	public void method5();
}
 
class A{
	public void depend1(I i){
		i.method1();
	}
	public void depend2(I i){
		i.method2();
	}
	public void depend3(I i){
		i.method3();
	}
}
 
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() {}
}
 
class C{
	public void depend1(I i){
		i.method1();
	}
	public void depend2(I i){
		i.method4();
	}
	public void depend3(I i){
		i.method5();
	}
}
 
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 Client{
	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());
	}
}
  

在这里插入图片描述

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");
	}
}
迪米特原则
  • 迪米特原则是使一个对象尽可能的减少与其他类之间的联系,迪米特法则又叫最少知道原则

  • 通俗的来讲,就是一个类对自己依赖的类知道的越少越好。也就是说,对于被依赖的类来说,无论逻辑多么复杂,都尽量地的将逻辑封装在类的内部,对外除了提供的public方法,不对外泄漏任何信息。(此时我们只知道一个方法,至于方法内部调用了什么函数我们不用知道)迪米特法则还有一个更简单的定义:只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。

  • 举一个例子:有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看一下违反迪米特法则的设计。

    //总公司员工
    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());
    		}
        
        //这里的代码就是我们使用了Employee做为局部变量,并了解到了其具有id的属性,因为我们调用了这个方法
    		List<Employee> list2 = this.getAllEmployee();
    		for(Employee e:list2){
    			System.out.println(e.getId());
    		}
    	}
    }
     
    public class Client{
    	public static void main(String[] args){
    		CompanyManager e = new CompanyManager();
    		e.printAllEmployee(new SubCompanyManager());
    	}
    }
    
  • 现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而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> list2 = this.getAllEmployee();
    		for(Employee e:list2){
    			System.out.println(e.getId());
    		}
    	}
    }
    

通过将间接朋友变成了直接朋友,从而减少了代码之间的相关性。当然关系不可能完全消失,这边编写的好处是方便拓展,我们只需要知道暴露出来的接口或者方法能否完成我们的功能就行。

组合/聚合复用原则
  • 这个原则的简短表述就是:要尽量使用组合,尽量不要使用继承。继承是联系最紧密的类与类的一种关系,而我们要的是低耦合,所以需要尽可能的使用组合代替使用继承,当然如果遵循里氏替换原则使用继承也是没有问题的,也就是需要根据类与类之间的关系来选择使用继承还是聚合。就如同恋爱中处于什么关系,相处的方式也不一样。

  • 举个例子说明为什么继承比组合联系更加紧密,亲密。继承是可以拥有父类的所有方法和属性,而组合只是自身包含一个对象,我们可以通过这个对象调用这个类的方法,和使用这个类的属性,是借用的某个对象来使用的。继承就如同父亲和我们的关系一样,如果父亲有一辆车(等同于车就是我们的),我们可以直接去拿钥匙去开着出行,而组合就如同你有一个亲戚,你知道它有一辆车,而你需要出行,可能需要亲戚开车来送你出行。

    • 继承:在我们想复用代码时,我们一般会优先想到继承,但是具有继承关系的两个类是耦合度最高的两个类。(父类改了子类可能会受影响,子类改了父类也可能会受影响)如果父类的功能比较稳定,建议使用继承来实现代码复用,因为继承是静态定义的,在运行时无法动态调用。

    • 组合:是整体与部分的关系,整体离不开部分,部分离开了整体没有意义,如飞机翅膀与飞机的关系。

    • 聚合:也是整体与部分的关系,但整体可以分离部分,部分也可以离开整体,如火车与车厢的关系。
      组合/聚合:是通过获得其他对象的引用,在运行时刻动态定义的,也就是在一个对象中保存其他对象的属性,这种方式要求对象有良好定义的接口,并且这个接口也不经常发生改变,而且对象只能通过接口来访问,这样我们并不破坏封装性,所以只要类型一致,运行时还可以通过一个对象替换另外一个对象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值