第一章设计模式前置知识

news2024/9/21 20:47:11

文章目录

  • 软件设计模式的概念
  • 学习设计模式的必要性
  • 设计模式分类
  • UML图
    • 类的表述方式
      • 类的表示方式
    • 类与类之间的表示方式
      • 关联关系
      • 聚合关系
      • 组合关系
      • 依赖关系
      • 继承关系
      • 实现关系
  • 软件设计原则
    • 开闭原则
      • 实例
    • 里氏代换原则
      • 反例
      • 优化
    • 依赖倒转原则
      • 组装电脑
      • 改进反例
    • 接口隔离原则
      • 安全门案例
    • 迪米特法则
      • 明星与经纪人的关系实例
    • 合成复用原则
      • 继承复用示例
      • 合成复用示例

软件设计模式的概念

软件设计模式(Software Design Pattern),又称设计模式,是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。它描述了在软件设计过程中的一些不断重复发生的问题,以及该问题的解决方案。也就是说,它是解决特定问题的一系列套路,是前辈们的代码设计经验的总结,具有一定的普遍性,可以反复使用

学习设计模式的必要性

设计模式的本质是面向对象设计原则的实际运用,是对类的封装性继承性多态性以及类的关联关系和组合关系的充分理解。
正确使用设计模式具有以下优点。

  • 可以提高程序员的思维能力、编程能力和设计能力。
  • 使程序设计更加标准化、代码编制更加工程化,使软件开发效率大大提高,从而缩短软件的开发周期。
  • 使设计的代码可重用性高、可读性强、可靠性高、灵活性好、可维护性强。

设计模式分类

创建型模式

  • 用于描述“怎样创建对象”,它的主要特点是“将对象的创建与使用分离”。GoF(四人组)书中提供了单例、原型、工厂方法、抽象工厂、建造者等 5 种创建型模式。

结构型模式

  • 用于描述如何将类或对象按某种布局组成更大的结构,GoF(四人组)书中提供了代理、适配器、桥接、装饰、外观、享元、组合等 7 种结构型模式。
    • 类比建房子,创建型模式就是让我们得到我们的建筑材料,而结构性模式则是将这些材料组成房子的结构

行为型模式

  • 用于描述类或对象之间怎样相互协作共同完成单个对象无法单独完成的任务,以及怎样分配职责。
  • GoF(四人组)书中提供了模板方法、策略、命令、职责链、状态、观察者、中介者、迭代器、访问者、备忘录、解释器等 11 种行为型模式。

UML图

统一建模语言(Unified Modeling Language,UML)是用来设计软件的可视化建模语言。它的特点是简单、统一、图形化、能表达软件设计中的动态与静态信息。
UML 从目标系统的不同角度出发,定义了用例图、类图、对象图、状态图、活动图、时序图、协作图、构件图、部署图等 9 种图。

类的表述方式

类图(Class diagram)是显示了模型的静态结构,特别是模型中存在的类、类的内部结构以及它们与其他类的关系等。类图不显示暂时性的信息。类图是面向对象建模的主要组成部分。

类图的作用
在软件工程中,类图是一种静态的结构图,描述了系统的类的集合,类的属性和类之间的关系,可以简化了人们对系统的理解;类图是系统分析和设计阶段的重要产物,是系统编码和测试的重要模型。

类的表示方式

在UML类图中,类使用包含类名、属性(field) 和方法(method) 且带有分割线的矩形来表示,比如下图表示一个Employee类,它包含name,age和address这3个属性,以及work()方法。

image-20230425162443833

属性/方法名称前加的加号和减号表示了这个属性/方法的可见性,UML类图中表示可见性的符号有三种:

  • +:表示public
  • -:表示private
  • #:表示protected

属性的完整表示方式是: 可见性 名称 :类型 [ = 缺省值]

方法的完整表示方式是: 可见性 名称(参数列表) [ : 返回类型]

  • 中括号中的内容表示是可选的
  • 也有将类型放在变量名前面,返回值类型放在方法名前面

例子

image-20230425162457564

上图Demo类定义了三个方法:

  • method()方法:修饰符为public,没有参数,没有返回值。
  • method1()方法:修饰符为private,没有参数,返回值类型为String。
  • method2()方法:修饰符为protected,接收两个参数,第一个参数类型为int,第二个参数类型为String,返回值类型是int。

