【设计模式】用Java手写21种常见设计模式

配套使用效果更佳: 本博客配套开源源码github点击跳转


引言

设计模式主要研究的是“变”与“不变”,以及如何将它们分离、解耦、组装,将其中“不变”的部分沉淀下来,避免“重复造轮子”,而对于“变”的部分则可以用抽象化、多态化等方式,增强软件的兼容性、可扩展性。

作为一个开发人员,在进行一个项目的设计与实现的过程中,应当具备软件架构的全局观,对项目进行模块化的设计,并充分考虑代码的可复用性,用最少的代码实现最完备的功能,使代码简洁、优雅。

优秀的系统应兼备功能强大、模块清晰、高扩展性,这离不开对各种设计模式的灵活运用。

  • 设计模式、面向对象六大原则:
    SRP:单一职责原则。 该设计原则是基于康威定律(Conway’s Law)的一个推论——一个软件系统的最佳结构高度依赖于开发这个系统的组织的内部结构。这样,每个软件模块都有且只有一个需要被改变的理由。很多程序员根据SRP这个名字想当然地认为这个原则就是指:每个模块都应该只做一件事。但实际上,更准确的定义应该是,任何一个软件模块都应该只对一个用户(User)或系统利益相关者(Stakeholder)负责。
    OCP:开闭原则。 该设计原则是由Bertrand Meyer在20世纪80年代大力推广的,其核心要素是:如果软件系统想要更容易被改变,那么其设计就必须允许新增代码来修改系统行为,而非只能靠修改原来的代码。设计良好的计算机软件应该易于扩展,同时抗拒修改。其实这也是我们研究软件架构的根本目的。如果对原始需求的小小延伸就需要对原有的软件系统进行大幅修改,那么这个系统的架构设计显然是失败的。
    LSP:里氏替换原则。 该设计原则是Barbara Liskov在1988年提出的一个著名的子类型定义。简单来说,这项原则的意思是如果想用可替换的组件来构建软件系统,那么这些组件就必须遵守同一个约定,以便让这些组件可以相互替换。在面向对象编程中的多态设计就体现了这一原则——能使用父类的地方,一定也可以替换为子类。
    ISP:接口隔离原则。 这项设计原则主要告诫软件设计师应该在设计中避免不必要的依赖,定义接口时应尽量拆分成较小的粒度,往往一个接口只对应一个功能。
    DIP:依赖反转原则。 该设计原则指出高层策略性的代码不应该依赖实现底层细节的代码,相反,那些实现底层细节的代码应该依赖高层策略性的代码。这一原则原则主要想告诉我们的是,如果想要设计一个灵活的系统,在源代码层次的依赖关系中就应该多引用抽象类型,而非具体实现。
    迪米特原则。 尽量减少对象之间的依赖,以减少系统中对象之间的耦合度。这样可以使得代码更加可维护、可扩展和易于理解。

  • 本文结构

本文第一章介绍面向对象的三大特性——封装、继承、多态,后续章节将展开介绍创建型、结构型、行为型三大类下的21种设计模式。

以下为本文的思维导图:

在这里插入图片描述

〇、面向对象及封装、继承、多态

  • 对象无处不在

在我们的生活中,对象无处不在,从我们周围的每一个人,到使用的电脑、桌椅,你的家,再到工作的公司、就读的学校,每一个都是一个对象。我们可以通过对这些对象进行模型建立,将现实世界“投射”到计算机的世界中去。

但是模型往往并不是孤立存在的,它们彼此之间存在着千丝万缕的联系,因此,我们应当充分利用面向对象的三大特性——也就是封装、继承、多态去建模,从而减少代码冗余、模块耦合。面向对象的三大特性是学习设计模式不可或缺的预备知识。

1.封装

  • 一个小例子

其实封装在我们的生活中也是随处可见的,就以我喜欢喝的烧仙草为例。在早期,许多奶茶店的烧仙草其实是采用塑封的方式进行包装的,这体现的就是一种封装的思想。封装隐藏了烧仙草内部的珍珠、芋圆、椰果、奶茶等,仅留给外界一根吸管用于访问,这根吸管相当于一个“接口”。

