面向对象的六大原则

一、面向对象的六大原则

       现在的编程的主流语言基本上都是面向对象的。我们知道,面向对象是一种编程思想,包括三大特性和六大原则,其中,三大特性指的是封装、继承多态;六大原则指的是单一职责原则、开闭式原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则其中,单一职责原则是指一个类应该是一组相关性很高的函数和数据的封装,这是为了提高程序的内聚性,而其他五个原则是通过抽象来实现的,目的是为了降低程序的耦合性以及提高可扩展性。在应用的开发过程中,最难的不是完成应用的开发工作,而是在后续的升级、维护过程中让系统能够拥抱变化。拥抱变化也意味着在满足需求且不破坏系统稳定性的前提下保持高可扩展性、高内聚、低耦合,在经历了各个版本的变更之后依然能够保持清晰、灵活、稳定的系统架构。当然,这是一种比较理想的情况,由于各种各样的原因(开发水平差、工期短、产品奇葩需求等),我们的应用可能会变得难以维护,但是我们必须向更好的方向努力,那么遵循面向对象的六大原则就是我们走向灵活软件之路所迈出的第一步。本篇文章将以实现一个简单的图片加载框架为例,来说明面向对象六大原则的应用。

二、图片加载框架需求分析

需求描述

1、需要根据Url将图片加载到对应的ImageView上;

2、需要图片缓存功能;

3、框架提供的API尽可能简单,方便使用;

4、框架能够灵活的扩展,比如灵活的改变缓存功能、下载图片方法等;

需求分析

分析一下需求,我们至少需要使用以下技术:

1、首先,我们需要根据Url下载图片,这里暂定使用UrlConnection;

2、为了不阻塞UI线程,我们下载图片需要在子线程中执行,方便起见,我们直接使用线程池;

3、在子线程中下载图片后,我们需要将图片显示到ImageView上,由于Android的特性,我们需要在UI线程中更新UI,所以这里需要使用Handler来切换线程到UI线程中显示图片;

4、图片需要缓存功能,一般图片缓存需要有内存缓存和文件缓存,内存缓存就是将下载好的图片保存在内存中,下次使用时直接从内存中获取,这里采用Lru算法来控制图片缓存,文件缓存即将图片缓存到文件中,下次使用直接从文件中获取,相对来说,内存缓存会占用较多的内存,但是效率较高。

三、源码实现

我们首先只实现功能,而不管代码的好坏,后面我们再根据六大原则对代码进行优化,看看遵循六大原则究竟能够带来哪些好处。

public class ImageLoader {
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    private LruCache<String, Bitmap> mLruCache = new LruCache<>(maxMemory / 4); // 内存缓存
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); // 下载图片使用的线程池
    private Handler mDealHandler = new Handler() { // 处理下载好图片的Handler
        @Override
        public void handleMessage(Message msg) {
            if (msg != null && msg.obj != null) {
                ImageResponse imageResponse = (ImageResponse) msg.obj;
                imageResponse.imageView.setImageBitmap(imageResponse.bitmap); // 将图片显示到ImageView上
                mLruCache.put(imageResponse.imageUrl, imageResponse.bitmap); // 将图片保存到缓存中
            }
        }
    };

    /**
     * 加载图片
     * @param imageView
     * @param imageUrl
     */
    public void displayImage(final ImageView imageView, final String imageUrl) {
        if (imageView == null || TextUtils.isEmpty(imageUrl)) {
            return;
        }

        Bitmap cacheBitmap = mLruCache.get(imageUrl);
        if (cacheBitmap == null) { // 使用缓存失败
            mExecutorService.submit(new Runnable() { // 使用线程池下载图片
                @Override
                public void run() {
                    Bitmap bitmap = downLoadImage(imageUrl); // 下载图片
                    if (bitmap != null) {
                        ImageResponse imageResponse = new ImageResponse(); // 图片下载好后,将信息封装成Message发送给Handler处理
                        imageResponse.bitmap = bitmap;
                        imageResponse.imageUrl = imageUrl;
                        imageResponse.imageView = imageView;
                        Message message = Message.obtain();
                        message.obj = imageResponse;
                        mDealHandler.sendMessage(message);
                    }
                }
            });
        } else {
            imageView.setImageBitmap(cacheBitmap);
        }
    }

    // 下载图片
    public Bitmap downLoadImage(final String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(connection.getInputStream());
            connection.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return bitmap;
    }
    
    public static class ImageResponse {
        public Bitmap bitmap;
        public ImageView imageView;
        public String imageUrl;
    }
}

