Java-软件设计七大原则

一、概述

软件设计原则是指在开发和设计软件时应遵循的一些基本原则和准则。为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,要尽量遵循软件设计七大原则来开发程序,从而提高软件开发效率、节约软件开发成本和维护成本。

遵循软件设计原则的优点如下:

遵循软件设计原则有以下几个重要原因:

  1. 提高代码质量:软件设计原则是一些经过验证的最佳实践,能够帮助开发人员以一种结构良好且可扩展的方式编写代码。可以提高代码的可读性、可维护性和可测试性,从而提高代码质量。

  2. 提升系统可扩展性:良好的软件设计原则可以使系统更易于扩展。通过组织代码结构、定义清晰的接口和模块化的设计,可以使新功能的添加和现有功能的修改更加简单和安全。这可以确保系统能够适应未来的需求变化,提高系统的可扩展性。

  3. 降低软件维护成本:合理的软件设计可以减少系统的复杂性和耦合度,使得对系统的修改和维护更加容易。在日常维护中,如果遵循设计原则,开发人员可以更快地理解和修改代码,从而降低了维护成本。

  4. 提高团队协作效率:软件设计原则提供了一种通用的设计语言和指导原则,可以促使开发团队成员之间更好地共享和理解设计思路。一个遵循设计原则的项目可以减少沟通和理解上的困惑,从而提高团队协作的效率。

  5. 提升软件系统的可靠性和性能:良好的软件设计原则可以降低系统中出现错误的可能性,并提高系统的稳定性和性能。通过合理地设计类之间的关系和划分模块,可以减少bug的产生,增强系统的可靠性和健壮性。

各种原则要求的侧重点不同,大致介绍如下:

1. 开闭原则:要对扩展开放,对修改关闭。
2. 里氏替换原则:不要破坏继承体系。
3. 依赖倒置原则:要依赖抽象接口编程。
4. 单一职责原则:实现类要职责单一。
5. 接口隔离原则:设计接口的时候要精简单一。
6. 迪米特法则:要降低耦合度。
7. 合成复用原则:优先使用组合或者聚合关系复用,少用继承关系复用。


二、开闭原则

2.1 介绍

        当应用的需求改变时,在不修改软件实体的源代码或者二进制代码的前提下,可以扩展模块的功能,使其满足新的需求。
        一个常见的做法是使用抽象和接口来实现开放封闭原则:我们定义接口和抽象类来表示一组功能,在具体实现类中实现具体的细节。当需要添加新的功能时,我们可以通过实现接口或者扩展抽象类的方式来添加新的代码,而不需要修改原有的代码。

2.2 作用

  • 提高代码可复用性:在面向对象程序设计中,根据原子和抽象编程提高代码可复用性。在拓展时,只需要实现现有的接口和抽象类就可以实现拓展
  • 提高可维护性:稳定性和延续性高,易于拓展和维护
  • 便于软件测试:只需要对拓展的代码进行测试,不用对原有的代码进行测试

2.3 演示

场景介绍

在”第五人格“游戏中,有很多个人物角色。此时可以抽象出一个角色类,每新增一个角色信息,就让它继承这个抽象类。

代码演示:

Step1:定义抽象角色类
/**
 * [1]定义抽象的角色类,每新增一个角色时,只需要继承该方法并重写抽象方法
 */
public abstract class Role {
    public abstract void display();
}
 Step2:定义具体角色类

        2.1 医生类

/**
 * [2.1] 医生实现类
 */
public class Doctor extends Role {

    @Override
    public void display() {
        System.out.println("医生:可以在受到攻击时自疗");
    }
}

        2.2 机械师类

/**
 * [2.2] 机械师实现类
 */
public class Mechanician extends Role {
    @Override
    public void display() {
        System.out.println("机械师:可以操控一个机器人,有时间和限制");
    }
}
Step3:定义游戏总体控制类
/**
 * [3] 游戏控制类:
 * 以后每新增一个类,都只用给该类的成员方法进行设置,而不用修改该类的成员方法
 */
public class FifthPersonalityGame {
    //注入实现了Role类的角色
    private Role role;

    public void setRole(Role role) {
        this.role = role;
    }

    //调用角色的方法
    public void display() {
        role.display();
    }
}

Step4:游戏测试
    @Test
    public void test(){
        //1. 创建第五人格游戏对象
        FifthPersonalityGame game = new FifthPersonalityGame();

        //2. 创建角色
        //如果还有其他可以选择的角色,可以直接创建后直接设置到游戏对象的成员方法中
        Doctor doctor = new Doctor();
        //c_Mechanician mechanician = new c_Mechanician();

        //3. 选择当前角色
        game.setRole(doctor);

        //4. 显示角色信息
        game.display();
    }

如果后续又新增了“律师”、“魔术师”等角色,只需要让这个类继承Role这个抽象类即可。而不用修改现有的源码。
这种方式提高了代码的可复用性和可维护性。同时,在进行测试时,也只需要测试新增的类。

2.4 优秀框架中的开闭原则

在Spring框架中,就实现了对修改关闭但对扩展开放的设计理念,这使得开发者可以通过扩展和配置的方式来增加新功能,而不需要修改或破坏原有的框架代码。从而使得Spring 框架变得更加灵活、可扩展和易于维护。

以下是Spring使用开闭原则的一些举例:

  1. 依赖注入(DI):Spring 使用依赖注入将各个组件解耦,并通过配置文件或注解的方式动态地注入依赖关系。这使得在不修改已有代码的情况下,可以轻松地替换、添加或删除组件。

  2. 扩展点机制:Spring 提供了多个扩展点,如 BeanPostProcessor、BeanFactoryPostProcessor 等。这些扩展点允许开发者在不修改框架源码的情况下,通过编写自定义的扩展类来添加额外的功能。

  3. 面向接口编程:Spring 通过接口和抽象类定义了许多核心组件,如 ApplicationContext、BeanFactory 等。这样一来,开发者可以基于这些接口和抽象类进行扩展实现,而不需要修改框架的源码。

  4. AOP(面向切面编程):Spring 的 AOP 功能通过动态代理机制实现,可以在不修改源代码的情况下,添加切面逻辑来实现横切关注点的功能。这使得系统的核心业务逻辑和横切关注点能够分离,符合开闭原则。