这种封装又能带来什么样的好处呢?作为一个喜欢喝烧仙草的人,我不必关注这个饮料是怎么做的以及它在瓶中的状态将如何变化,我仅仅只需要通过吸管这个“接口”去访问它的内部内容,这样做非常方便省事。其次,封装带来的好处就是,我既不用担心瓶中的饮料会倾洒出来,因为零散的数据被集中管理起来了;也不必担心内部会受到来自外界的污染,封装使得数据的安全性得到保障。

在这里插入图片描述

  • Java语言中的封装

在Java编程语言中,一对大括号“{}”就是类的外壳、边界,它能很好地把类的各种属性及行为包裹起来,将它们封装在类内部并固化成一个整体。封装好的类如同一个黑匣子,外部无法看到内部的构造及运转机制,而只能访问其暴露出来的属性或方法。

下面仍以实现一个烧仙草类为例,展示Java语言中的封装。

为了简化代码,而回归其原理本身,假使烧仙草GrassJelly中的组成为奶茶MilkTea、配料Material,则对于一个烧仙草类可以有以下实现。

package com.wang.design.chapter0;

/**
 * @author Tracy
 */
public class GrassJelly {
   
    //属性
    private String milkTea;
    private String material;

    //实例化对象
    public GrassJelly(String milkTea, String material) {
   
        this.milkTea = milkTea;
        this.material = material;
    }
    public GrassJelly(){
   

    }

    //访问属性
    public String getMilkTea() {
   
        return milkTea;
    }
    public String getMaterial() {
   
        return material;
    }
    public void show(){
   
        System.out.println("this is a cup of grass jelly.");
    }

    //修改属性
    public void setMilkTea(String milkTea) {
   
        this.milkTea = milkTea;
    }
    public void setMaterial(String material) {
   
        this.material = material;
    }
}

2.继承

现实生活中的对象是不计其数的,我们如果为每一个对象都单独创建一个类,不仅代码量会非常巨大,且其中的代码冗余会导致代码难以修改和维护,总之这样的做法实在是称不上优雅。

继承可以使父类的属性和方法延续到子类中,这样子类就不需要重复定义,并且子类可以通过重写来修改继承而来的方法实现,或者通过追加达到属性与功能扩展的目的。

下面我以交通工具体系为例,来展示如何通过Java中的继承来灵活扩展类。

  • Vehicle抽象类

首先,Vehicle为顶级抽象父类,因此将四种具体子类的公共特征与行为提取到其中,这样就不必在子类中重新定义,从一定程度上保证了代码的可复用性,降低了代码的冗余性。由于show()方法会因具体实现类的不同而不同,因此定义为抽象方法,为子类留有余地。

以下为代码:

package com.wang.design.chapter0;

/**
 * @author Tracy
 *
 * 0-2 继承
 */
public abstract class Vehicle {
   
    /**
     * 以下为所有交通工具共有的属性
     */
    protected String category;//"交通工具"
    protected String size;//"大","中","小"
    protected String name;//具体的名字"飞机"、"轮船"等
    protected String load;//载重

    /**
     * 构造函数
     */
    public Vehicle(String size, String load,String name) {
   
        this.category = "交通工具";
        this.size = size;
        this.name = name;
        this.load=load;
    }

    public Vehicle() {
   
    }

    /**
     * setter & getter
     *
     */

    public String getCategory() {
   
        return category;
    }

    public void setCategory(String category) {
   
        this.category = category;
    }

    public String getSize() {
   
        return size;
    }

    public void setSize(String size) {
   
        this.size = size;
    }

    public String getName() {
   
        return name;
    }

    public void setName(String name) {
   
        this.name = name;
    }

    /**
     * 以下为所有交通工具共有的方法
     */
    public abstract void show();
}
  • Airplane子类

定义一个实现类Airplane子类继承自Vehicle父类,新增航班属性flightNumber。

以下为代码:

package com.wang.design.chapter0;

/**
 * @author Tracy
 *
 * 0-2 继承
 *
 * 继承自父类Vehicle
 */
public class Airplane extends Vehicle{
   
    protected String flightNumber;//专属属性:航班号

    public Airplane(String size,String load,String flightNumber) {
   
        super(size,load,"飞机");//调用父类构造方法
        this.flightNumber = flightNumber;
    }