类与类之间的表示方式

关联关系

关联关系是对象之间的一种引用关系,用于表示一类对象与另一类对象之间的联系,如老师和学生、师傅和徒弟、丈夫和妻子等。关联关系是类与类之间最常用的一种关系,分为一般关联关系、聚合关系和组合关系。

单向关联

image-20230425162511881

  • 在UML类图中单向关联用一个带箭头的实线表示。上图表示每个顾客都有一个地址,这通过让Customer类持有一个类型为Address的成员变量类实现

双向关联

image-20230425162529015

  • 从上图中我们很容易看出,所谓的双向关联就是双方各自持有对方类型的成员变量。在UML类图中,双向关联用一个不带箭头的直线表示。上图中在Customer类中维护一个List,表示一个顾客可以购买多个商品;在Product类中维护一个Customer类型的成员变量表示这个产品被哪个顾客所购买

自关联

image-20230425162554461

  • 自关联在UML类图中用一个带有箭头且指向自身的线表示。上图的意思就是Node类包含类型为Node的成员变量,也就是“自己包含自己”。

聚合关系

image-20230425162610788

  • 聚合关系是关联关系的一种,是强关联关系,是整体和部分之间的关系。
  • 聚合关系也是通过成员对象来实现的,其中成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师的关系,学校包含老师,但如果学校停办了,老师依然存在。
  • 在 UML 类图中,聚合关系可以用带空心菱形的实线来表示,菱形指向整体

组合关系

image-20230425162626750

  • 组合表示类之间的整体与部分的关系,但它是一种更强烈的聚合关系。

  • 在组合关系中,整体对象可以控制部分对象的生命周期,一旦整体对象不存在,部分对象也将不存在,部分对象不能脱离整体对象而存在。例如,头和嘴的关系,没有了头,嘴也就不存在了。

  • 在 UML 类图中,组合关系用带实心菱形的实线来表示,菱形指向整体。

依赖关系

image-20230425162637751

  • 依赖关系是一种使用关系,它是对象之间耦合度最弱的一种关联方式,是临时性的关联。在代码中,某个类的方法通过局部变量方法的参数或者对静态方法的调用来访问另一个类(被依赖类)中的某些方法来完成一些职责。

  • 在 UML 类图中,依赖关系使用带箭头的虚线来表示,箭头从使用类指向被依赖的类。

  • 所示是司机和汽车的关系图,司机驾驶汽车

继承关系

image-20230425162652998

  • 继承关系是对象之间耦合度最大的一种关系,表示一般与特殊的关系,是父类与子类之间的关系,是一种继承关系。
  • 在 UML 类图中,泛化关系用带空心三角箭头的实线来表示,箭头从子类指向父类。在代码实现时,使用面向对象的继承机制来实现泛化关系。例如,Student 类和 Teacher 类都是 Person 类的子类

实现关系

image-20230425162706146

  • 实现关系是接口与实现类之间的关系。在这种关系中,类实现了接口,类中的操作实现了接口中所声明的所有的抽象操作。
  • 在 UML 类图中,实现关系使用带空心三角箭头的虚线来表示,箭头从实现类指向接口。例如,汽车和船实现了交通工具

软件设计原则

在软件开发中,为了提高软件系统的可维护性可复用性,增加软件的可扩展性灵活性,程序员要尽量根据 6 条原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。

  • 开闭原则:对拓展开放,对修改封闭。
  • 里式代换原则:任何基类可以出现的地方,子类一定可以出现,反之不一定。
  • 依赖倒转原则:高层模块不应该依赖低层模块,两者都应该依赖其抽象。
  • 接口隔离原则:客户端不应该被迫依赖于它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上。
  • 迪米特法则:只和你的直接朋友交谈,不跟 “陌生人” 说话(Talk only to your immediate friends and not to strangers)。
  • 合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

开闭原则

开闭原则:**对扩展开放,对修改关闭。**在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果。这是为了使程序的扩展性好,易于维护和升级。

  • 比如我们的电脑,提供对应的USB接口,想使用鼠标就插入鼠标的接头,想使用键盘,就插入键盘的接头——热插拔