三、里氏替换原则

3.1 介绍

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

3.2 作用

        里氏替换原则是实现开闭原则的重要方式之一。 它克服了继承中重写父类造成的可复用性变差的缺点。 是动作正确性的保证,即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。

3.3 演示

场景介绍

关于里氏替换原则的例子有很多,最有名的是“正方形不是长方形”问题。生活中的例子也有许多,例如:玩具狗不会叫,不能定义为”狗“的子类;玩具炸弹伤害不了敌人,不能定义为”武器炮弹“的子类;”气球鱼“不会游泳,不能定义为”鱼类“的子类等等……

在生物学上划分鸟类包括了麻雀、燕子、企鹅等。但是从类继承关系上看,”企鹅“不会飞,不能继承分类的”飞行“方法,否则会破坏类的继承关系。

代码演示

错误示范
Step1:定义“鸟类”
//鸟类的父类
public class Bird {

    //飞行速度
    private double speed;

    //飞行速度设置
    public void setSpeed(double speed) {
        this.speed = speed;
    }

    /**
     * 计算飞行时间
     * @param distance 飞行距离
     * @return 飞行时间
     */
    public double getFlyTime(double distance){
        double time = distance / speed;
        return time;
    }
}
Step2:定义“麻雀类”

        2.1 “麻雀类”继承“鸟类”这个父类

// 麻雀类,会继承父类的方法
public class Sparrow extends Bird{

}

        2.2  对麻雀类进行测试

    @Test
    public void testSparrow(){
        double distance = 100;//飞行距离
        //麻雀飞行
        Bird sparrow = new Sparrow();
        sparrow.setSpeed(10); // 继承父类方法
        double sparrowFlyTime = sparrow.getFlyTime(distance);
        System.out.println(sparrowFlyTime);
    }

以上测试中,输出该麻雀飞行时间为10.0。运行结果正确。

Step3:定义“企鹅类”

        3.1 “企鹅类”继承“鸟类”这个父类并重写父类方法

由于企鹅不会飞,所以对企鹅所继承的父类方法进行重写,设置飞行速度为0。

// 企鹅类,继承鸟类方法后,对飞行速度方法进行了重写,设置了飞行速度为0
public class Penguin extends Bird {

    //企鹅不会飞,重写父类方法,设置飞行速度为0
    @Override
    public void setSpeed(double speed) {
        super.setSpeed(0);
    }
}

      3.2 对企鹅类进行测试

    @Test
    public void testPenguin(){
        double distance = 100;//飞行距离
        //企鹅飞行
        //由于企鹅不会飞,其子类重写父类方法,即飞行速度为0,导致计算飞行时间时出现Infinity
        Bird penguin = new Penguin();
        penguin.setSpeed(10); // 已被企鹅的子类重写为速度 = 0
        double penguinFlyTime = penguin.getFlyTime(distance);
        System.out.println(penguinFlyTime);
    }

        以上测试中,输出该企鹅飞行时间为Infinity。此时出现:距离100 / 速度0 (数学计算错误)

        以上模式存在一个问题:企鹅类虽然是鸟类,但是企鹅不会飞,被迫继承了鸟类的飞行方法。企鹅类对该飞行方法进行了重写,设置了飞行速度为0,导致在计算飞行时间时出现了错误。说明此时企鹅类重写父类方法导致类的继承关系被破坏,即违背了里氏替换原则

正确示范

对以上方式进行改进。

Step1:定义“动物类”
//动物类
public class Animal {
    //所有动物都会吃
    public void eat(){
        System.out.println("动物会吃饭");
    }
}
Step2:定义“鸟类”
// 鸟类,继承动物类
public class Bird extends Animal {
    private double speed; //飞行速度

    //设置飞行速度
    public void setSpeed(double speed) {
        this.speed = speed;
    }

    //计算飞行时间
    public double getFlyTime(double distance){
        double time = distance / speed;
        return time;
    }
}
Step3:定义“麻雀类”
// 麻雀类,会继承父类的方法
public class Sparrow extends Bird {

}
Step4:定义“企鹅类”
// 企鹅类,由于没有鸟类的飞行方法,因此不继承鸟类,直接继承动物类
public class Penguin extends Animal {

}
Step5:测试
    @Test
    public void test(){
        double distance = 100;

        //麻雀
        Bird sparrow = new Sparrow();
        sparrow.setSpeed(10); // 继承父类方法
        double sparrowFlyTime = sparrow.getFlyTime(distance);
        System.out.println(sparrowFlyTime); 
        sparrow.eat();

        //企鹅
        //由于企鹅不会飞,直接继承动物类,从而无法调用飞行方法
        Animal penguin = new Penguin();
        penguin.eat();
    }


四、依赖倒置原则

4.1 介绍

        高层模块不应该依赖低层模块,两者都应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象。即要求对抽象进行编程,不要对实现进行编程,这样就降低了客户与实现模块间的耦合。简单来说:要面向接口编程,不要面向实现编程。

实现方式:

  1. 每个类进行提供接口或抽象类,或两者都提供

  2. 变量的声明类型尽量使用接口或抽象类

  3. 任何类都不应该从具体类派生

  4. 继承时注意要尽量遵循里氏替换原则(即不要重写父类方法导致破坏类的继承结构)

4.2 作用

  • 降低类间的耦合性

  • 提高系统稳定性

  • 提高代码可读性及可维护性

4.3 演示

        以组装电脑为例。组装一台电脑,需要配件CPU、硬盘、内存条。CPU有Intel、AMD等品牌,硬盘有希捷、西数等品牌,内存条有金士顿、海盗船等品牌。

这种方式导致一个问题,即每次更换配置品牌,如想换成AMD牌的CPU,我需要修改Computer类的代码。

故修改为以下方式:

Step1:定义电脑的各个组件接口
//CPU
public interface Cpu {
    void run();
}