    public String getFlightNumber() {
   
        return flightNumber;
    }

    public void setFlightNumber(String flightNumber) {
   
        this.flightNumber = flightNumber;
    }

    @Override
    public void show() {
   
        System.out.println(category+":"+size+""+name+",航班号为"+flightNumber+"。");
    }

    public static void main(String[] args) {
   
        new Airplane("大型","250t","12345").show();
    }
}

调用show():

在这里插入图片描述

  • Bus子类
    定义一个实现类Bus子类继承自Vehicle父类,新增路线属性route。
package com.wang.design.chapter0;

/**
 * @author Tracy
 *
 * 0-2 继承
 *
 * 继承自父类Vehicle
 */
public class Bus extends Vehicle{
   
    protected String route;//线路

    public Bus(String size,String load,String route) {
   
        super(size,load,"公交车");
        this.route = route;
    }

    public String getRoute() {
   
        return route;
    }

    public void setRoute(String route) {
   
        this.route = route;
    }

    @Override
    public void show() {
   
        System.out.println(category+":"+size+""+name+",线路为"+route+"路。");
    }

    public static void main(String[] args) {
   
        new Bus("中型","2t","220").show();
    }
}

调用show():

在这里插入图片描述

3.多态

对于父类定义的引用只能指向本类或者其子类实例化而来的对象,这就是一种多态。除此之外,还有其他形式的多态,例如抽象类引用指向子类对象,接口引用指向实现类的对象,其本质上都别无二致。

简单来说,多态就是指一个引用指向不同的类的对象,但这种指向只能是自己->自己,自己->子类,不能是子类->父类。如下所示:

package com.wang.design.chapter0;

/**
 * @author Tracy
 *
 * 0-3 多态
 */
public class Polymorphic {
   
    public static void main(String[] args) {
   
        /**
         * [交通工具]是[交通工具]
         */
        Vehicle vehicle = new Vehicle() {
   
            @Override
            public void show() {
   
                System.out.println("交通工具");
            }
        };
        vehicle.show();
        /**
         * [飞机]是[交通工具]
         */
        vehicle = new Airplane("大型","250t","12345");
        vehicle.show();
        /**
         * [公交车]是[交通工具]
         */
        vehicle = new Bus("中型","2t","220");
        vehicle.show();

        /**
         * 以下为错误用法
         * [交通工具]是[公交车] 错误
         */
        //Bus bus=new Vehicle();
    }
}

面向对象及其三大特性至此已介绍完毕,预备工作已完成,下面就真正进入到设计模式的学习中去。

一、创建型1——单例模式

单例模式指在某个系统中一个类只存在一个实例,同时提供集中、统一的访问接口,以使系统行为保持协调一致。

1.饿汉模式(常用)

饿汉模式,即在初始阶段就主动进行实例化,并时刻保持一种渴求的状态,无论此单例是否有人使用。

下面我以上帝God为例,看看如何对世间唯一(假设唯一)存在的God进行饿汉模式的实现。

  • God类解析
  • private关键字确保God实例的私有性。

  • static关键字确保God的静态性,在类加载的时候就实例化了,在内存中永生且唯一,即使是内存垃圾收集器也不会对其进行回收。

  • final关键字则确保这个God是常量、恒量,引用一旦被赋值就不能再修改;

  • new关键字初始化God类的静态实例,并赋予静态常量god。

  • 最后,getInstance()方法用来提供给外界唯一获取这个god单例的接口。

package com.wang.design.chapter1;

/**
 * @author Tracy
 *
 * 1-1 创建型——单例模式
 */
public class God {
   
    private static final God god=new God();

    private God(){
   }

    private static God getInstance(){
   
        return god;
    }
}

2.懒汉模式

虽说在一开始就对god单例进行饿汉模式的加载具有一定的方便性,但如果加载之后很长一段时间都没有人访问god,那对上帝或者内存来说这恐怕是一种没有必要的消耗。基于这种考虑,于是就有了懒汉模式,也就是在需要的时候再对单例进行实例化。

  • 懒汉模式的God类
  • 去掉final关键字:由于god不是在声明时进行初始化的,因此将声明语句中的final关键字去掉了,否则后续真正需要初始化时就无法对其进行初始化(属于一种修改操作)。
  • 好处与坏处:好处是在一定程度上节省了内存,坏处是临时初始化实例会消耗CPU资源使得程序有些许的延迟(尽管看上去也许不太明显)。
  • synchronized关键字保证线程同步。