想要达到这样的效果,我们需要使用接口和抽象类

  • 因为抽象灵活性好,适应性广,只要抽象的合理,可以基本保持软件架构的稳定。而软件中易变的细节可以从抽象派生来的实现类来进行扩展,当软件需要发生变化时,只需要根据需求重新派生一个实现类来扩展就可以了。

实例

搜狗输入法 的皮肤设计。

  • 分析: 搜狗输入法 的皮肤是输入法背景图片、窗口颜色和声音等元素的组合。用户可以根据自己的喜爱更换自己的输入法的皮肤,也可以从网上下载新的皮肤。这些皮肤有共同的特点,可以为其定义一个抽象类(AbstractSkin),而每个具体的皮肤(DefaultSpecificSkin和HeimaSpecificSkin)是其子类。用户窗体可以根据需要选择或者增加新的主题,而不需要修改原代码,所以它是满足开闭原则的。

image-20230425160441970

AbstractSkin

/**
 * 抽象皮肤类
 */
public abstract class AbstractSkin {
    public abstract void display();
}

DefaultSkin

/*
 *默认皮肤类
*/
public class DefaultSkin extends AbstractSkin{
    @Override
    public void display() {
        System.out.println("默认皮肤类");
    }
}

LscSkin

/*
 *Lsc的自定义皮肤类
 */
public class LscSkin extends  AbstractSkin{
    @Override
    public void display() {
        System.out.println("lsc的定制化皮肤");
    }
}

SouGouInput

/*
* 搜狗输入
*/
public class SouGouInput {
    private AbstractSkin skin;

    public void setSkin(AbstractSkin skin) {
        this.skin = skin;
    }
    public void  display(){
        skin.display();
    }
}

测试类

public class Client {
    public static void main(String[] args) {
        //1创建搜狗输入法对象
        SouGouInput input=new SouGouInput();
        //2默认创建皮肤对象
        DefaultSkin skin=new DefaultSkin();
//        LscSkin skin1=new LscSkin();
        //3将皮肤设置到输入法中
        input.setSkin(skin);
//        input.setSkin(skin1);
        //4显示皮肤
        input.display();
    }
}
  • 结果输出——默认皮肤类
  • 满足了我们的开闭原则,如果我们下次想加一个黑马的皮肤,我们直接创建一个子类实现AbstractSkin抽象类,然后赋值给我们的SouGouInput,我们增加了代码,但是并没有修改原有的代码

里氏代换原则

里氏代换原则是面向对象设计的基本原则之一。

  • 里氏代换原则:任何基类可以出现的地方,子类一定可以出现,反之不一定。

  • 子类可以扩展父类的功能,但不能改变父类原有的功能。换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。

  • 如果必须重写,更适合在父类中定义成抽象方法

  • 如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

反例

正方形不是长方形。

在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,我们开发的一个与几何图形相关的软件系统,就可以顺理成章的让正方形继承自长方形。

image-20230425170923535

/**
*长方形类
 */
@Data
public class Rectangle {
    private double length;
    private double width;
}

/**
 * 正方形类
 */
public class Square  extends  Rectangle{
    public void setWidth(double width){
        super.setLength(width);
        super.setWidth(width);
    }
    public void setLength(double length) {
        super.setLength(length);
        super.setWidth(length);
    }
}

public class RectangleDemo {
    public static void main(String[] args) {
        // 创建长方形对象
        Rectangle r = new Rectangle();
        r.setLength(20);
        r.setWidth(10);
        resize(r);
        printLengthAndWidth(r);
        System.out.println("=====================");
        // 创建正方形对象
        Square s = new Square();
        s.setLength(10);
        resize(s);
        printLengthAndWidth(s);
    }
    // 拓宽方法
    public static void resize(Rectangle rectangle) {
        while (rectangle.getWidth() <= rectangle.getLength()) {
            rectangle.setWidth(rectangle.getWidth() + 1);
        }
    }

    // 打印长方形的长和宽
    public static void printLengthAndWidth(Rectangle rectangle) {
        System.out.println(rectangle.getLength());
        System.out.println(rectangle.getWidth());
    }
}