使用方法:

    ImageLoader mImageLoader = new ImageLoader();
    mImageLoader.displayImage(imageView, imageUrl);

在上面的代码中,我们不管三七二十一,将所有的功能都堆积到了ImageLoader这个类中,ImageLoader不仅承担了提供Api接口的功能,同时还承担了具体实现细节的功能,比如缓存功能、下载图片功能、显示图片功能等等,这样虽然也能实现图片加载的效果,但是确会让我们的代码变得臃肿复杂并且难以维护,不说别人看不懂,可能过个几天后连自己都看不懂了,所以,我们自然而然的要想到,我们需要根据功能对类进行细分,将某些相关性很强的功能分到独立的类中去处理,比如,图片的缓存我们可以划分为一个类,图片的下载可以作为另外一个类,通过类的划分,我们可以让代码变得清晰,这也就是面向对象六大原则中的第一个原则----单一职责原则

优化代码第一步----单一职责原则----高内聚

单一职责原则的定义是:就一个类而言,应该只有一个引起它变化的原因。简单来说,一个类应该是一组相关性很高的函数和数据的封装。因为单一职责的划分界限并不是那么清晰,每个人的理解不一样,这就导致了不同的人划分的类承担的职责也不一样,就图片加载的例子来说,可能有的人就认为整个图片加载是一组相关性很高的功能,于是将其放入在一个类中处理。一般来说,我们首先需要具备单一职责原则的思想,如果发现一个类承担了太多的功能,这个时候就要考虑将某些功能划分到其他类中去处理,具体的划分细节要平开发者的个人经验。

下面,我们利用单一职责原则的思想对ImageLoader进行改造,根据功能来说,下载图片和图片缓存应该属于单独的功能,我们可以分别用两个类来实现,然后ImageLoader调用这两个类即可:

public class ImageLoader {
    ImageCache mImageCache = new ImageCache();
    RequestImage mRequestImage = new RequestImage();
    
    private Handler mDealHandler = new Handler() { // 处理下载好图片的Handler
        @Override
        public void handleMessage(Message msg) {
            if (msg != null && msg.obj != null) {
                ImageResponse imageResponse = (ImageResponse) msg.obj;
                imageResponse.imageView.setImageBitmap(imageResponse.bitmap); // 将图片显示到ImageView上
                mImageCache.put(imageResponse.imageUrl, imageResponse.bitmap); // 将图片保存到缓存中
            }
        }
    };

    /**
     * 加载图片
     * @param imageView
     * @param imageUrl
     */
    public void displayImage(final ImageView imageView, final String imageUrl) {
        if (imageView == null || TextUtils.isEmpty(imageUrl)) {
            return;
        }

        Bitmap cacheBitmap = mImageCache.get(imageUrl);
        if (cacheBitmap == null) { // 使用缓存失败
            mRequestImage.requestImage(imageView, imageUrl, mDealHandler);
        } else {
            imageView.setImageBitmap(cacheBitmap);
        }
    }
    