/**
 * @author Tracy
 *
 * 1-1 创建型——单例模式——懒汉模式
 */
public class God {
   
    private static God god;

    private God(){
   }

    private static synchronized God getInstance(){
   
        if(god==null)god=new God();
        return god;
    }
}
  • 懒汉模式的God类——优化线程同步处理

上面的代码其实有一些不足,当有多个方法访问getInstance()方法时,在还没有进入这个方法时就开始了排队,会造成线程提前阻塞,其实这是没有必要的。下面进行了优化:首先,进入方法不排队,而是当god判空才开始排队;在同步块中进行二重检测(双检锁),是为了排除在进入getInstance的同时已经有线程获取到了god。

懒加载模式的“双检锁”:外层放宽入口,保证线程并发的高效性;内层加锁同步,保证实例化的单次运行。如此里应外合,不仅达到了单例模式的效果,还完美地保证了构建过程的运行效率,一举两得。

/**
 * @author Tracy
 *
 * 1-1 创建型——单例模式——懒汉模式
 */
public class God {
   
    private volatile static God god;

    private God(){
   }

    private static God getInstance(){
   //进入方法不排队
        if(god==null){
   
            synchronized(Sun.class){
   //判空才开始排队
                if(god==null)god=new God();//二重检测是为了排除在进入getInstance的同时已经有线程获取到了god
            }
        }
        return god;
    }
}

相比“懒汉模式”,其实在大多数情况下我们通常会更多地使用“饿汉模式”,原因在于这个单例迟早是要被实例化占用内存的,延迟懒加载的意义并不大,加锁解锁反而是一种资源浪费,同步更是会降低CPU的利用率,使用不当的话反而会带来不必要的风险。越简单的包容性越强,而越复杂的反而越容易出错。

二、创建型2——原型模式

我们可以关注一下实例化与克隆之间的区别,二者都是在造对象,原型模式的目的是从原型实例克隆出新的实例,应对那些有非常复杂的初始化过程的对象或者是需要耗费大量资源的情况时具有一定的性能优势。

在这里插入图片描述

1.克隆对象

究其本质,克隆操作时Java虚拟机会进行内存操作,直接拷贝原型对象数据流生成新的副本对象,绝不会拖泥带水地触发一些多余的复杂操作(如类加载、实例化、初始化等),所以其效率远远高于“new”关键字所触发的实例化操作。

我以路人甲类Passerby为例,通过实现java.lang包中的克隆接口Cloneable,并在实现的clone()方法中调用了父类Object的克隆方法,如此一来外部就能够对本类的实例进行克隆操作了,省去了由类而生的再造过程。注意,对于属性中的引用类型需要深拷贝才能完全彻底地克隆这个实例的所有。

  • 可被克隆的Passerby类
package com.wang.design.chapter2;

/**
 * @author Tracy
 *
 * 2-1 创建型——原型模式
 */
public class Passerby implements Cloneable{
   
    // x坐标和y坐标
    private int x;
    private int y;
    private Equipment equipment;

    public Passerby(int x, int y) {
   
        this.x = x;
        this.y = y;
    }

    public void setX(int x) {
   
        this.x = x;
    }

    public void setEquipment(Equipment equipment) {
   
        this.equipment = equipment;
    }

    public int getY() {
   
        return y;
    }

    public void setY(int y) {
   
        this.y = y;
    }

    //行走
    public void walk(){
   
        ++y;
    }

    //奔跑
    public void run(){
   
        y+=5;
    }

    //重写克隆方法
    @Override
    protected Passerby clone() throws CloneNotSupportedException {
   
        Passerby passerby=(Passerby)super.clone();
        passerby.setEquipment(this.equipment.clone());//深拷贝
        return passerby;
    }
}

class Equipment implements Cloneable{
   
    @Override
    protected Equipment clone() throws CloneNotSupportedException {
   
        return (Equipment)super.clone();
    }
}