运行这段代码会发现:

  • 假如我们把一个普通长方形作为参数传入 resize 方法,就会看到长方形宽度逐渐增长的效果,当宽度大于长度,代码就会停止,这种行为的结果符合我们的预期

  • 假如我们再把一个正方形作为参数传入 resize 方法后,就会看到正方形的宽度和长度都在不断增长,代码会一直运行下去,直至系统产生溢出错误。

    • 所以,普通的长方形是适合这段代码的,正方形不适合。
  • 我们得出结论:在 resize 方法中,Rectangle 类型的参数不能被 Square 类型的参数所代替,如果进行了替换就得不到预期结果。

  • 因此,Square 类和 Rectangle 类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。

优化

如何改进呢?此时我们需要重新设计他们之间的关系。抽象出来一个四边形接口(Quadrilateral),让Rectangle类和Square类实现Quadrilateral接口

image-20230425173042104

  • 我们的resize只能出现的是长方形
  • printLengthAndWidth出现的只能四边形接口 Quadrilateral
    • 满足了任何基类可以出现的地方,子类一定可以出现,反之不一定。
public interface Quadrilateral {
    double getLength();
    double getWidth();
}

@Setter
public class Rectangle implements Quadrilateral {
    private double length;
    private double width;

    @Override
    public double getLength() {
        return length;
    }

    @Override
    public double getWidth() {
        return width;
    }
}
@Data
public class Square implements Quadrilateral {
    private double side;

    @Override
    public double getLength() {
        return side;
    }

    @Override
    public double getWidth() {
        return side;
    }
}
public class RectangleDemo {
    public static void main(String[] args) {
        // 创建长方形对象
        Rectangle r = new Rectangle();
        r.setLength(20);
        r.setWidth(10);
        // 调用方法进行扩宽操作
        resize(r);
        printLengthAndWidth(r);
    }

    // 扩宽的方法
    public static void resize(Rectangle rectangle) {
        // 判断宽如果比长小,进行扩宽的操作
        while(rectangle.getWidth() <= rectangle.getLength()) {
            rectangle.setWidth(rectangle.getWidth() + 1);
        }
    }

    // 打印长和宽
    public static void printLengthAndWidth(Quadrilateral quadrilateral) {
        System.out.println(quadrilateral.getLength());
        System.out.println(quadrilateral.getWidth());
    }
}

依赖倒转原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象

  • A的成员变量中有B对象,那么A是高层模块,B的低层模块
  • 这里的依赖是泛指组合和聚合

抽象不应该依赖细节,细节应该依赖抽象。简单的说就是要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。

组装电脑

现要组装一台电脑,需要配件cpu,硬盘,内存条。只有这些配置都有了,计算机才能正常的运行。选择cpu有很多选择,如Intel,AMD等,硬盘可以选择希捷,西数等,内存条可以选择金士顿,海盗船等

image-20230426091751134

希捷硬盘类(XiJieHardDisk):

public class XiJieHardDisk implements HardDisk {

    public void save(String data) {
        System.out.println("使用希捷硬盘存储数据" + data);
    }

    public String get() {
        System.out.println("使用希捷希捷硬盘取数据");
        return "数据";
    }
}

Intel处理器(IntelCpu):

public class IntelCpu implements Cpu {

    public void run() {
        System.out.println("使用Intel处理器");
    }
}

金士顿内存条(KingstonMemory):

public class KingstonMemory implements Memory {

    public void save() {
        System.out.println("使用金士顿作为内存条");
    }
}

电脑(Computer):

/**
 * @author lsc07
 */
@Data
public class Computer {
    private XiJieHardDisk hardDisk;
    private IntelCpu cpu;
    private KingstonMemory memory;
    public void run(){
        System.out.println("计算机工作");
        cpu.run();
        memory.save();
        String data=hardDisk.get();
        System.out.println("从硬盘中获取的数据为:" + data);
    }
}

测试类(TestComputer):

测试类用来组装电脑。

public class TestComputer {
    public static void main(String[] args) {
        Computer computer = new Computer();
        computer.setHardDisk(new XiJieHardDisk());
        computer.setCpu(new IntelCpu());
        computer.setMemory(new KingstonMemory());

        computer.run();
    }
}

上面代码可以看到已经组装了一台电脑,但是似乎组装的电脑的cpu只能是Intel的,内存条只能是金士顿的,硬盘只能是希捷的,这对用户肯定是不友好的,用户有了机箱肯定是想按照自己的喜好,选择自己喜欢的配件。

改进反例