    public static class ImageResponse {
        public Bitmap bitmap;
        public ImageView imageView;
        public String imageUrl;
    }
}
public class RequestImage {
    ExecutorService mExecutorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); // 下载图片使用的线程池

    public void requestImage(final ImageView imageView, final String imageUrl, final Handler handler) {
        mExecutorService.submit(new Runnable() { // 使用线程池下载图片
            @Override
            public void run() {
                Bitmap bitmap = downLoadImage(imageUrl); // 下载图片
                if (bitmap != null) {
                    ImageLoader.ImageResponse imageResponse = new ImageLoader.ImageResponse(); // 图片下载好后,将信息封装成Message发送给Handler处理
                    imageResponse.bitmap = bitmap;
                    imageResponse.imageUrl = imageUrl;
                    imageResponse.imageView = imageView;
                    Message message = Message.obtain();
                    message.obj = imageResponse;
                    handler.sendMessage(message);
                }
            }
        });
    }

    // 下载图片
    public Bitmap downLoadImage(final String imageUrl) {
        Bitmap bitmap = null;
        try {
            URL url = new URL(imageUrl);
            final HttpURLConnection connection = (HttpURLConnection) url.openConnection();
            bitmap = BitmapFactory.decodeStream(connection.getInputStream());
            connection.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }

        return bitmap;
    }
}
public class ImageCache {
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    private LruCache<String, Bitmap> mLruCache = new LruCache<>(maxMemory / 4); // 内存缓存

    public void put(String url, Bitmap bitmap) {
        if (!TextUtils.isEmpty(url) && bitmap != null) {
            mLruCache.put(url, bitmap);
        }
    }

    public Bitmap get(String url) {
        return mLruCache.get(url);
    }
}

可以看到,经过单一职责原则的优化,我们的ImageLoader变得简洁了许多,也更加易于维护,比如,我们想修改缓存相关功能,只需要查看ImageCache类即可。我们并没有对具体功能代码做修改,只是将相关性较高的功能单独提取了出来,封装成类。单一职责原则也就是我们经常说的高内聚。

更高的可扩展性----开闭式原则

经过单一职责原则的优化,我们的ImageLoader变得不再臃肿,但是,还远远没有达到要求,我们在需求中说过,ImageLoader需要支持内存缓存和文件缓存,现在我们只支持了内存缓存,可能你会说,在原来的基础上加上文件缓存不就可以了吗?我们先来看看,直接在ImageLoader中加入文件缓存后的代码会是什么样的:

    MemoryImageCache mMemoryImageCache = new MemoryImageCache();
    DiskImageCache mDiskImageCache = new DiskImageCache(App.getAppContext());
    private int mCacheType = 1;
    public final static int MEMORY_CACHE_TYPE = 1;
    public final static int DISK_CACHE_TYPE = 2;

    /**
     * 设置缓存类型
     * @param type
     */
    public void setCacheType(int type) {
        mCacheType = type;
    }

首先,我们添加了一个变量mCacheType来保存缓存类型,并提供了一个setCacheType方法来设置缓存类型,然后在使用缓存时,我们需要根据保存的缓存类型来确定到底该使用哪个缓存:

    switch (mCacheType) {
	        case MEMORY_CACHE_TYPE:
	            mMemoryImageCache.put(imageResponse.imageUrl, imageResponse.bitmap); // 将图片保存到内存缓存中
	            break;
	        case DISK_CACHE_TYPE:
	            mDiskImageCache.put(imageResponse.imageUrl, imageResponse.bitmap); // 将图片保存在磁盘缓存中
	            break;
	    }

    Bitmap cacheBitmap = null;
    switch (mCacheType) {
        case MEMORY_CACHE_TYPE:
            cacheBitmap = mMemoryImageCache.get(imageUrl); // 从内存缓存中获取图片
            break;
        case DISK_CACHE_TYPE:
            cacheBitmap = mDiskImageCache.get(imageUrl); // 从磁盘缓存中获取图片
            break;
    }