//硬盘
public interface HardDisk {
    void save();
}

//内存条
public interface Memory {
    void save();
}
Step2:定义一台电脑
//电脑需要配件CPU、硬盘、内存条配置。
// CPU有Intel、AMD等品牌,硬盘有希捷、西数等品牌。内存条有金士顿、海盗船等品牌。
public class Computer {
    private Cpu cpu; //CPU
    private HardDisk hardDisk; //硬盘
    private Memory memory; // 内存条

    public Cpu getCpu() {
        return cpu;
    }

    public void setCpu(Cpu cpu) {
        this.cpu = cpu;
    }

    public HardDisk getHardDisk() {
        return hardDisk;
    }

    public void setHardDisk(HardDisk hardDisk) {
        this.hardDisk = hardDisk;
    }

    public Memory getMemory() {
        return memory;
    }

    public void setMemory(Memory memory) {
        this.memory = memory;
    }

    public void run(){
        System.out.println("=====电脑开机=====");
        cpu.run();
        memory.save();
        hardDisk.save();
    }
}
Step3:定义具体的组件类

        3.1 定义CPU

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

        3.2 定义硬盘

// 希捷品牌硬盘
public class XiJieHardDisk implements HardDisk {
    public void save() {
        System.out.println("使用希捷硬盘");
    }
}

        3.3 定义内存条

// 金士顿品牌内存条
public class KingstonMemory implements Memory{
    public void save() {
        System.out.println("使用金士顿内存条");
    }
}
Step4:测试
    @Test
    public void test(){
        //组装计算机
        IntelCpu cpu = new IntelCpu(); //CPU
        XiJieHardDisk hardDisk = new XiJieHardDisk(); //硬盘
        KingstonMemory memory = new KingstonMemory(); //内存

        Computer computer = new Computer();
        computer.setCpu(cpu);
        computer.setHardDisk(hardDisk);
        computer.setMemory(memory);

        computer.run();
    }

      优点:
         * 新增一个品牌,直接重写配置类
         * 此时如果想要换一个配置品牌,只需要重新new一个配置再装配到Computer类中即可

以上方式符合以下实现条件:

  1.  为每个配置类提供接口
  2. Computer类变量声明使用接口
  3. 类又接口进行派生
  4. 不破坏类的继承结构


五、单一职责原则

5.1 介绍

单一职责原则的核心思想是:一个类或者模块应该只负责完成一个单一的职责或功能

通俗地说,就是一个类或者模块应该只做一件事情,不涉及其它不相关的功能。

该原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下缺点:

  • 一个职责的变化可能会削弱或抑制这个类其他能力

  • 客户端需要该对象的某一个职责,被迫将其他不需要的职责都包含进来,造成了代码冗余

  • 一个类或模块功能较多,代码杂乱,导致代码的复杂度增加、可读性下降,难以阅读和维护

 5.2 作用

        这样设计的代码更加清晰、可读性更好,并且易于维护和扩展。每个类或者模块的职责清晰明确,容易理解和维护

        当有需求变更或者功能扩展时,只需要关注相关的类或者模块,而不需要影响其它不相关的功能。

        其次,单一职责原则有助于降低代码之间的耦合度,提高代码的灵活性和可测试性。

5.3 演示

        公司工作内容主要包括人员管理项目管理两个方面的工作。人员管理主要包括人员招聘、薪资发放等工作。项目管理主要包括技术培训、开发运营等工作。如果将这些工作交给一个人负责容易造成混乱。正确的做法是人员管理分给人事部门,项目管理分给项目部门。

Step1:定义“公司类”
//公司
public class Company {
    private PersonnelDepartment personnel; //人事部门
    private ProjectDepartment project; //项目部门

    public PersonnelDepartment getPersonnel() {
        return personnel;
    }

    public void setPersonnel(PersonnelDepartment personnel) {
        this.personnel = personnel;
    }

    public ProjectDepartment getProject() {
        return project;
    }

    public void setProject(ProjectDepartment project) {
        this.project = project;
    }
}

      Step2:定义“人事部门”
//人事部门:人员招聘、薪资发放
public class PersonnelDepartment {
    public void recruitment(){
        System.out.println("人员招聘");
    }

    public void payroll(){
        System.out.println("薪资发放");
    }
}
Step3:定义“项目部门”
//项目部门:技术培训、开发运营
public class ProjectDepartment {
    public void technicalTraining(){
        System.out.println("技术培训");
    }

    public void developmentAndOperation(){
        System.out.println("开发运营");
    }
}
Step4:测试
    @Test
    public void test(){
        Company company = new Company();
        PersonnelDepartment personnel = new PersonnelDepartment(); //人事部门
        ProjectDepartment project = new ProjectDepartment(); //项目部门
        company.setPersonnel(personnel);
        company.setProject(project);
    }


六、接口隔离原则

6.1 介绍

        接口隔离原则要求程序员尽量将臃肿庞大的接口拆分成更小的和更具体的接口,让接口中只包含客户感兴趣的方法,不应该被迫依赖于它不使用的方法。一个类对另一个类的依赖应该建立在最小的接口上。简单来说,就是要为各个类建立它们需要的专用接口,而不要试图去建立一个很庞大的接口供所有依赖它的类去调用。

接口隔离原则和单一职责都是为了提高类的内聚性、降低它们之间的耦合性,体现了封装的思想,但两者是不同的:

  • 单一职责原则注重的是类的职责,而接口隔离原则注重的是对接口依赖的隔离。
  • 单一职责原则主要是约束类,它针对的是程序中的实现和细节;接口隔离原则主要约束接口,主要针对抽象和程序整体框架的构建

区别:

⚪单一职责原则强调一个类或模块的职责单一,一个类或模块只负责一项功能。

⚪接口隔离原则强调合理定义接口,确保接口粒度适当,不强迫实现类去实现其不需要的接口成员。

⚪两个原则都有助于提高代码的可维护性、可扩展性和可测试性,但侧重点略有不同。

6.2 作用

