设计模式:结构型模式-代理、适配、装饰模式

结构型模式

结构型模式描述如何将类或对象按某种布局组成更大的结构。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者采用组合或聚合来组合对象。

由于组合关系或聚合关系比继承关系耦合度低,满足 “合成复用原则”,所以对象结构型模式比类结构型模式具有更大的灵活性。

结构型模式分为以下7种:

  • 代理模式
  • 适配器模式
  • 装饰者模式
  • 桥接模式
  • 外观模式
  • 组合模式
  • 享元模式

代理模式:

概述:

由于某些原因需要给某对象提供一个代理以控制对该对象的访问。这时,访问对象不适合或者不能直接引用目标对象,代理对象作为访问对象和目标对象之间的中介。

Java中的代理按照代理类生成时机不同又分为 静态代理动态代理。静态代理代理类在编译期就生成,而动态代理代理类则是在Java运行时动态生成。动态代理又有 JDK 代理和 CGLib 代理两种

结构:

代理(Proxy)模式分为三种角色:

  • 抽象主题(Subject)类:通过接口或抽象类声明真实主题和代理对象实现的业务方法。
  • 真实主题(Real Subject)类:实现了抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象。
  • 代理(Proxy)类:提供了与真实主题相同的接口,其内部含有对真实主题的引用,它可以访问、控制或扩展真实主题的功能。

静态代理:

如果要买火车票的话,需要去火车站买票,坐车到火车站,排队等一系列的操作,显然比较麻烦。而火车站在多个地方都有代售点,我们去代售点买票就方便很多了。这个例子其实就是典型的代理模式,火车站是目标对象,代售点是代理对象。

//买票接口
public interface SellTickets {
    void sell();
}

//火车站类
public class TrainStation implements SellTickets {
    @Override
    public void sell() {
        System.out.println("火车站卖票");
    }
}

//代售点类
public class ProxyPoint implements SellTickets {
    //声明火车站类
    private final TrainStation trainStation = new TrainStation();

    @Override
    public void sell() {
        System.out.println("代售点收取服务费");
        trainStation.sell();
    }
}

//测试类
public class Client {
    public static void main(String[] args) {
        //创建代售点对象
        ProxyPoint proxyPoint = new ProxyPoint();
        //通过代售点买火车票
        proxyPoint.sell();
    }
}

从上面代码中可以看出测试类直接访问的是 ProxyPoint 类对象,也就是说 ProxyPoint 作为访问对象和目标对象的中介。同时也对 sell() 方法进行了增强(代理点收取一些服务费用)。

JDK动态代理:

接下来我们使用动态代理实现上面案例,先说说JDK提供的动态代理。Java中提供了一个动态代理类 ProxyProxy 并不是我们上述所说的代理对象的类,而是提供了一个创建代理对象的静态方法(newProxyInstance方法)来获取代理对象。

相关业务代码:
//买票接口
public interface SellTickets {
    void sell();
}


//火车站类
public class TrainStation implements SellTickets {
    @Override
    public void sell() {
        System.out.println("火车站卖票");
    }
}


//获取代理对象工厂类,代理类也实现了对应的接口
public class ProxyFactory {
    //声明目标对象
    private final TrainStation trainStation = new TrainStation();

    //获取代理对象的方法
    public SellTickets getProxyObject() {
        //返回代理对象
        /*
         * Proxy.newProxyInstance()参数说明
         * ClassLoader loader:类加载器,用于加载代理类。可以通过目标对象获取类加载器
         * Class<?>[] interfaces:代理类实现的接口的字节码对象
         * InvocationHandler h:代理对象的调用处理程序
         */
        SellTickets proxyObject = (SellTickets) Proxy.newProxyInstance(
                trainStation.getClass().getClassLoader(),
                trainStation.getClass().getInterfaces(),
                new InvocationHandler() {
                    /**
                     * Object invoke:代理对象和proxyObject对象是同一个对象,在invoke方法中基本不用
                     * Method method:对接口中进行封装的代理对象
                     * Object[] args:调用方法的实际参数
                     * 返回值:就是方法的返回值
                     */
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        System.out.println("invoke方法执行了");
                        //执行目标对象的方法
                        Object invoke = method.invoke(trainStation, args);
                        System.out.println("代售点收取服务费用:JDK动态代理");
                        return invoke;
                    }
                }
        );
        return proxyObject;
    }
}

//测试类
public class Client {
    public static void main(String[] args) {
        //创建代理工厂对象
        ProxyFactory proxyFactory = new ProxyFactory();
        //使用代理工厂对象获取代理对象
        SellTickets proxyObject = proxyFactory.getProxyObject();
        //调用买票的方法
        proxyObject.sell();
    }
}
问题:

使用了动态代理,我们思考下面问题:

ProxyFactory 是代理类吗?

ProxyFactory 不是代理模式中所说的代理类,而代理类是程序在运行过程中动态的在内存中生成的类。通过阿里巴巴开源的Java诊断工具(Arthas【阿尔萨斯】)查看代理类的结构:

//先确保程序处于运行状态,并获取代理类的全路径名称
//使用Arthas的jar 获取代理类结构,选中对应的运行主类接口
//使用 jad 代理类的全路径名称,即可查看代理的结构
public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.proxy.jdk_proxy.SellTickets").getMethod("sell", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
            return;
        } catch (NoSuchMethodException noSuchMethodException) {
            throw new NoSuchMethodError(noSuchMethodException.getMessage());
        } catch (ClassNotFoundException classNotFoundException) {
            throw new NoClassDefFoundError(classNotFoundException.getMessage());
        }
    }

    public final boolean equals(Object object) {
        try {
            return (Boolean) this.h.invoke(this, m1, new Object[]{object});
        } catch (Error | RuntimeException throwable) {
            throw throwable;
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final String toString() {
        try {
            return (String) this.h.invoke(this, m2, null);
        } catch (Error | RuntimeException throwable) {
            throw throwable;
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final int hashCode() {
        try {
            return (Integer) this.h.invoke(this, m0, null);
        } catch (Error | RuntimeException throwable) {
            throw throwable;
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

    public final void sell() {
        try {
            this.h.invoke(this, m3, null);
            return;
        } catch (Error | RuntimeException throwable) {
            throw throwable;
        } catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }
}

从上面的类中,我们可以看到以下几个信息:

  • 代理类($Proxy0)实现了 SellTickets。这也就印证了我们之前说的真实类和代理类实现同样的接口。

  • 代理类($Proxy0)将我们提供了的匿名内部类对象传递给了父类。

动态代理的执行流程是什么样的?

//程序运行过程中动态生成的代理类
public final class $Proxy0 extends Proxy implements SellTickets {
    private static Method m3;

    public $Proxy0(InvocationHandler invocationHandler) {
        super(invocationHandler);
    }

    static {
        try {
            m3 = Class.forName("com.proxy.jdk_proxy.SellTickets").getMethod("sell", new Class[0]);
        } catch (NoSuchMethodException | ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    public final void sell() {
        try {
            this.h.invoke(this, m3, null);
        } catch (Throwable e) {
            e.printStackTrace();
        }
    }
}

//java提供的动态代理相关类
public class Proxy implements java.io.Serializable {
    protected InvocationHandler h;
    
	protected Proxy(InvocationHandler h) {
        Objects.requireNonNull(h);
        this.h = h;
    }
}

//上面的相关业务代码……

执行流程如下:

  1. 在测试类中通过代理对象调用 sell() 方法
  2. 根据多态的特性,执行的是代理类($Proxy0)中的 sell() 方法
  3. 代理类($Proxy0)中的 sell() 方法中又调用了 InvocationHandler接口的自实现类对象的 invoke 方法
  4. invoke方法通过反射执行了真实对象所属类(TrainStation)中的 sell() 方法

CGLIB动态代理:

同样是上面的案例,我们再次使用CGLIB代理实现。

**如果没有定义sellrickets接口,只定义了TxainStation (火车站类)。**很显然JDK代理是无法使用了,因为JDpK动态代理要求必须定义接口,对接口进行代理。

CGLIB是一个功能强大,高性能的代码生成包。它为没有实现接口的类提供代理,为JDK的动态代理提供了很好的补充。

CGLIB是第三方提供的包,所以需要引入jar包的坐标:

<!-- https://mvnrepository.com/artifact/cglib/cglib -->
<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.3.0</version>
</dependency>

要实现:MethodInterceptor 接口,动态代理时会执行该接口中的 intercept 的方法

//火车站类
public class TrainStation {
    public void sell() {
        System.out.println("火车站卖票");
    }
}


//代理对象工厂,用来获取代理对象
public class ProxyFactory implements MethodInterceptor {
    //声明火车站对象
    private final TrainStation trainStation = new TrainStation();

    public TrainStation getProxyObject() {
        //创建Enhancer对象,类似JDK代理中的Proxy类
        Enhancer enhancer = new Enhancer();
        //设置父类的字节码对象
        enhancer.setSuperclass(TrainStation.class);
        //设置回调函数
        enhancer.setCallback(this);
        //创建代理对象
        TrainStation proxyTrainStation = (TrainStation) enhancer.create();
        return proxyTrainStation;
    }

    @Override
    public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
        System.out.println("CGLIB 动态代理中的intercept方法执行了");
        System.out.println("代售点收取服务费用:CGLIB动态代理");
        //要调用目标对象的方法
        return method.invoke(trainStation, objects);
    }
}

//测试类
public class Client {
    public static void main(String[] args) {
        //创建代理工厂对象
        ProxyFactory proxyFactory = new ProxyFactory();
        //获取代理对象
        TrainStation trainStation = proxyFactory.getProxyObject();
        //调用代理对象中的sell方法卖票
        trainStation.sell();
    }
}

三种代理的对比:

  • JDK代理 和 CGLIB代理
    使用CGLib实现动态代理,CGLib底层采用ASM字节码生成框架,使用字节码技术生成代理类在JDK1.6之前比使用Java反射效率要高。唯一需要注意的是,CGLib不能对声明为final的类或者方法进行代理,因为CGLib原理是动态生成被代理类的子类。在JDK1.6、JDK1.7、JDK1.8逐步对JDK动态代理优化之后,在调用次数较少的情况下,JDK代理效率高于CGLIB代理效率,只有当进行大量调用的时候,JDK1.6和JDK1.7比CGLIB代理效率低一点,但是到JDK1.8的时候,JDK代理效率高于CGLIB代理。所以如果有接口使用JDK动态代理,如果没有接口使用CGLIB代理
  • 动态代理 和 静态代理
    动态代理与静态代理相比较,最大的好处是接口中声明的所有方法都被转移到调用处理器一个集中的方法中处理(InvocationHandler.invoke)。这样,在接口方法数量比较多的时候,我们可以进行灵活处理,而不需要像静态代理那样每一个方法进行中转。如果接口增加一个方法,静态代理模式除了所有实现类需要实现这个方法外,所有代理类也需要实现此方法。增加了代码维护的复杂度。而动态代理不会出现该问题。

优缺点:

优点:
  • 代理模式在客户端与臼标对象之间起到一个中介作用和保护目标对象的作用;
  • 代理对象可以扩展目标对象的功能;
  • 代理模式能将客户端与目标对象分离,在一定程度上降低了系统的耦合度;
缺点:
  • 增加了系统的复杂度;

使用场景:

  • 远程(Remote)代理
    本地服务通过网络请求远程服务。为了实现本地到远程的通信,我们需要实现网络通信,处理其中可能的异常。为良好的代码设计和可维护性,我们将网络通信部分隐藏起来,只暴露给本地服务一个接口,通过该接口即可访问远程服务提供的功能,而不必过多关心通信部分的细节。
  • 防火墙(Firewall)代理
    当你将浏览器配置成使用代理功能时,防火墙就将你的浏览器的请求转给互联网;当互联网返回响应时,代理服务器再把它转给你的浏览器。
  • 保护(Protect or Access)代理
    控制对一个对象的访问,如果需要,可以给不同的用户提供不同级别的使用权限。

适配器模式:

将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能一起工作的那些类能一起工作。

适配器模式分为类适配器模式对象适配器模式,前者类之间的耦合度比后者高,且要求程序员了解现有组件库中的相关组件的内部结构,所以应用相对较少些。

结构:

适配器模式(Adapter)包含以下主要角色:

  • 目标(Target)接口:当前系统业务所期待的接口,它可以是抽象或接口。
  • 适配者(Adaptee)类:它是被访问和适配的现存组件库中的组件接口。
  • 适配器(Adapter)类:它是一个转换器,通过继承或引起适配者的对象,把适配者接口转换成目标接口,让客户按目标接口的格式访问适配者。

类适配器模式:

实现方式:定义一个适配器类来实现当前系统的业务接口,同时又继承现有组件库中已经存在的组件。

//计算机类
public class Computer {
    //从SD卡中读取数据
    public String readSD(SDCard sdCard) {
        if (sdCard == null) {
            throw new NullPointerException("SD is notFind");
        }
        return sdCard.readSD();
    }
}

//目标接口SD卡
public interface SDCard {
    //从SD卡中读取数据
    String readSD();

    //往SD卡中写数据
    void writeSD(String msg);
}

//具体的SD卡
public class SDCardImpl implements SDCard {
    @Override
    public String readSD() {
        return "SDCard read msg";
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("SDCard write msg: " + msg);
    }
}

//适配者类的接口
public interface TFCard {
    //从TF卡中读取数据
    String readTF();

    //往TF卡中写数据
    void writeTF(String msg);
}

//适配者类
public class TFCardImpl implements TFCard{
    @Override
    public String readTF() {
        return "TFCard read msg";
    }

    @Override
    public void writeTF(String msg) {
        System.out.println("TFCard write msg: "+msg);
    }
}

//适配器类
public class SDAdapterTF extends TFCardImpl implements SDCard {
    @Override
    public String readSD() {
        System.out.println("使用适配器");
        return readTF();
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("使用适配器");
        writeTF(msg);
    }
}

//测试方法
public static void main(String[] args) {
    //创建计算机类
    Computer computer = new Computer();
    //创建SDCard类
    SDCard sdCard = new SDCardImpl();
    //读取SDCard中的数据
    System.out.println(computer.readSD(sdCard));
    System.out.println("========================");
    //使用该电脑读取TF卡中的数据
    //定义适配器类
    System.out.println(computer.readSD(new SDAdapterTF()));
}

类适配器模式违背了合成复用原则。类适配器是客户类有一个接口规范的情况下可用,反之不可用。

对象适配器模式:

实现方式:对象适配器模式可采用将现有组件库中已经实现的组件引入适配器类中,该类同时实现当前系统的业务接口。

将上面代码改写:仅修改适配器类

//适配器类
public class SDAdapterTF implements SDCard {
    //声明适配者类
    private final TFCard tfCard;

    public SDAdapterTF(TFCard tfCard) {
        this.tfCard = tfCard;
    }

    @Override
    public String readSD() {
        System.out.println("使用适配器");
        return tfCard.readTF();
    }

    @Override
    public void writeSD(String msg) {
        System.out.println("使用适配器");
        tfCard.writeTF(msg);
    }
}

//测试类
public static void main(String[] args) {
    //创建计算机类
    Computer computer = new Computer();
    //创建SDCard类
    SDCard sdCard = new SDCardImpl();
    //读取SDCard中的数据
    System.out.println(computer.readSD(sdCard));
    System.out.println("========================");
    //使用该电脑读取TF卡中的数据
    //定义适配器类对象
    TFCard tfCard = new TFCardImpl();
    SDAdapterTF sdAdapterTF = new SDAdapterTF(tfCard);
    System.out.println(computer.readSD(sdAdapterTF));
}

注意:还有一个适配器模式是接口适配器模式。当不希望实现一个接口中所有的方法时,可以创建一个抽象类Adapter,实现所有方法。而此时我们只需要继承该抽象类即可。

应用场景:

  • 以前开发的系统存在满足新系统功能需求的类,但其接口同新系统的接口不一致。
  • 使用第三方提供的组件,但组件接口定义和自己要求的接口定义不同。

JDK源码解析:

Reader(字符流) 、InputStream(字节流)的适配使用的是就是InputStreamReader和OutputStreamWriter。InputStreamReader和OutputStreamWriter分别继承自java.io包中的Reader和writer,对他们中的抽象的未实现的方法给出实现。如:

//InputStreamReader 类
private final StreamDecoder sd;

public int read(char cbuf[], int offset, int length) throws IOException {
    return sd.read(cbuf, offset, length);
}

//OutputStreamWriter 类
private final StreamEncoder se;

public void write(char cbuf[], int off, int len) throws IOException {
    se.write(cbuf, off, len);
}

装饰者模式:

使用传统继承者模式存在的问题:

  • 扩展性不好
  • 产生过多的子类

定义:

​ 指在不改变现有对象结构的情况下,动态地给该对象增加一些职责(即增加其额外功能)的模式。

结构:

装饰(Decorator)模式中的角色:

  • 抽象构件(Component)角色︰定义一个抽象接口以规范准备接收附加责任的对象。
  • 具体构件(Concrete Component)角色︰实现抽象构件,通过装饰角色为其添加一些职责。
  • 抽象装饰(Decorator)角色︰继承或实现抽象构件,并包含具体构件的实例,可以通过其子类扩展具体构件的功能。
  • 具体装饰(Concrete Decorator)角色︰实现抽象装饰的相关方法,并给具体构件对象添加附加的责任。

案例:

//快餐类
public abstract class FastFood {
    //价格
    private float price;
    //描述
    private String desc;

    public float getPrice() {
        return price;
    }

    public void setPrice(float price) {
        this.price = price;
    }

    public String getDesc() {
        return desc;
    }

    public void setDesc(String desc) {
        this.desc = desc;
    }

    public FastFood(float price, String desc) {
        this.price = price;
        this.desc = desc;
    }

    public FastFood() {
    }

    public abstract float cost();
}

//炒饭类(具体地构建角色)
public class FriedRice extends FastFood {
    public FriedRice() {
        super(10, "炒饭");
    }

    @Override
    public float cost() {
        return getPrice();
    }
}

//炒面(具体地构建角色)
public class FriedNoodles extends FastFood {

    public FriedNoodles() {
        super(12, "炒面");
    }

    @Override
    public float cost() {
        return getPrice();
    }
}

//装饰者类(抽象装饰者角色)
public abstract class Garnish extends FastFood {
    //声明快餐类的变量
    private FastFood fastFood;

    public FastFood getFastFood() {
        return fastFood;
    }

    public void setFastFold(FastFood fastFood) {
        this.fastFood = fastFood;
    }

    public Garnish(float price, String desc, FastFood fastFood) {
        super(price, desc);
        this.fastFood = fastFood;
    }
}

//鸡蛋类(具体的装饰者角色)
public class Egg extends Garnish {
    public Egg(FastFood fastFood) {
        super(1, "鸡蛋", fastFood);
    }

    @Override
    public float cost() {
        //计算价格
        return getPrice() + getFastFood().cost();
    }

    @Override
    public String getDesc() {
        return super.getDesc() + getFastFood().getDesc();
    }
}

//培根类(具体的装饰者角色)
public class Bacon extends Garnish {
    public Bacon(float price, String desc, FastFood fastFood) {
        super(2, "培根", fastFood);
    }

    public Bacon(FastFood fastFood) {
        super(2, "培根", fastFood);
    }

    @Override
    public float cost() {
        //计算价格
        return getPrice() + getFastFood().cost();
    }

    @Override
    public String getDesc() {
        return super.getDesc() + getFastFood().getDesc();
    }
}

//测试类
 public static void main(String[] args) {
     FastFood food = new FriedRice();
     System.out.println(food.getDesc() + " : " + food.cost() + " 元");
     System.out.println("=================");
     //在上面的炒饭中加一个鸡蛋
     food = new Egg(food);
     System.out.println(food.getDesc() + " : " + food.cost() + " 元");
     System.out.println("=================");
     //在上面的炒饭中再加一个鸡蛋
     food = new Egg(food);
     System.out.println(food.getDesc() + " : " + food.cost() + " 元");
     System.out.println("=================");
     //在上面的炒饭中加一个培根
     food = new Bacon(food);
     System.out.println(food.getDesc() + " : " + food.cost() + " 元");
}
输出结果:
炒饭 : 10.0=================
鸡蛋炒饭 : 11.0=================
鸡蛋鸡蛋炒饭 : 12.0=================
培根鸡蛋鸡蛋炒饭 : 14.0

好处:

  • 饰者模式可以带来比继承更加灵活性的扩展功能,使用更加方便,可以通过组合不同的装饰者对象来获取具有不同行为状态的多样化的结果。装饰者模式比继承更具良好的扩展性,完美的遵循开闭原则,继承是静态的附加责任,装饰者则是动态的附加责任。
  • 装饰类和被装饰类可以独立发展,不会相互耦合,装饰模式是继承的一个替代模式,装饰模式可以动态扩展一个实现类的功能。

使用场景

  • 当不能采用继承的方式对系统进行扩充或者采用继承不利于系统扩展和维护时。不能采用继承的情况主要有两类:
    • 第一类是系统中存在大量独立的扩展,为支持每一种组合将产生大量的子类,使得子类数目呈爆炸性增长;
    • 第二类是因为类定义不能继承(如final类)
  • 在不影响其他对象的情况下,以动态、透明的方式给单个对象添加职责。
  • 当对象的功能要求可以动态地添加,也可以再动态地撤销时。

JDK源码解析:

io流中的包装类使用到了装饰者模式。BufferedInputstreamBufferedoutputstreamBufferedReaderBufferedWriter
我们以BufferedWriter举例来说明,先看看如何使用BufferedWriter:

public static void main(String[] args) {
    try {
        //创建BufferedWriter对象
        //创建FileWriter对象
        FileWriter fileWriter = new FileWriter("D:\\a.txt");
        BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);
        //写数据
        bufferedWriter.write("a");
        bufferedWriter.close();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

BufferedWriter使用装饰者模式对writer子实现类进行了增强,添加了缓冲区,提高了写数据的效率。

代理和装饰者模式的区别:

静态代理和装饰者模式的区别:

  • 相同点:
    • 都要实现与目标类相同的业务接口。在两个类中都要声明目标对象
    • 都可以在不修改目标类的前提下增强目标方法。
  • 不同点:
    • 目的不同
      装饰者是为了增强目标对象
      静态代理是为了保护和隐藏目标对象。
    • 获取目标对象构建的地方不同
      装饰者是由外界传递进来,可以通过构造方法传递
      静态代理是在代理类内部创建,以此来隐藏目标对象
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值