代码我们只需要修改Computer类,让Computer类依赖抽象(各个配件的接口),而不是依赖于各个组件具体的实现类。

image-20230426094903130

将硬盘、Cpu、内存抽取出接口

/**
* 硬盘接口
 */
   public interface HardDisk {
   public void save(String data);
   public String get();
   }
/**
	* Cpu接口
 */
   public interface Cpu {
   public void run();
   }

/**
 * 内存条接口
 */
   public interface Memory {
   		public void save();
   }

具体型号的配件实现其接口

/**
 * 希捷硬盘
 */
public class XiJieHardDisk implements HardDisk {
    public void save(String data) {
        System.out.println("使用希捷硬盘存储数据为:" + data);
    }
    public String get() {
        System.out.println("使用希捷希捷硬盘取数据");
        return "数据";
    }
}

/**
 * Intel cpu
 */
public class IntelCpu implements Cpu {
    public void run() {
        System.out.println("使用Intel处理器");
    }
}

/**
 * 金士顿内存条
 */
public class KingstonMemory implements Memory {
    public void save() {
        System.out.println("使用金士顿内存条");
    }
}

组装电脑的时候,利用接口来实现

@Data
public class Computer {
   private HardDisk hardDisk;
   private Cpu cpu;
   private Memory memory;

   //运行计算机
   public void run() {
       System.out.println("运行计算机");
       String data = hardDisk.get();
       System.out.println("从硬盘上获取的数据是:" + data);
       cpu.run();
       memory.save();
   }
 }

在使用的时候,如果需要组装不同的组件,就不需要去修改 Computer 类,只需要创建新的组件对象并赋给计算机对象

public class ComputerDemo {
    public static void main(String[] args) {
        // 创建计算机的组件对象
        HardDisk hardDisk = new XiJieHardDisk();
        Cpu cpu = new IntelCpu();
        Memory memory = new KingstonMemory();
        // 创建计算机对象
        Computer c = new Computer();
        // 组装计算机
        c.setCpu(cpu);
        c.setHardDisk(hardDisk);
        c.setMemory(memory);
        // 运行计算机
        c.run();
    }
}
  • 面向对象的开发很好的解决了这个问题,一般情况下抽象的变化概率很小,让用户程序依赖于抽象,实现的细节也依赖于抽象。即使实现细节不断变动,只要抽象不变,客户程序就不需要变化。

  • 以上代码符合依赖倒转原则,大大降低了程序与实现细节的耦合度。

多数情况下,以上三个设计原则会同时出现:

开闭原则是目标,里式代换原则是基础,依赖倒转原则是手段,它们相辅相成,相互补充,目标一致。

接口隔离原则

客户端不应该被迫依赖于它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上。

安全门案例

我们需要创建一个黑马品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。

image-20230426102948915

/**
*安全门接口
*/
public interface SafetyDoor {
// 防盗
void antiTheft();
// 防火
void fireProof();
// 防水
void waterProof();
}

/**
	*黑马品牌的安全门
*/
public class HeimaSafetyDoor implements SafetyDoor {
public void antiTheft() {
    System.out.println("防盗");
}
public void fireProof() {
    System.out.println("防火");
}
public void waterProof() {
    System.out.println("防水");
}
}

public class Client {
public static void main(String[] args) {
    HeimaSafetyDoor door = new HeimaSafetyDoor();
    door.antiTheft();
    door.fireProof();
    door.waterProof();
}
}

上面的设计我们发现了它存在的问题,黑马品牌的安全门具有防盗,防水,防火的功能。

现在如果我们还需要再创建一个传智品牌的安全门,而该安全门只具有防盗、防水功能呢?很显然如果实现 SafetyDoor 接口就违背了接口隔离原则

  • 对于一个类(接口)而言,只有一个引起它变化的原因
  • 比如我们的防盗功能,只有关于防盗方面的改变才能引起这个接口的改变,而不是像上面一样,防盗和防水或者防火的改变都会引起其改变

上面的设计我们发现了它存在的问题,黑马品牌的安全门具有防盗,防水,防火的功能。现在如果我们还需要再创建一个传智品牌的安全门,而该安全门只具有防盗、防水功能呢?很显然如果实现SafetyDoor接口就违背了接口隔离原则,那么我们如何进行修改呢?看如下类图

image-20230426103638185