可以看到,无论是获取缓存还是保存缓存,都需要进行判断,不仅麻烦还容易出错,试想一下,如果这时我们需要ImageLoader支持双缓存,即优先使用内存缓存,当内存缓存中没有时,我们需要使用文件缓存(在实际应用中,图片加载框架一般都是双缓存的,这样既能保证加载速度,又能尽可能的减少图片的下载),那么应该怎么办呢?难道还是和添加文件缓存一样,在ImageLoader中再加入一种缓存内型?显示,这种方式是不合理的,不仅让代码变得难以维护,而且扩展性极差,这个时候,开闭式原则就派上用场了。

开闭式原则的定义:软件中的对象应该对扩展是开放的,但是,对修改是关闭的。软件开发过程中,需求是不断变化的,因为变化、升级和维护等原因需要对原有的软件代码进行修改,而一旦对原有的代码进行修改,就有可能影响到原有的模块,引起bug,因此,在软件开发过程中,我们应该尽可能通过扩展的方式实现变化,而不是修改原有的代码,当然,这是一种理想的情况,在实际的软件开发中,完全通过扩展的方式实现变化是不现实的。那么,如何让对象对扩展是开放的呢?回顾一下面向对象的三大特性:封装、继承和多态,在单一职责原则中,我们使用到了封装的特性,而继承和多态还没有使用,现在是派上用场的时候了,继承和多态的精髓在于抽象,一般来说,高层次模块不应该直接依赖低层次模块的实现细节,而应该依赖其抽象,在ImageLoader这个例子中,ImageLoader直接依赖了具体的缓存类,这就让ImageLoader和具体的缓存类紧紧的耦合在一起,我们一直强调软件开发要做到高内聚、低耦合,高内聚通过单一职责原则可以达到,而低耦合则需要依赖抽象。

通过分析,我们不难发现无论是哪种缓存类,其都需要提供获取缓存和放入缓存的功能,即get和put,既然这样,我们为什么不将其抽象成接口呢?

/**
 * 缓存图片接口类
 * 这个接口定义了缓存图片所需要的基本功能,缓存图片以及从缓存中获取图片
 */
public interface ImageCache {
    public void put(String url, Bitmap bitmap); // 缓存图片
    public Bitmap get(String url); // 获取图片
}

然后具体的缓存类实现ImageCache接口:

/**
 * ImageLoader内存缓存类,采用Lru算法
 */
public class MemoryImageCache implements ImageCache {
    final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    private LruCache<String, Bitmap> mLruCache = new LruCache<>(maxMemory / 4);

    @Override
    public void put(String url, Bitmap bitmap) {
        if (!TextUtils.isEmpty(url) && bitmap != null) {
            mLruCache.put(url, bitmap);
        }
    }

    @Override
    public Bitmap get(String url) {
        return mLruCache.get(url);
    }
}
/**
 * ImageLoader文件缓存
 */
public class DiskImageCache implements ImageCache {
    private Context mContext;

    public DiskImageCache() {
        this.mContext = App.getContext();
    }

    @Override
    public void put(String url, Bitmap bitmap) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(DiskCacheFileUtils.getImageCacheFile(url, this.mContext));
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
            fileOutputStream.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            CloseUtils.close(fileOutputStream);
        }
    }

    @Override
    public Bitmap get(String url) {
        String imageCachePath = DiskCacheFileUtils.getImageCachePath(url, this.mContext);
        if (DiskCacheFileUtils.isFileExists(imageCachePath)) {
            return BitmapFactory.decodeFile(imageCachePath);
        }
        return null;
    }
}
/**
 * ImageLoader双缓存类
 */
public class DoubleImageCache implements ImageCache {
    private ImageCache mMemoryImageCache;
    private ImageCache mDiskImageCache;

    public DoubleImageCache() {
        mMemoryImageCache = new MemoryImageCache();
        mDiskImageCache = new DiskImageCache();
    }

    @Override
    public void put(String url, Bitmap bitmap) {
        mMemoryImageCache.put(url, bitmap);
        mDiskImageCache.put(url, bitmap);
    }