接口隔离原则是为了约束接口、降低类对接口的依赖性,遵循接口隔离原则有以下优点:

1. 避免接口臃肿和依赖冗余
        接口隔离原则要求将接口拆分成更小的、更具体的接口,每个接口只包含其实现类所需的最小接口成员。这样,实现类就不会被强迫实现其不需要的接口成员,从而避免了接口冗余和依赖关系过于复杂的问题。
2. 降低耦合度
        通过精确定义接口,让不同的模块和类之间的依赖关系变得清晰明了。这样能够使得系统更加灵活,易于维护和修改。
3. 容易重构和扩展
        遵循接口隔离原则的设计能够更好地支持系统的重构和扩展。当需要增加新的功能或类时,只需要实现其需要的接口,而不必去修改原有的接口和实现类,这样能够减少修改的范围和影响。同时,这也能够使得系统更加模块化,方便模块的组合和重用。
4. 提高代码质量
        接口隔离原则能够提高代码的可读性、可维护性和可测试性。实现类只需要实现其需要的接口成员,让代码变得更加简洁明了,并且方便单元测试和代码复用。

6.3 演示

安全门案例:创建一个XX品牌的安全门,该安全门具有防火、防水、防盗的功能。可以将防火,防水,防盗功能提取成一个接口,形成一套规范。

此时,如果又加了一个YY品牌的安全门,但是YY牌安全门只有防火、防盗功能,如果实现了以上安全门的接口显然不合理,因为YY品牌安全门并没有防水功能,但是被迫接受了该功能。很显然如果实现SafetyDoor接口就违背了接口隔离原则。

因此,可以采用以下方式:

将防火、防水、防盗三个功能拆成三个接口,每个安全门具有什么功能,就实现什么接口。

Step1:定义功能接口

分别定义“防盗”接口、“防火”接口、“防水”接口

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

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

/**
 * 防水接口
 */
public interface Waterproof {
    void waterProof();
}
Step2:定义不同品牌的安全门
//XX牌防盗门,能够防盗、防火、防水
public class XXDoor implements AntiTheft,Fireproof,Waterproof{
    public void antiTheft() {
        System.out.println("防盗");
    }

    public void fireProof() {
        System.out.println("防火");
    }

    public void waterProof() {
        System.out.println("防水");
    }
}
//YY牌防盗门,能够防盗、防火
public class YYDoor implements AntiTheft,Fireproof{
    public void antiTheft() {
        System.out.println("防盗");
    }
    public void fireProof() {
        System.out.println("防火");
    }
}
Step3:测试
    @Test
    public void test(){
        XXDoor xxDoor = new XXDoor();
        xxDoor.antiTheft();
        xxDoor.fireProof();
        xxDoor.waterProof();

        YYDoor yyDoor = new YYDoor();
        yyDoor.antiTheft();
        yyDoor.fireProof();
    }


七、迪米特法则

7.1 介绍

        迪米特法则又叫最少知识原则。只和你的直接朋友交谈,不跟“陌生人”说话。

        “朋友”是指:当前对象本身、当前对象的成员对象、当前对象所创建的对象、当前对象的方法参数等,这些对象同当前对象存在关联、聚合或组合关系,可以直接访问这些对象的方法。

        迪米特法则含义是:如果两个软件实体无须直接通信,那么就不应当发生直接的相互调用,可以通过第三方转发该调用。其目的是降低类之间的耦合度,提高模块的相对独立性。

        从迪米特法则的定义和特点可知,它强调以下两点:

从依赖者的角度来说,只依赖应该依赖的对象。

从被依赖者的角度说,只暴露应该暴露的方法。

所以,在运用迪米特法则时要注意以下 6 点:

在类的划分上,应该创建弱耦合的类。类与类之间的耦合越弱,就越有利于实现可复用的目标。
在类的结构设计上,尽量降低类成员的访问权限。
在类的设计上,优先考虑将一个类设置成不变类。
在对其他类的引用上,将引用其他对象的次数降到最低。
不暴露类的属性成员,而应该提供相应的访问器(set 和 get 方法)。
谨慎使用序列化(Serializable)功能。

 迪米特法则和单一职责原则的区别:

单一职责原则(Single Responsibility Principle)和迪米特原则(Law of Demeter)是面向对象设计原则中的两个重要原则,它们的区别如下:

  1. 单一职责原则:单一职责原则指的是一个类或模块应该有且只有一个责任。它强调一个类或模块应该只负责一件事情,并且只有一个引起它变化的原因。这样做有助于降低类或模块的复杂性、提高可维护性和可重用性。单一职责原则主要关注的是类或模块的内聚性,要求类或模块的功能要高度集中和独立。

  2. 迪米特原则:迪米特原则,也称为最少知识原则,强调一个对象应该对其他对象有尽可能少的了解。迪米特原则要求在设计和交互过程中,对象之间应该尽量减少彼此的依赖关系,只与直接的朋友进行通信。直接的朋友指的是当前对象本身、被当作方法参数传入的对象、当前对象所创建的对象以及当前对象的组件对象。迪米特原则主要关注的是类和对象之间的关系,要求减少类之间的耦合度,提高系统的灵活性和扩展性。

总结起来,单一职责原则关注的是类或模块自身的内聚性,强调一个类或模块应该只负责一项职责;而迪米特原则关注的是对象之间的依赖关系,强调一个对象应该尽可能少地了解其他对象。它们都是为了提高软件设计的质量和可维护性,但侧重点和考虑的因素略有不同。

7.2 作用

迪米特法则要求限制软件实体之间通信的宽度和深度,正确使用迪米特法则将有以下两个优点:

降低了类之间的耦合度,提高了模块的相对独立性。

由于亲合度降低,从而提高了类的可复用率和系统的扩展性。

但是,过度使用迪米特法则会使系统产生大量的中介类,从而增加系统的复杂性,使模块之间的通信效率降低。所以,在釆用迪米特法则时需要反复权衡,确保高内聚和低耦合的同时,保证系统的结构清晰。

7.3 演示

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