将各个功能抽成单独的接口

/**
 * 防盗接口
 */
public interface AntiTheft {
    void antiTheft();
}

/**
 * 防火接口
 */
public interface Fireproof {
    void fireproof();
}

/**
 * 防水接口
 */
public interface Waterproof {
    void waterproof();
}

当前有个黑马防盗门,它可以实现防盗、防火、防水功能:

/**
	*HeiMaSafetyDoor
*/
public class HeiMaSafetyDoor implements AntiTheft, Fireproof, Waterproof {
	public void antiTheft() {
    	System.out.println("防盗");
	}
	public void fireproof() {
    	System.out.println("防火");
	}
	public void waterproof() {
    	System.out.println("防水");
    }
}

如果此时还需要新增一个传智防盗门,它只有防盗、防火功能:

/**
	*传智安全门
*/
public class ItcastSafetyDoor implements AntiTheft, Fireproof {
	public void antiTheft() {
    	System.out.println("防盗");
	}
	public void fireproof() {
    	System.out.println("防火");
	}
}

测试类:

public class Client {
    public static void main(String[] args) {
        // 创建黑马安全门对象
        HeimaSafetyDoor door = new HeimaSafetyDoor();
        // 调用功能
        door.antiTheft();
        door.fireProof();
        door.waterProof();
      

    // 创建传智安全门对象
    ItcastSafetyDoor door1 = new ItcastSafetyDoor();
    //调用功能
    door1.antiTheft();
    door1.fireproof();
	}
}

以上代码实现了接口隔离原则,没有强迫客户端去依赖它不使用的方法。

迪米特法则

  • 迪米特法则又叫最少知识原则。

  • 迪米特法则:只和你的直接朋友交谈,不跟“陌生人”说话(Talk only to your immediate friends and not to strangers)。

  • 其含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。

  • 一个类对于其他类知道的越少越好

    • 朋友:有耦合(依赖,关联,组合,聚合)关系的对象,可以直接访问这些对象的方法。
    • 直接的朋友:成员变量,方法参数,方法返回值的中类
    • 当前对象本身当前对象的成员对象当前对象所创建的对象当前对象的方法参数等
  • 其目的是:降低类之间的耦合度,提高模块的相对独立性。

明星与经纪人的关系实例

明星由于全身心投入艺术,所以许多日常事务由经纪人负责处理,如和粉丝的见面会,和媒体公司的业务洽淡等。这里经纪人是明星的朋友,而粉丝和媒体公司是陌生人,所以适合使用迪米特法则。

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

image-20230426112942988

/**
 * 明星类
 * @author lsc07
 */
@Getter
public class Star {
    private String name;
    public Star(String name) {
        this.name = name;
    }
}

/**
 * 粉丝类
 * @author lsc07
 */
@Getter
public class Fans {
    private String name;
    public Fans(String name) {
        this.name = name;
    }
}

/**
 * 媒体公司类
 * @author lsc07
 */
@Getter
public class Company {
    private String name;
    public Company(String name) {
        this.name = name;
    }
}
/**
 * 经纪人类
 * @author lsc07
 */
@Data
public class Agent {
    private Star star;
    private Fans fans;
    private Company company;

    public void meeting() {
        System.out.println(star.getName() + "和粉丝" + fans.getName() + "见面");
    }

    public void business() {
        System.out.println(star.getName() + "和" + company.getName() + "洽谈");
    }
}

合成复用原则

合成复用原则:尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

  • 通常类的复用分为:继承复用和合成复用两种。

继承复用虽然有简单和易实现的优点,但它也存在以下缺点:

  • 继承复用破坏了类的封装性。因为继承会将父类的实现细节暴露给子类,父类对子类是透明的,所以这种复用又称为“白箱”复用。

  • 子类与父类的耦合度高。父类的实现的任何改变都会导致子类的实现发生变化,这不利于类的扩展与维护。

  • 它限制了复用的灵活性。从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。

采用组合或聚合复用时,可以将已有对象纳入新对象中,使之成为新对象的一部分,新对象可以调用已有对象的功能,它有以下优点:

  • 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用。
  • 对象间的耦合度低。可以在类的成员位置声明抽象
  • 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

继承复用示例