2.克隆工厂

定义克隆工厂类可以更方便地生产对象。

package com.wang.design.chapter2;

/**
 * @author Tracy
 *
 * 2-2 创建型——原型模式——克隆工厂
 */
public class PasserbyFactory {
   
    //单例饿汉模式先创建一个Passerby原型
    private static Passerby passerby=new Passerby(0,0);

    //获取克隆实例
    public static Passerby getInstance(){
   
        try{
   
            Passerby one=passerby.clone();
            return one;
        }catch (Exception e){
   
            e.printStackTrace();
        }
        return null;
    }
}

三、创建型3——工厂模式

程序设计中的工厂类往往是对对象构造、实例化、初始化过程的封装,而工厂方法(FactoryMethod)则可以升华为一种设计模式,它对工厂制造方法进行接口规范化,以允许子类工厂决定具体制造哪类产品的实例,最终降低系统耦合,使系统的可维护性、可扩展性等得到提升。

我们都知道,传统的实例构造方式就是使用new关键字,但这样会导致客户端与实例化对象这个过程产生耦合,但如果我们把生产过程交给一个工厂类,由工厂类来完成实例化、初始化等过程,那么我们就能摆脱传统生产方式带给我们的束缚。

实现工厂模式并不是简单地把生产对象这个过程换一个位置,如果这样做,当工厂类需要扩展的时候必然会需要不断地重写它,这样做事实上也并没有太多优越性。相反,我们力求创建一个便于管理的、可扩展的工厂体系。

1.实体类

  • 乘客实体类
package com.wang.design.chapter3;

import java.util.Random;

/**
 * @author tracy
 * 3——工厂模式——乘客实体类
 */
public class Passenger {
   
    private int x,y;
    private int gender;//0表示女性 1表示男性

    public Passenger(int x,int y){
   
        this.x=x;
        this.y=y;
        this.gender=new Random().nextInt(2);
    }

    public void show(){
   
        System.out.println("乘客 坐标["+x+","+y+"]");
    }
}

2.工厂类

下面的Factory接口是工厂模式的核心,对于每个实体类都可以创建一个专门的工厂类来生产它,这样就能使代码具有低耦合、可扩展性。

package com.wang.design.chapter3;

/**
 * @author tracy
 * 3——工厂模式——工厂类
 */
public interface Factory {
   
    Passenger create(int x,int y);
}

class PassengerFactory implements Factory{
   
    @Override
    public Passenger create(int x,int y) {
   
        return new Passenger(x,y);
    }
}
  • 客户端
package com.wang.design.chapter3;

/**
 * 3-工厂模式-客户端
 */
public class Client {
   
    public static void main(String[] args) {
   
        System.out.println("begin");
        
		//批量生产10个乘客
        Factory factory=new PassengerFactory();
        for(int i=0;i<10;++i){
   
            factory.create(0,0).show();
        }

        System.out.println("end");
    }
}

//执行结果
//begin
//乘客 坐标[0,0]
//乘客 坐标[0,0]
//乘客 坐标[0,0]
//乘客 坐标[0,0]
//乘客 坐标[0,0]
//乘客 坐标[0,0]
//乘客 坐标[0,0]
//乘客 坐标[0,0]
//乘客 坐标[0,0]
//乘客 坐标[0,0]
//end

在工厂方法模式中,不仅产品需要分类,工厂同样需要分类,与其把所有生产方式堆积在一个简单工厂类中,不如把生产方式放在具体的子类工厂中去实现,这样做对工厂的抽象化与多态化有诸多好处,避免了由于新加入产品类而反复修改同一个工厂类所带来的困扰,使后期的代码维护以及扩展更加直观、方便。

四、创建型4——建造者模式

建造者模式的主要目的在于把烦琐的构建过程从不同对象中抽离出来,使其脱离并独立于产品类与工厂类,最终实现用同一套标准的制造工序能够产出不同的产品。

这一章的代码参考自《秒懂设计模式》。

  • 明确角色

我们以房屋施工为例。开发商找了施工方来为一块规划地修建别墅和公寓,由施工方提供两个施工队来对不同的建筑进行施工。由于开发商不是完全放心将任务全权交给施工方,因此又聘请了一位工程总监来对施工队的施工工作进行指导和监督。角色关系如下所示。