Step1:定义“明星类”
// 明星类
public class Star {
    private String name;

    public Star(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
Step2:定义“粉丝类”
// 粉丝类
public class Fans {
    private String name;

    public String getName() {
        return name;
    }

    public Fans(String name) {
        this.name = name;
    }
}
Step3:定义“公司类”
// 公司类
public class Company {
    private String name;

    public String getName() {
        return name;
    }

    public Company(String name) {
        this.name = name;
    }

}
Step4:定义“经纪人”类
// 经纪人类
public class Agent {
    private Star star;
    private Fans fans;
    private Company company;

    public void setStar(Star star) {
        this.star = star;
    }

    public void setFans(Fans fans) {
        this.fans = fans;
    }

    public void setCompany(Company company) {
        this.company = company;
    }

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

    //和媒体公司洽谈的方法
    public void business() {
        System.out.println(star.getName() + "和" + company.getName() + "洽谈");
    }
}
Step5:测试
    @Test
    public void test(){
        //创建经纪人类
        Agent agent = new Agent();
        //创建明星对象
        Star star = new Star("林青霞");
        agent.setStar(star);
        //创建粉丝对象
        Fans fans = new Fans("李四");
        agent.setFans(fans);
        //创建媒体公司对象
        Company company = new Company("黑马媒体公司");
        agent.setCompany(company);

        agent.meeting();//和粉丝见面
        agent.business();//和媒体公司洽谈业务
    }


八、合成复用原则

8.1 介绍

        通常,类的复用分为继承复用合成复用两种。

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

        合成复用原则又叫组合/聚合复用原则。它要求在软件复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。 如果要使用继承关系,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。

        通俗地解释,合成复用原则就像是用积木搭建模型一样。当我们需要创建一个新的模型时,不必从头开始制作每一个构件,而是可以选择现有的积木来组合。每个积木都有自己的特性和功能,我们可以根据需求选择适合的积木进行组合,从而形成新的模型。这样做的好处是,我们可以灵活地搭建出各种不同的模型,而无需重复造轮子。

        在软件设计中,同样的道理适用。合成复用原则告诉我们,当我们需要创建一个新的对象时,可以利用现有的对象来构建,而不是通过继承关系来实现。通过合成复用,我们可以将现有对象的不同部分组合起来,形成新的对象,达到代码复用的目的。这样做有助于降低耦合度,增加系统的灵活性和可维护性。

8.2 作用

通常类的复用分为继承复用和合成复用两种,继承复用虽然有简单和易实现的优点,但它也存在以下缺点。

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

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

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

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

  1. 它维持了类的封装性。因为成分对象的内部细节是新对象看不见的,所以这种复用又称为“黑箱”复用

  2. 新旧类之间的耦合度低。这种复用所需的依赖较少,新对象存取成分对象的唯一方法是通过成分对象的接口。

  3. 复用的灵活性高。这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象。

8.3 演示

汽车按“动力源”划分可分为汽油汽车、电动汽车等;按“颜色”划分可分为白色汽车、黑色汽车和红色汽车等。如果同时考虑这两种分类,其组合就很多。

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

Step1:定义“颜色接口”
/**
 * 颜色接口
 */
public interface Color {
    String tintage();//涂色
}
Step2:定义具体颜色类
/**
 * 白色车漆
 */
public class White implements Color {
    @Override
    public String tintage() {
        return "白色";
    }
}


/**
 * 红色车漆
 */
public class Red implements Color{
    @Override
    public String tintage() {
        return "红色";
    }
}


/**
 * 黑色车漆
 */
public class Black implements Color{
    @Override
    public String tintage() {
        return "黑色";
    }
}
Step3:定义“汽车父类”
public abstract class Car {
    protected Color color;

    public Car(Color color) {
        this.color = color;
    }

    public abstract void move();
}
Step4:定义具体汽车类
/**
 * 汽油汽车
 */
public class GasolineCar extends Car {

    public GasolineCar(Color color) {
        super(color);
    }

    @Override
    public void move() {
        System.out.println(color.tintage() + "汽油汽车在行驶");
    }
}


/**
 * 电动汽车
 */
public class ElectricCar extends Car {
    public ElectricCar(Color color) {
        super(color);
    }

    @Override
    public void move() {
        System.out.println(color.tintage() + "电动汽车在行驶");
    }
}
Step5:测试
    @Test
    public void test(){
        Car gasolineCar = new GasolineCar(new White());
        gasolineCar.move();

        Car electricCar = new ElectricCar(new Red());
        electricCar.move();
    }

  • 20
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
范围:CPU上可以识别的代码和数据。全部的代码总和。 要求:从定义开始的设计。完整性,彻底地定义从无开始的整个设计。这是因为软件之软,也是因为硬件平台的多样性和特殊性。 完整把握,从头设计是第一原则。因为软件世界自己并不能统一,还有继续分化的趋势。没有根本一致的基础可能是软件的本性。退回到一无所有是处理软件问题的根本。 在这样的视野下,操作系统只是一个部分,一个模块,不同的操作系统任你选择;语言的选择是运行环境的选择(每种语言有每种语言的运行时布局);所谓框架只是“类库+运行环境”的一种构造。 没有对其负载能力、操作强度进行评估前,这些东西(操作系统、语言、框架)还都不能纳入设计规范。 性能:运行过程的收敛(长时间运行的均态)。操作强度设计(串行处理速度),负载能力设计(并发处理的量)。可靠性设计。 软件问题的3个方面: 1、硬件,软件的操作对象和运行软件的数字系统(CPU系统和数字加速硬件) 2、交互操作(界面),专业界面设计 3、软件调度性能,实时的自动化过程(设备控制和自动测量)和用户交互过程(请求服务过程和干预过程;本地交互和远程交互),程控和网络访问的调度(服务器)。 软件项目的3个部分:(把3个阶段由纵向横过来,进行统筹) 分解文档,集成平台,可维护性要求。 软件设计必须有自说明特性。不能对文档产生依赖性。软件代码中合适的地方,需要对文档进行恰如其分说明。原则是,每段代码,每处需要理解的地方,如果和总体架构相关,就要有说明。 软件领域需要简化。需要还原软件本来的面目。EDA有泛滥的趋势,软件的各个方面都需要简化。软件形态、需求分析、文档说明、开发工具等。 需求分析过分强调适应生命周期的变化和没有需求分析是一样的。不切实际的面向未来的需求架构的直接结果是软件的复杂和错误百出。 软件只有一个,而观察的视角很多。要采用最适合的观察视角,让软件一目了然。 软件的生成过程和观察过程是两个不同的观念。生成过程又可以区分为:研究过程和工程过程。研究过程可以通过结果,研究报告反映;工程过程则必须采用过程刻画。 软件规范使用的语言一定要有普遍语义,但描述本身具有特殊性;不能强求它的全球唯一。一定要雄视全体,才能选择正确的立足点,这就要求对目前的软件技术有一个了解;要考虑纳入新的发展,那么规范应该分层,把一般的和具体易变的成分分开;要有具体的指导意义,越具体指导意义越大,但通用性则越小。 所谓架构,可能是十分具体应用的代表;不同类别的应用必然有不同的架构。软件架构本身是“应用架构”。因此,不能规范具体的架构。到是可以做:应用架构规范的规范。 逻辑架构的特殊性。可以判断,任何一款实用的软件采取的软件逻辑抽象都是别样的,特例的逻辑。否则,软件不可能那么轻快实用。软件逻辑,鬼魅也。而需求分析,必须是现实实用的,而不是同构/仿真的-这似乎是反对象分析的。因为这里强调的是和软件的交互界面,这个界面远远没有反映现实世界的结构。须知,软件强调的是数据处理,是输入输出。否则,就不能达到最简化。 可能现实世界的结构映射,最适合的方式是数据库 - 采用纯数据结构进行映射。除此之外,能有更合适的技术吗? 面向对象建模是吗?那么对象又如何与现实世界的对象绑定在一起呢? 这再次表明,在软件技术和需求分析之间有鸿沟。软件技术作为特殊的技术,有它的有限性。也反映了,包含软件应用在内的现实架构已经固定。 如果软件是数据处理,是输入输出,那么软件结构也就可以确定了! 可视化、用户操作界面解开了另外的软件世界,因为可视化可以代表用户更抽象的逻辑。用户希望操作可视对象,象操作现实对象一样。软件从模拟现实对象的过程中继承了其结构。 工业控制也开启了新的软件世界,因为软件要从分离的输入建立“综合感知”,感知到设备状态,然后做出响应。 软件有其固有的物理属性,也就是计算的量。算法领域,无论算法的论证多么曲折,求得的结果,物化为软件,总是“早已熟知”的软件。这一区分,是定义软件规范的基石。 算法构造领域是和软件完全不同的领域,算法不是软件。算法类似数学系统。也一如数学系统那样多样。 软件构造。算法总要转化为软件,这就是软件构造问题。寻址系统,数组。软件把自己的生成作为问题,给算法开辟了新的领域。软件生成,是一个“构造-编译”问题。手工构造,自动编译。语言的发展,是一个软件生成的历史。所谓统一建模,所谓设计模式,其实都是软件生成的问题。 需求分析。需求分析本质上是独立的。所谓OOA,面向对象的建模,把程序构造概念上升到需求分析领域可能是不对的。一个先验的,复杂的难于掌握的限制,只会让人对需求分析望而却步;即使勉强掌握,难求对需求分析的创造性发展。需求分析应该专注于需求分析本身,独立发展,一切为了准确、快捷的分析。 需求分析层次高一些,抽象一些,自由一些,这样可以充分表达需求的本质。反而可以促进更高级别的程序自动生成。 软件生成的历史。软件生成是为了解决人机沟通,让“计算机语言”更接近普通人的思维逻辑。把这种“高级计算机语言”翻译成可以执行的代码,就是软件生成(代码生成)的任务。而软件编制是专业人员的事情,因此语言问题的本质其实不那么重要。须知,经过培训,莫尔司码的电报发报可以比说话的语速还快!因此,计算机语言的前途迷茫;实际上也确实迷茫,历史上语言的层出不穷本身就说明了问题,至今仍然如此。在当今,必须建立这样的观点:语言是因人而异的;面对一个语言的时候,要清醒,首先明确“这是为谁设计的语言”;也就是说,需求分析之前的需求是要明确,让什么人来设计软件,然后为他们选择合适的语言。软件生成除了代码生成,还包括另外一个意思:软件构造。这在前面已经论述过了。只是,这里的软件构造机制已经在语言中奠定了。手工参与的软件构造只是语言给出的构造机制的应用。手工的软件构造就是语言构造机制的复制,产生大量的代码,应付实际问题的量。 立体构造。这里还有一个立体问题,实际问题的构造可能产生立体构造,如同建筑,基本的构件组装出复杂的立体结构。这里是建筑设计师的功劳。可能目前我们在语言层面上混淆不清的关键也在这里,没有区分语言和立体构造的责任。一个趋势是语言本身总是试图包揽建筑师的责任。把立体构造独立出来,带来的问题是:这个构造本身必须能够证明自己是正确的。1)能产生软件2)构造逻辑上正确,确实能解决应用问题。构造本身有一个属性,它有通用性。根本原理是通用的;总体构造本身具有一般性,也就是抽象性、实际问题无关性;局部构件具有通用性。也就是说,这里存在容器和容量的区别,构造是容器,实际问题是装在容器中的量。一个好的容器要能顶住容量的压力;一个好的建筑架构要能满足负载和抗振性要求。而架构本身的承受能力是客观的,只与架构本身有关。这也就是说,架构本身自我构造的,因此也就是科学。可能软件构造本身是澄清问题的工作,明确“容量”的特点,为软件构造的选择提供准确的依据,杀鸡不要用牛刀。实际问题的“容量”很容易测量,因为它反映为应用的规模,流程的流量。(架构是什么?架构是否存在?如果我们所说非虚,那么如何为架构下一个定义-一定是一个由具体业务流量和模式支撑的架构) 软件(算法)的构造。一个是数据的复杂性(内在互相关系),一个是计算方法(步骤和缓冲)。从宏观角度,数据关系是更根本的东西。目前的高级语言,变量和流程(顺序、分支-步骤;循环-缓冲和迭代)研究的多,而数据复杂性构造不足。 同构现象。CPU指令集合可以说是硬件直接实现的软件。软件帝国从这里提取软件精神,并升华它。从硬件的角度,从寄存器和指令执行流程,体现出的是变量和迭代(顺序更迭,循环往复)。(迭代流程)基于固定寻址的变量,经过寻址接口,可以处理任意数据,从而把迭代流程变成了一般流程。CPU的基本过程,产生了指令和数据,指令天生具有子程序的基因(一般流程),数据天生具有数据结构(寻址能力)的基因。高级的构造一般也是这种结构的类似:设计一套类似CPU的机制,支撑程序和数据;独特的“寻址机制”和“CPU处理能力”是实现构造的核心机制;迭代是所有这种机制的动力学和构造方式。而数据化是“寻址机制”的基础。抽象是数据化的工厂,也因此必须研究抽象技术。 抽象技术。所谓抽象,就是具体化,是范围的界定和比对(两种具体化对象之间的比对)。如果范围界定的完整,那么比对建立的联系就是普遍联系,普遍联系也就是所谓抽象原则。 评价标准。软件架构需要评测。这种评测是“在商言商”似的评测。评测的基础是软件架构的具体化。当掌握了架构的构造方法,每种架构本身也就具体化,是一种具体的架构。一种具体化的架构,就可以识别;可以识别则可以客观评测。可以按照立体架构的“压力”、“流量”等概念进行评测。 需求的把握-需求的变化。我们希望永恒不变的需求,核心需求和需求方式(表现和满足步骤);而事实上需求总在演化。软件必须无条件、最大限度地方便需求的表达和需求的满足。软件可能永远只是皮肤,需求源于现实核心深处,软件是一件衣服。这种观点下,软件是没有中心的一种架构。软件架构和需求之间联系的定量评测。 软件和算法的分开 软件的构造作为软件的通用属性 需求的独立 推论:算法是应用的算法。比如数学公式的计算、图形图象的处理、频谱分析、词法和语法分析。因此算法不是通用的软件算法。也因此软件构造是软件规范的一部分,因为它是通用的软件构造技术。 计算技术和应用之间有明显的区别,是两种不同的成分。软件规范是纯粹的,只关心计算技术。而不关心应用建模。计算方法本身早已经被发现了(也就是怎么自动计算,或者说什么是可计算的),剩下的问题只是应用问题。把应用问题的解决纳入软件计算模式。自动计算技术在汇编指令集合那里得到了说明。所谓软件设计是把这种计算方式发扬广大。 所谓算法,就是明确问题,然后发现用自动计算的方式解决问题。从这个意义上说,软件是应用问题导向的。那么,也就是要以问题为中心谈论软件。不同类型的问题需要的解决方式有独特的强调。这也就反映为所谓不同的软件技术。所以,区分软件计算技术和应用问题的成分,是软件规范需要首先识别的东西。 解决问题。本质上是把问题装到变量里面的过程,是放大CPU寄存器的过程。表示层:(把局面、环境;起点和终点需要定义在一个世界里)装进去,组织起来。计算层(展开层):基于表示,定义问题解决步骤(定义运动和过程)。 需求分析。问题描述采用的方法可能应该和软件算法完全分开。否则不能发现问题描述的创造性方法,不能表达问题本质。阐述问题,写文章我们有某篇布局之法;哲学研究我们有严谨的逻辑方法。需求分析,我们一定可以创造自己的方法。这是什么方法?满足使用要求,满足使用流程。离散/隔离各个需求。事实上,面向外部的分析理解和面向内部的分析理解之间有鸿沟。因为这是两个不同的世界。在两个相差悬殊的世界之间,搭建的构造也必然多种多样,以奇为平常。那么,建立联系的媒介少的可怜。可能问题本身也正在于这种联系的分析和设计。 软件的量,是静态的。强调这部分就忽略了活跃的、奇异的、动态的部分。软件的出现不仅仅是被动地适应显示需求,同时也改变了现实需求本身。这种和现实需求融合在一起形成的状态,正是软件活跃的部分。在以前,仅仅以“应用软件”指称是不够的。(操作系统、编译软件、应用软件) 在范畴上,分为三个层次,或说3个范畴域: 1、 活跃的、黏性的动态层次。应用层。和现实之间的界面,是设备逻辑。需求简化、解决方案的奇异性;应用算法的专业性。这是软件形象最活跃的部分。 这里用的是抽象(业务流程)和具体(设备能力)统一的思维方法,构造逻辑的软件过程同时又是可以用具体进行描述的;动态的、物理的分析手段(物理的量)。 业务流程的设计几乎就是艺术设计。 2、 中间层。程序构造层。语言、编译技术、数据结构、设计方法(过程、数据、对象)等可以形式化的计算机科学的任务。对程序能力进行抽象,设计程序自动化生成的一套系统:语言、计算系统、编译系统。这是在静态和活跃部分之间的层次。这里的观念:设计方法、主程序、程序过程(和应用层的过程不是一一对应的)。 3、 静态层。软件的量,度量层。所有程序构造过程的差别消失了。这是软件的静态观点。 每层都有对软件的自己的理念,概念、过程和模型。两个层的对比,则凸显出不可调和的差别。也是所有关于软件的不成熟的印象、抽象产生的地方。 在应用层,抽象的、逻辑过程强一些。想象的部分占据主要的部分。需要对现实的业务,基于设备的具体能力,进行构造。 3个范畴定义了“软件”和“程序”的分别。第1层和第3层论述的是“软件”,而第2层论述的是“程序”。 软件和程序的研究方法不同。程序研究方法是完备的,而软件不完备。 程序开发应当体现软件特性。1)是逻辑的过程,总体的过程和子过程的观察和校验程序。2)软件的量层次上,软件的规模、运行强度和稳定性指标的自测试程序。 第二阶段 一定要有一个标准。软件如衣服,软件的交付文档应当显示出衣服是如何编织起来的。(相对于需求,软件是衣服,非核心;相对于硬件,软件是衣服,包裹) 要有一个理论说明。 架构也是衣服的一个部件,类似衣服的连接方式,模块集合的重心比对。 衣服是一个没有核心的结构。软件也一样要显示出这个特性。 无论如何,我们需要有观察软件的眼光,无论一套软件依据什么样的理论产生。 什么是软件?描述是软件的存在形式(文本格式)。软件一定是可执行的(这是软件的严肃性,精确、定量)。软件是异化的,一般异化为具体、特例(对抽象力最好的归结方式)(没有完美满足需求的软件,相对于需求,软件只能满足固定的需求,而不能满足需求的变化,即一款软件总是具体的;由一般产生出具体的思考方法,也就是构造的方法;或着是磁力打造,一个好的理论一定对现实素材有吸引力,向磁铁一般;这也是在矛盾中建造现实的方法,只要是具体的就肯定是可以分析出潜在矛盾、不完美的,问题不仅仅是分析、认识现实,还要能够构造现实;不存在完美的现实,只存在完美的理论 科学研究的方法是简化。工程的方法是‘相似’,复制发现事物时的状态,那么事物的表现就会复现。 在具体化这里,软件和硬件工作的方法在结果上实现了一致。只是方向不同,软件是从一般进行到具体;硬件一开始就是从具体出发,层层构造,搭建系统。硬件的设计明显具有以工艺、器件为核心的特征。配合器件的特新,进行外围设计。在硬件领域,是‘具体’为上;在软件领域,是‘具体’为下。) 对具体性的解释:组成所有物资的电子、质子、中子是圆的、相同的,但是这些相同的东西组成的原子则有几百种不同。每次量的规模的添加,都导致特殊性的添加。对于软件来说,也是如此。如下的概念是母庸质疑的,软件如同大山,沟壑鲜明。(这种巨大的特殊性,一定是和巨大的需求特殊性相应的)。 “软件以文本形式存在;软件在执行着;软件以个例的形式存在”,归结为在一起就是“软件是具体的”。 低一级别的定义:软件与数据和逻辑相关(数据和逻辑是软件的基本语义)。软件与过程相关(积分(存储,数据的数据化)和步骤(逻辑);过程是步骤的遍历,是数据的消长变化)。 执行的异化。区分独立执行和整体执行的概念。独立执行的代码称为模块,否则只是‘片段’。独立性和数据完整性相关,数据越庞大那么不独立的代码片段越多,模块就越大。模块独立性具有比和整体执行所要求的更大的自由度,也就是说整体只是使用了模块一部分的执行能力。模块独立执行获得的自由度是应该能够度量;模块的执行设计应该为了获取更大的自由度;自由度是模块可执行性质量的评定指标。对于整体执行的设计来说,自由度设计可能是设计过程的主导方法,它和全面、完整的需求理解相关,也和需求变化相关;因此自由度设计也是需求定位的设计。 软件的量,也就是软件的能力。这是理解软件解决问题的方式的基础。比如逻辑能力、计算能力、存储能力、图象能力等。 软件是运行的,软件是自我构造的,软件的全体的各个环节都有自己的量。编译、操作系统、文件管理等各环节都是不同分工的软件实现的。 需要构造在功能层次上的互相配合,解释这种完整性。显然每个部分都具有独立的完整性;完整性和完整性的配合构成一个总体的系统。因此未必要求系统的完整性、长期性、稳定性。反过来,系统满足需求的快速性、快速变化适应性、和现实一起变化、消长的特性、瞬态响应特性可能更接近系统的本质。 这好比太极拳,要在一个完满的氛围里运动。 软件能力是比代码高一个级别的抽象。又是构成软件内涵的基础语义。 ‘设备能力’的概念更基础,可以统一所有其它能力;又可以作为以硬件为中心的观念的基础。 能力的获得在于‘二分’。在于互相支撑的界面,支撑在一起的双方互为能力。 1.所谓需求分析,我们总是在创造一套新的方法和语言。而最有效的需求分析是自然语言分析。借助人们心目中的全部理解所用到的描述形式。也就是进入到实际存在的需求中去理解需求,分析需求。 因为领域、术语、行业表述习惯的原因,这个阶段千差万别。 2.其次是电脑的使用方式-电脑技术(外设、通信和电脑本身的硬件形态),尝试去设计合适的使用方式和硬件解决方案。 这里有使用环境、专业技术、成本、时间,以及个人习惯等原因,同样是一个精彩的过程。对领域工作方式的熟悉、外设相关的专业技术背景、折中技术决定了这是一个经验至上的活动。这就是电脑使用方式的确定。 3.进一步,确定使用者角色。使用者和使用地点关联。使用地点也就是前面电脑使用方式的一部分。 这是一个沟通过程,也是对有了电脑辅助参与,相关领域习惯改革的问题。 4.然后,进入二元分析阶段:使用者管角度、客观功能角度,分析功能,并完成二者之间的映射。 这个阶段,功能被量化。职能量化。职能和功能之间会有模糊,有授权的转移。这个阶段就是充分考虑这些问题。 5.然后,进入传统的需求分析阶段。 计算架构和功能描述的规格分析。使用者界面规划(详细、规格级别)。 界面规划、功能、架构三者之间组成互动的具体化过程。 最后会产生系统级别的文档。运行实体、接口;系统运行态、实体接口的输入输出规格。 6.然后,实体级别的程序构造阶段。 算法构造和程序构造。主要是从资源占用的角度确定宏观的算法。在这个阶段,是程序文档化阶段。文档这个属于是这个阶段的工具。 最后会产生严格的程序模块的文档。所有这些文档组合起来,可以构成运行流程。这些文档化的程序就是逻辑化的程序本身。 7.最后,编码阶段 用一种具体的语言,按照模块文档的接口、资源、算法要求,编制代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值