汽车按 “动力源” 划分可分为汽油汽车、电动汽车等;按 “颜色” 划分可分为白色汽车、黑色汽车和红色汽车等。

如果同时考虑这两种分类,其组合就很多。类图如下:

image-20230426140450735

从上面类图我们可以看到使用继承复用产生了很多子类,如果现在又有新的动力源或者新的颜色的话,就需要再定义新的类。

合成复用示例

我们试着将继承复用改为聚合复用,类图如下:

image-20230426140524845

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/465575.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

1分钟学会Midjourney十种绘图风格关键词

Midjourney最新V5版的卡通模型中最流行的就是皮克斯&#xff0c;今天介绍十种绘图风格。我们统一用如下描述词来绘制&#xff0c;每次只是风格不一样&#xff0c;对比看看。 首先我们先画一个皮克斯风格(Pixar)&#xff0c;打开ai绘图软件&#xff0c;点击左上角的图像绘制&a…

centos主机测磁盘读写速度极限

下面将使用dd命令在CentOS主机上测试磁盘的极限速度 先测试磁盘的极限写入速度 dd if/dev/zero of/tmp/testfile bs1M count3072 convfdatasync,notrunc statusprogress 该命令将在/tmp目录下创建一个名为testfile的文件&#xff0c;并向其中写入3GB的数据 if/dev/zero&#x…

【算法】【算法杂谈】折纸问题(完全二叉树解法)

目录 前言问题介绍解决方案代码编写java语言版本c语言版本c语言版本 思考感悟写在最后 前言 当前所有算法都使用测试用例运行过&#xff0c;但是不保证100%的测试用例&#xff0c;如果存在问题务必联系批评指正~ 在此感谢左大神让我对算法有了新的感悟认识&#xff01; 问题介…

Camunda8流程引擎私有化安装部署快速入门

Camunda8是基于标准云原生架构设计开发的&#xff0c;所以官方优先推荐基于Kubernetes和docker的方式安装部署Camunda8&#xff0c;考虑到大部分开发者不一定具备Kubernetes环境和云原生相关知识&#xff0c;所以本文介绍如何在一台windows机器下手动安装运行Camunda8的方式&am…

Linux文件操作基础及基本I/O函数使用

文章目录 前言一、基础命令简单讲解二、文件描述符三、open、read、write函数讲解1.open函数2.read函数3.write函数 四、使用open、read、write操作标准输入输出总结 前言 Linux是一个基于文件系统的操作系统&#xff0c;文件操作是其中一项最基本的任务之一。通过文件操作&am…

什么!appium安装不上???快来试试这种方法吧!——appium的手动安装步骤教程

前言 相信你不少软件测试行业小伙伴应该在用npm安装appuim或者是cpm安装appuim途中也碰到下面一些报错吧&#xff0c;接下来小陈教你改为手动安装appium吧。 一、手动下载appium安装包 appuim手动安装包下载链接&#xff1a; appium / Appium.app / Downloads — Bitbucket &a…

《Netty》从零开始学netty源码(四十四)之PoolChunk释放内存

free 当PoolChunk需要释放内存空间时可调用free方法&#xff0c;具体的源码过程如下&#xff1a; 在这个过程中最重要的是第三步的collapseRuns方法&#xff0c;当释放了空间以后要更新runsAvail和runAvailsMap的信息&#xff0c;如果handle对应的内存空间的上边界以及下边界是…

什么是AMS?什么是WMS?

AMS和WMS AMS ActivityManagerService是Android系统中一个特别重要的系统服务&#xff0c;也是上层APP打交道最多的系统服务之一&#xff08;几乎所有的APP应用都需要与AMS打交道&#xff09;。 AMS与操作系统中的进程管理和调度模块很像&#xff0c;在Android系统中非常重要…

从“恰当”的项目管理工具中,了解自己的缺点

项目管理工具是为了帮助管理者&#xff0c;但管理者需要了解自己在特定情况下的“缺点”&#xff0c;才能从“恰当”的工具中获得“恰当”的帮助。如果你不知道在某个特定项目中自己&#xff08;作为项目经理&#xff09;的缺点&#xff0c;也不知道自己需要利用哪些好用的项目…

【Python_Opencv图像处理框架】边缘检测、轮廓检测、图像金字塔