    @Override
    public Bitmap get(String url) {
        Bitmap bitmap = mMemoryImageCache.get(url);
        if (bitmap != null) {
            return bitmap;
        }
        bitmap = mDiskImageCache.get(url);
        if (bitmap != null) {
            return bitmap;
        }
        return null;
    }
}

上面的三个具体的缓存类,都实现了ImageCache接口,表示其具备了缓存能力,我们再来看一下在ImageLoader中如何设置缓存:

    private ImageCache mImageCache;

    /**
     * 设置缓存
     * @param imageCache(默认使用MemoryImageCache缓存)
     * @return
     */
    public ImageLoader setImageCache(ImageCache imageCache) {
        this.mImageCache = imageCache;
        return this;
    }

使用缓存:

        if (mImageCache != null) { // 将图片放入缓存
    	    mImageCache.put(url, bitmap);
        }

        Bitmap cacheBitmap = mImageCache.get(url); // 获取缓存

可以看到,ImageLoader并没有依赖于具体的缓存实现类,而只是依赖了缓存类接口ImageCahe,通过这种方式,我们让ImageLoader具备了可以兼容任何实现了ImageCache接口的缓存类的能力,比如,现在ImageLoader想使用双缓存,只需要调用如下代码即可:

        imageLoader.setImageCache(new DoubleImageCache());

再比如,我们想使用其他的缓存方案,只需要定义一个缓存类,实现ImageCache接口即可:

imageLoader.setImageCache(new ImageCache() {
            @Override
            public void put(String url, Bitmap bitmap) {
                // 具体的放入缓存实现
            }

            @Override
            public Bitmap get(String url) {
                // 具体的获取缓存实现
                return null;
            }
        });

通过遵循开闭式原则,我们大大提高了缓存功能的可扩展性,并且去除了ImageLoader与具体缓存类的耦合,这一切都要归功于抽象。

里氏替换原则

通过前面的优化,我们的ImageLoader已经大体上满足需求了,我们再来看一下里氏替换原则的定义:所有引用基类的地方都必须能够透明的使用其子类,这句话是什么意思呢,以我们的ImageLoader为例,所有引用到ImageCache的地方应该都可以替换成具体的子类对象。想象一下,如果我们的ImageCache中的ImageCache不能够被具体的缓存类所替换,那我们应该如何给ImageCahce设置缓存呢?难道还是使用原来的mCacheType的方式吗?显然不是,里氏替换原则就是给这类问题提供了指导原则,通过建立抽象,让高层次模块依赖于抽象类,在运行时在替换成具体的实现类,保证了系统的扩展性和灵活性

依赖倒置原则

依赖倒置原则指的是高层次模块不应该依赖于低层次模块的具体实现,两者都应该依赖其抽象,具体如下:

1、高层次模块不应该依赖低层次模块的具体实现,两者都应该依赖其抽象;

2、抽象不应该依赖细节;

3、细节应该依赖抽象。

在面向对象语言中,抽象指的是接口或者抽象类,两者都不能被实例化,而细节指的是具体的实现类。其实一句话就可以概括,面向接口编程,或者说面向抽象编程,试想一下,如果类和类之间之间依赖细节,那么这两个类将会紧紧的耦合在一起,这就意味着,修改了其中一个类,很可能需要对另外一个类也需要进行修改,并且这样也大大限制了系统的可扩展性。依赖倒置原则提供了一种解耦方式,通过抽象让类和类之间不再依赖细节,而是在具体运行时再进行替换。以我们的ImageLoader为例,通过建立抽象类ImageCache,我们让ImageLoader不再依赖于具体的缓存类,并且能够灵活的扩展使用其他的缓存功能。从上面几个原则来看,想要让系统具备更好的可扩展性,抽象似乎成为了唯一的手段。

接口隔离原则