在这里插入图片描述

我们将建筑物简化为三个模块:地基、墙体、屋顶,施工队需要分别对三个模块进行施工和组装。

1.建筑物

package com.wang.design.chapter4;

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

/**
 * @author tracy
 * 4-建造者模式-建筑物类
 */
public class Building {
   
    private List<String> buildingComponents=new ArrayList<>();//组装组件

    public void setBasement(String basement){
   //修建地基
        buildingComponents.add(basement);
    }

    public void setWall(String wall){
   //修建墙体
        buildingComponents.add(wall);
    }

    public void setRoof(String roof){
   //修建屋顶
        buildingComponents.add(roof);
    }

    @Override
    public String toString() {
   
        String str="";
        for(int i = buildingComponents.size()-1;i>=0;--i){
   
            str += buildingComponents.get(i);
        }
        return str;
    }

}

2.施工队

package com.wang.design.chapter4;

/**
 * @author tracy
 * 3-建造者模式-施工队
 */
public interface Builder {
   
    void buildBasement();
    void buildWall();
    void buildRoof();
    Building getBuilding();
}

/**
 * 别墅施工队
 */
class HouseBuilder implements Builder{
   
    private Building house;

    public HouseBuilder(){
   
        house=new Building();
    }

    /**
     * 施工三步曲
     */
    @Override
    public void buildBasement() {
   
        System.out.println("建造地基");
        house.setBasement("╬╬╬╬╬╬╬╬╬╬\n");
    }

    @Override
    public void buildWall() {
   
        System.out.println("建造墙体");
        house.setWall("|田|田 田|\n");
    }

    @Override
    public void buildRoof() {
   
        System.out.println("建造屋顶");
        house.setRoof("╱◥███████◣\n");
    }

    @Override
    public Building getBuilding() {
   
        return house;
    }
}

/**
 * 公寓施工队
 */
class ApartmentBuilder implements Builder{
   
    private Building apartment;

    public ApartmentBuilder(){
   
        apartment=new Building();
    }

    /**
     * 施工三步曲
     */
    @Override
    public void buildBasement() {
   
        System.out.println("建造地基");
        apartment.setBasement("╚═════════╝\n");
    }

    @Override
    public void buildWall() {
   
        System.out.println("建造墙体");
        for (int i = 0; i < 8; i++) {
   // 8层
            apartment.setWall("║ □ □ □ □ ║\n");
        }
    }

    @Override
    public void buildRoof() {
   
        System.out.println("建造屋顶");
        apartment.setRoof("╔═════════╗\n");
    }

    @Override
    public Building getBuilding() {
   
        return apartment;
    }
}

3.工程监理

package com.wang.design.chapter4;

/**
 * 4-建造者模式-工程监理
 */
public class Director {
   
    private Builder builder;

    public void setBuilder(Builder builder) {
   
        this.builder = builder;
    }

    /**
     * 控制施工流程
     * @return
     */
    public Building direct(){
   
        System.out.println("=====工程项目启动=====");
        builder.buildBasement();
        builder.buildWall();
        builder.buildRoof();
        System.out.println("=====工程项目竣工=====");
        return builder.getBuilding();
    }
}

4.实际施工

package com.wang.design.chapter4;

/**
 * @author tracy
 * 4-建造者模式-实际施工
 */
public class Client {
   
    public static void main(String[] args) {
   
        Director director=new Director();
        //监理别墅施工队
        director.setBuilder(new HouseBuilder());
        System.out.println(director.direct());
        //监理公寓施工队
        director.setBuilder(new ApartmentBuilder());
        System.out.println(director.direct());
    }
}
  • 运行效果

在这里插入图片描述

五、结构型1——门面模式

这个模式的精髓就在于“封装”二字,把暴露在客户端的细节封装成一个大的系统,仅留给外部一个接口。只看文字描述可能不够直观,下面直接上代码。

  • 使用门面模式前

下面我以【上一门课】为例。我们都知道,一门课需要经过上课考勤、提问、作业、考试几个步骤,如果不使用门面模式,代码如下:

package com.wang
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TracyCoder123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值