写在前面 本篇文章是opencv学习的第四篇文章&#xff0c;主要讲解了边缘及轮廓检测的主要操作&#xff0c;并对两种图像金字塔简单的介绍了一下&#xff0c;作为初学者&#xff0c;我尽己所能&#xff0c;但仍会存在疏漏的地方&#xff0c;希望各位看官不吝指正&#x1f60d; …

Kafka3.0.0版本——生产者数据有序与乱序

目录 一、生产经验数据有序二、生产经验数据乱序2.1、kafka1.x版本之前保证数据单分区有序的条件2.2、kafka1.x版本及以后保证数据单分区有序的条件2.3、kafka1.x版本及以后开启幂等性数据单分区有序的原因 一、生产经验数据有序 单分区内&#xff0c;数据有序。如下图partion…

React之购物车+动态获取参数+Hooks+Redux

1、redux-logger中间件 1️⃣&#xff1a;安装redux-logger依赖包 yarn add redux-logger 2️⃣&#xff1a;在store的配置文件index中配置 import {legacy_createStore as applyMiddleware,applyMiddleware} from reduximport counterReducer from ./counterReducerimport {c…

【论文写作】-我的第一篇论文形成记(投稿过程、课题来源、python与数学学习、实验设计、论文思路建立与写作、回复审稿人)

我的第一篇论文形成记 投稿过程背景记录课题来源-本科毕业设计python及数学学习实验设计调参阶段实验阶段 论文思路建立论文写作回复审稿人总结 投稿过程 2022年12月28日 投AIChE 2023年01月05日 AlChE编辑认为方向不太符合期刊定位&#xff0c;建议投其他期刊 2023年01月06日…

SpringCloud_服务调用OpenFeign和断路器Resilience4J

文章目录 一、负载均衡概论1、服务器负载均衡2、客户端负载均衡3、客户端负载均衡策略(SpringCloudRibbon)4、客户端负载均衡策略(SpringCloudLoadBalancer) 二、SpringCloudOpenFeign服务调用1、OpenFeign服务调用的使用2、OpenFeign服务调用的日志增强3、OpenFeign服务调用超…

Vector - CAPL - 检查DUT发出与dbc定义一致

目录 ChkCreate_UndefinedMessageReceived 代码示例 有回调函数 无回调函数 Trace报文 报告显示 ChkCreate_UndefinedMessage

一维离散小波变换原理和代码实现

基本原理&#xff1a; 离散小波变换&#xff1a;对连续小波变换的尺度因子和时移动因子采用不同的离散条件进行离散&#xff0c;得到Discrete Wavelet Transform(DWT)。降低计算量的同时&#xff0c;保持连续小波变换的光滑性、紧支性、对称性。 离散小波函数&#xff1a; ψ…

脚本函数基础

shell脚本编程系列 函数是一个脚本代码块&#xff0c;可以为其命名并在脚本中的任何位置重用它。每当需要在脚本中使用该代码块时&#xff0c;直接写函数名即可。称作调用函数。 创建函数 方式1&#xff1a; function name {commands }name定义了该函数的唯一名称&#xff0…

私有部署、重构企业软件,第四范式发布大模型“式说”

大模型领域再添重要一员&#xff01; 4月26日&#xff0c;第四范式首次向公众展示其大模型产品「式说3.0」&#xff0c;并首次提出AIGS战略&#xff08;AI-Generated Software&#xff09;&#xff1a;以生成式AI重构企业软件。式说将定位为基于多模态大模型的新型开发平台&…

ImageJ 用户手册——第五部分(菜单命令Process)

这里写目录标题 ImageJ 用户手册——第五部分29. Process29.1 Smooth29.2 Sharpen29.3 Find Edges29.4 Find Maxima29.5 Enhance Contrast29.6 Noise29.6.1 Add Noise29.6.2 Add Specified Noise29.6.3 Salt and Pepper29.6.4 Despeckle29.6.5 Remove Outliers29.6.6 Remove Na…

Shell编程条件语句 if case (愿此行终抵群星)

一、Shell条件语句 Shell环境根据命令执行后的返回状态值&#xff08;$?&#xff09;来判断是否执行成功&#xff0c;当返回值为0(真 true)时表示成功&#xff0c;返回值为非0值&#xff08;假 false)时表示失败或异常。 test 命令 更多操作可使用 man test 查看 1.条件表达式…