接口隔离原则的定义:类间的依赖关系应该建立在最小的接口之上。接口隔离原则将非常庞大、臃肿的接口拆分成更小更具体的接口。 一个接口定义的过于臃肿,则代表它的每一个实现类都要考虑所有的实现逻辑。如果一个类实现了某个接口,也就是说这个类承载了这个接口所有的功能,维护这些功能成为了自己的职责。这就无形中增加了一个类的负担。这里有一点需要说明,接口定义要小,但是要有限度,对接口细化可以增加灵活性,但是过度细化则会使设计复杂化。同时接口的使用率不高,提高了代码的维护成本。这种极端的体现就是每个接口只含有一个方法,这显然是不合适的。以前面的ImageCache为例,图片的缓存类必须提供两个功能,放入缓存和获取缓存,所以ImageCache封装了这两个抽象方法,而不应该再进行细分。我们再举一个接口隔离的例子,我们在文件缓存类中用到了FileOutputStream,在Java中,在使用了可关闭对象后,需要调用其close方法对其进行关闭:

    public void put(String url, Bitmap bitmap) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(DiskCacheFileUtils.getImageCacheFile(url, this.mContext));
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fileOutputStream);
            fileOutputStream.flush();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
             if (fileOutputStream != null) { // 关闭fileOutputStream对象
                try {
                    fileOutputStream .close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
             }
        }
    }

我们可以看到这段代码的可读性非常差,各种try...catch,并且,在Java中,类似FileOutputStream这种需要关闭的对象有很多,难道我们每次使用时都要加上这种判断吗?有没有什么办法可以优化呢?答案是肯定的,在Java中,这种需要关闭的对象都继承了Closeable接口,表示这个对象是可以关闭的,Closeable接口的定义如下:

public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}

我们可以建立一个工具类来专门处理继承了Closeable接口的类对象的关闭处理:

public class CloseUtils {
    public static void close(Closeable closeable) {
        if (closeable != null) {
            try {
                closeable.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

这样,我们就可以将关闭FileOutputStream的代码修改为:

    CloseUtils.close(fileOutputStream);

并且这个工具类适用于关闭所用继承了Closeable接口的对象。

通过前面的学习,我们发现这几大原则可以总结为下面几个关键点:单一职责、抽象、最小化。在实际开发中,我们要灵活的运行这几大原则。

迪米特原则

迪米特原则的定义:一个对象应该对其他对象有最小的了解。迪米特原则也称作最小知道原则,即类和类直接应该建立在最小的依赖之上,一个类应该对其依赖的类有最小的了解,即只需要知道其所需要的方法即可,至于其内部具体是如何实现的则不用关心。迪米特原则的目的是减少类和类之间的耦合性,类和类之间的耦合性越大,当一个类发生修改后,会其他类造成的影响也越大。以前面的ImageLoader为例,使用者在用ImageLoader加载图片时,应该只需要和ImageLoader打交道,而不用关心具体的图片时如何下载、缓存以及显示的,并且我们在封装ImageLoader类时,还应该只将一些必要的方法设置为public方法,这些public方法表示提供给使用者使用的方法,比如加载图片和设置缓存,而对于其他的方法应该设置为private,将其隐藏起来。迪米特原则总结来说就是通过最小了解来降低类之间的耦合。

总结

在这篇文章中,我们通过设计一个简易的图片加载框架来说明面向对象六大原则的应用,六大原则指的是单一职责原则、开闭式原则、迪米特原则、里氏替换原则、依赖倒置原则以及接口隔离原则其中,单一职责原则是指一个类应该是一组相关性很高的函数和数据的封装,这是为了提高程序的内聚性,而其他五个原则是通过抽象来实现的,目的是为了降低程序的耦合性以及提高可扩展性。六大原则的目的是为了让程序达到高内聚、低耦合,提高可扩展性的目的,其实现手段是面向对象的三大特性:封装、继承以及多态。

  • 21
    点赞
  • 60
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值