设计图案协同作用,探索装饰图案

In this article I am going to talk about the Decorator Design Pattern.

在本文中,我将讨论装饰器 设计模式

More specifically, I will present some combinations of the Decorator pattern with other Design Patterns in order to achieve better readability and maintainability of our code. I am going to use Java in the code segments of this article but all the examples should be applicable to any object oriented language.

更具体地说,我将介绍Decorator模式与其他Design模式的一些组合,以实现更好的代码可读性和可维护性。 我将在本文的代码段中使用Java ,但是所有示例都应适用于任何面向对象的语言。

Before examining the Design Pattern synergies lets make sure that we have a common understanding of the Decorator pattern.

在检查设计模式协同作用之前,请确保我们对装饰器模式有共同的理解。

装饰图案定义 (Decorator Pattern Definition)

In object-oriented programming, the decorator pattern is a design pattern that allows behavior to be added to an individual object, dynamically, without affecting the behavior of other objects from the same class. -Wikipedia

在面向对象的编程中,装饰器模式是一种设计模式,它允许将行为动态地添加到单个对象中,而不会影响同一类中其他对象的行为。 -维基百科

Another definition provided in the book Head First Design Patterns is the following:

Head First设计模式 》一书中提供的另一个定义是:

The Decorator Pattern attaches additional responsibilities to an object dynamically. Decorators provide a flexible alternative to subclassing for extending functionality. -Head First Design Patterns

装饰器模式动态地将附加职责附加到对象。 装饰器为子类提供了灵活的替代方案,以扩展功能。 头先设计模式

了解装饰器模式 (Understanding the Decorator Pattern)

The Decorator pattern has some very powerful features.

装饰器模式具有一些非常强大的功能。

The Decorator Pattern attaches additional responsibilities to an object dynamically.

装饰器模式动态地将附加职责附加到对象。

This means that one may decorate an object at runtime and add additional functionality to that object.

这意味着可以在运行时 装饰一个对象,并为该对象添加其他功能。

Imagine this example. We have a program that writes a file to the file system.

想象一下这个例子。 我们有一个程序可以将文件写入文件系统。

This could be expressed in code like that:

这可以用以下代码表示:

At some point we decide that the file should be compressed! Then by following the Decorator pattern we may add this additional functionality without changing any of the involved classes!

在某些时候,我们决定压缩文件! 然后通过遵循Decorator模式,我们可以添加此附加功能, 而无需更改任何涉及的类

Image for post

Have in mind that the try-with-resources may fail to close the resources in case an error is thrown during the construction of one of the wrappers.

请记住, 如果在其中一个包装器的构造过程中引发错误 ,则 try-with-resources 可能 无法关闭资源

To make the change at runtime part more obvious, let’s say that some of our users want to select if the file will be compressed or not.

为了使运行时更改更加明显,我们假设某些用户希望选择是否压缩文件。

Image for post

See, in the above example the behavior may be decided at runtime.

参见,在以上示例中,行为可以在运行时确定

The key point in this case is that the decorators all implement the OutputStream interface.

在这种情况下,关键点是装饰器都实现了OutputStream接口。

Every Decorator adds an extra bit of functionality around the methods defined by the interface. For example the GZIPOutputStream will wrap the os.write("Hello, World!".getBytes()) and compress the data. Then the write() method of FileOutputStream will be called which will write the compressed data to the specified file.

每个Decorator都围绕该接口定义的方法添加了额外的功能。 例如, GZIPOutputStream将包装os.write("Hello, World!".getBytes())并压缩数据。 然后将调用FileOutputStreamwrite()方法,该方法会将压缩数据写入指定的文件。

I guess this is the most common and probably most powerful form of the Decorator pattern. You may have an OutputStream object which has been wrapped around dozen of times, each time with a decorator which adds a tiny bit more functionality.

我猜这是Decorator模式的最常见且可能是最强大的形式。 您可能已经缠的时候打一个OutputStream对象,与增加了更多一点点功能的装饰各一次。

Except the case described above there are times where we may need to add additional functionality to an object. Functionality which may not follow a common interface.

除上述情况外,有时我们可能需要向对象添加其他功能。 可能不遵循通用接口的功能

We still can do that. Consider the following example where we want additionally to produce a hash of the created file and print it to the user.

我们仍然可以做到。 考虑以下示例,在该示例中,我们还希望生成所创建文件的哈希并将其打印给用户。

Image for post

See, we have added some extra functionality for the calculation of the checksum. This has the implication that now we rely on the existence of the cos variable in our code which has the explicit type of CheckedOutputStream.

瞧,我们添加了一些额外的功能来计算校验和 。 这意味着现在我们依靠代码中显式类型为CheckedOutputStreamcos变量的存在。

This is not so bad actually. On the other hand, it would complicate things if we wanted to write the try-with-resources a bit differently, by not using separate variables for each decorator.

实际上,这还不错。 另一方面,如果我们想以不同的方式编写try-with-resources ,这将使事情复杂化,因为不为每个装饰器使用单独的变量。

Image for post

Of course, in that specific case there is a good way to get the checksum by keeping a reference of the Adler32 object.

当然,在这种特定情况下,有一种很好的方法,可以通过保留对Adler32对象的引用来获取校验和

Image for post

This is not always the case as we may need to access fields of a specific wrapper. In case the specific decorator we need to access is the last one on the stack then it is easy to access it. Otherwise keeping a reference of the specific decorator object is the only option.

并非总是如此,因为我们可能需要访问特定包装器的字段。 如果我们需要访问的特定装饰器是堆栈中的最后一个装饰器 ,则很容易访问它。 否则,保留对特定装饰器对象的引用是唯一的选择。

OutputStream装饰器 (OutputStream Decorators)

Most of the Decorators we have seen in the previous examples are part of the java.io package. The basic interface the Decorators use is the OutputStream.

我们在前面的示例中看到的大多数装饰器都是java.io包的一部分。 装饰器使用的基本接口是OutputStream

There are numerous implementations of this Decorator. One may find them by examining the Javadoc of the OutputStream interface.

Decorator的实现有很多。 通过检查OutputStream接口的Javadoc可以找到它们。

The Direct Known Subclasses of OutputSteam are:

OutputSteam直接已知子类是:

In addition to that the Direct Known Subclasses of FilterOutputStream are:

除了FilterOutputStream直接已知子类还有:

The reason FilterOutputStream has so many subclasses is that this class is the superclass of all classes that filter output streams. These streams sit on top of an already existing output stream (the underlying output stream) which it uses as its basic sink of data, but possibly transforming the data along the way or providing additional functionality.

FilterOutputStream具有这么多子类的原因是, 此类是过滤输出流的所有类的超类。 这些流位于已存在的输出流(基础输出流)之上,该流用作基本数据接收器,但可能会沿途转换数据或提供其他功能。

Wait! There is more! The DeflaterOutputStream has the following Direct Known Subclasses:

等待! 还有更多DeflaterOutputStream具有以下直接已知子类

Then ZipOutputStream has as Direct Known Subclasses the JarOutputStream.

然后ZipOutputStream具有JarOutputStream作为直接已知的子类

PrintStream has as Direct Known Subclasses the LogStream, which however is now deprecated.

PrintStream的具有作为直接已知子类LogStream已 ,然而现在已经过时了

If we count all that we have in total 15 concrete OutputStream implementations.

如果算上全部 ,我们总共有15个具体的OutputStream实现。

For sure one has to be pretty familiar with the java.io API in order to use it in its full potential.

当然,必须充分熟悉java.io API才能充分发挥其潜力。

The Decorator pattern is also used in other parts of the API, for example the InputStream.

API的其他部分(例如InputStream )中也使用Decorator模式。

装饰图案的缺点 (Decorator Pattern Drawbacks)

By now the problems of the Decorator pattern should be apparent.

现在, 装饰器模式的问题应该已经很明显了。

  • Too many implementations of a Decorator interface may exist. It may be difficult for one to find all the implementations or know how to use them. Note that the OutputStream classes are not even all included in a package exclusive to this type of decorator!

    装饰器接口的实现过多。 一个人可能很难找到所有实现或不知道如何使用它们。 注意,此类装饰器独有的包中甚至没有全部包含OutputStream类!

  • It’s difficult to access methods that are not defined by the base interface, especially when the specific decorator which adds the extra functionality is wrapped by many other instances of decorators.

    很难访问未由基本接口定义的方法,尤其是当添加了额外功能的特定装饰器被许多其他装饰器实例包装时。

Below we are going to try to address these two issues by adding more design patterns in the mix!

下面,我们将通过在组合中添加更多设计模式来尝试解决这两个问题!

比较代码 (Code for comparison)

Before going on let’s set some common code that we are going to try to improve later on:

在继续之前,让我们设置一些通用代码,稍后我们将尝试对其进行改进:

Image for post

In this example we are keeping an OutputStream object for making all the basic actions on it and we keep references to the CheckedOutputStream and PrintStream decorators so we may call some methods provided exclusively by these 2 types.

在此示例中,我们保留一个OutputStream对象以对其执行所有基本操作,并保留对CheckedOutputStreamPrintStream装饰器的引用,因此我们可以调用这两种类型专门提供的一些方法。

Let’s omit the try-with-resources for now for the sake of clarity.

为了清楚起见,让我们现在省略try-with-resources

使用工厂方法启动装饰器 (Initiating Decorators By Using a Factory Method)

The first design pattern which comes in mind when initializing objects that share a common interface is probably the Factory method pattern.

初始化共享公共接口的对象时想到的第一个设计模式可能是Factory方法模式。

Normally a factory method accepts one or a few more arguments. A factory method for our case could seem like that:

通常, 工厂方法接受一个或几个其他参数。 我们的案例的工厂方法看起来像这样:

Image for post

In code this could be used like that:

在代码中可以这样使用:

Image for post

So, what have we achieved?

那么,我们取得了什么成就?

PROS

优点

  • Less code to initialize our decorated object.

    更少的代码来初始化我们的装饰对象。
  • All the possible decorators we want to support may be initialized by the factory method. It is easy to inspect a single method, its arguments and documentation and know what kind of decorators are available.

    我们要支持的所有可能的装饰器都可以通过工厂方法进行初始化。 检查单个方法,其参数和文档并了解可用的装饰器很容易。
  • The use of the concrete decorator classes is encapsulated inside the factory method and this permits future changes with little refactoring effort.

    具体的装饰器类的使用封装在工厂方法内 ,这使得将来的更改无需进行任何重构即可。

CONS

缺点

  • There is no way to get access to the internal decorators.

    无法访问内部装饰器。
  • The factory method may get too crowded with many arguments.

    工厂方法可能太拥挤了许多参数。

  • Exception handling and object initialization may be a bit more complex.

    异常处理和对象初始化可能要复杂一些。

In general I don’t find personally any great benefit to the above solution.

总的来说,我个人并不觉得上述解决方案有什么好处。

Maybe a bit different implementation of multiple factory methods, one for each case could be more beneficial.

多种工厂方法的实现可能有所不同,每种情况可能更有利。

Consider the following class:

考虑以下课程

Image for post

Hmm, this way we have actually wrapped the initialization of each concrete OutputStream to a separate method. This would be used in action like that:

嗯,这样,我们实际上已经将每个具体OutputStream的初始化包装到了单独的方法中。 这将用于如下操作:

Image for post

If you consider this for a moment this is exactly like calling the concrete constructors but with some additional benefits!

如果暂时考虑一下,这就像调用具体的构造函数一样,但是有一些额外的好处!

Furthermore we could return the interface type but I didn’t implemented it like that in this example to avoid extra type casting.

此外,我们可以返回接口类型,但是在本示例中,我没有像这样实现它,以避免额外的类型转换

Extra PROS

额外的优点

  • Easily find all the available Decorators by browsing the (preferably documented) methods of the OutputStreamFactory class. All modern IDE s should support auto-completion.

    通过浏览OutputStreamFactory类的(最好记录在案)方法,可以轻松找到所有可用的Decorator 。 所有现代IDE都应支持自动补全。

  • Encapsulated classes can be altered with minimal refactoring (especially if we would return OutputStream objects instead of the concrete classes).

    可以通过最少的重构来更改封装的类(尤其是如果我们将返回OutputStream对象而不是具体的类)。

In general I see benefit from having a class like this only in the case there are too many decorators around. The programmer would be free to spend less time hunting external documentation and more time relying to the every day tools of the trade in order to find easily the required information.

总的来说, 只有周围有太多的装饰器,才能从这样的类中受益。 程序员将可以自由地花费更少的时间来寻找外部文档,而可以将更多的时间用于日常交易工具上,从而轻松地找到所需的信息。

CONS

缺点

  • Every other drawback we had before still remains.

    我们以前遇到的所有其他缺点仍然存在。
  • The API is too verbose. Statically importing the factory class makes it less verbose but maybe a bit less readable because all the factory methods could be mistaken for local ones.

    API 冗长。 静态导入factory类会使它的详细程度降低,但可读性可能会降低,因为所有factory方法都可能会误认为是本地方法。

使用构建器模式进行装饰 (Decorating by using the Builder Pattern)

Another popular pattern for object creation is the Builder pattern.

另一种流行的对象创建模式Builder模式

We are going to use Josh Bloch’s variation of the pattern as described in Effective Java.

我们将使用有效Java中描述的模式的Josh Bloch的变体。

This should look like this:

看起来应该像这样:

Image for post

This could be used like that:

可以这样使用:

Image for post

Let’s rate this pattern!

让我们为这个模式评分!

PROS

优点

  • The decorators can be easily found under the builder class.

    装饰器可以在builder类下轻松找到。

  • The syntax is short enough.

    语法足够简短。
  • We can use the created builder to create many similar objects.

    我们可以使用创建的构建器来创建许多相似的对象。

CONS

缺点

  • We cannot access intermediate decorators. For example in the code above we cast the OutputStream to PrintStream which is the last one used. We cannot access for example the CheckedOutputStream directly.

    我们无法访问中间装饰器。 例如,在上面的代码中,我们将OutputStream强制转换PrintStream ,这是最后使用的那个。 例如,我们无法直接访问CheckedOutputStream

  • The order of the decoration is fixed inside the build() method. We cannot decorate multiple times with the same decorator or alter the order easily without making the implementation too complex. However, this structure is sufficient for most of the common cases.

    装饰的顺序在build()方法中固定。 我们不能使用相同的装饰器多次装饰或轻松更改顺序,而不会使实现过于复杂。 但是,此结构对于大多数常见情况而言已足够。

A couple of notes.

几个注意事项。

  1. We could make the builder class implement Autocloseable in order to free correctly the resources if used in try-with-resources.

    我们可以使builder类实现Autocloseable ,以便在try-with-resources中使用时正确释放 资源

  2. We can omit calling the builder methods if we want to pass false to them because this is the default value. Additionally we could achieve a more fluentAPI. if we didn’t accept arguments at all. For example calling a more fluentAPI for the previous example would be like that:

    如果要向其传递false ,则可以省略对构建器方法的调用,因为这是默认值。 另外,我们可以实现更流畅的 API 。 如果我们根本不接受论点。 例如,为前面的示例调用更流畅的API就像这样:

Image for post

Of course having arguments makes a bit easier to use the API in some cases. Consider the example where the compressed functionality was added at runtime based on some condition.

当然,在某些情况下,具有参数会使使用API更加容易。 考虑在某些情况下在运行时添加压缩功能的示例。

I think I personally like this way most of all so far. It has some rough edges but it provides a beautiful and readable interface.

我认为到目前为止,我个人最喜欢这种方式。 它具有一些粗糙的边缘,但提供了美观且可读的界面。

怎么办? 更多建造者模式 (Now what? More Builder Pattern)

The builder pattern as described by Bloch has some important benefits.

Bloch描述的构建器模式具有一些重要的好处。

  1. In contrast to Javabean initialization of objects by using the setters methods this way we can be sure that the created object will be fully initialized and has the expected state before we use it.

    与通过使用setters方法的Javabean对象初始化相反,我们可以确保创建的对象将在使用之前完全初始化并具有预期的状态。

  2. A factory may be re-used and create other instances of similar objects.

    可以重新使用工厂并创建相似对象的其他实例。

In the case of the decorators’ builder we may disregard these benefits in favor of other. The builder could be written as such:

对于装饰工,我们可能会忽略这些好处而转为其他。 构建器可以这样写:

Image for post

Now, we have lost the 2nd previously mentioned benefit. However, we gained something else. We can access this way the intermediate decorators:

现在,我们已经失去了前面提到的第二个好处。 但是,我们获得了其他东西。 我们可以通过这种方式访问​​中间装饰器:

Image for post

If wanted we could wrap a decorator multiple times that way. Also, the order the decorators are applied isn’t any more fixed.

如果需要的话,我们可以多次包装装饰器。 同样,装饰器的应用顺序不再固定。

As we can see this variation of building objects may often be more suitable for building decorators in comparison to the original.

如我们所见,与原始建筑相比, 建筑对象的这种变化通常更适合建筑装饰

I think with this we have more or less exhausted the topic of initializing instances of decorators. At the process we got familiar with each approach and with the benefits that offers and the problems that introduces.

我认为与此相关的是,我们或多或少用尽了初始化装饰器实例的主题。 在此过程中,我们熟悉了每种方法以及所提供的好处和引入的问题。

Let’s see if we can address the second issue. When we decorate an object with a decorator which adds additional functionality not defined in the decorator interface we have to keep references to the concrete class instead.

让我们看看是否可以解决第二个问题。 当我们装饰的对象与增加的装饰没有定义的接口我们必须继续引用具体的类,而不是附加功能的装饰。

认识适配器 (Meet the Adapter)

In software engineering, the adapter pattern is a software design pattern (also known as wrapper, an alternative naming shared with the decorator pattern) that allows the interface of an existing class to be used as another interface.[1] It is often used to make existing classes work with others without modifying their source code. -Wikipedia

在软件工程中,适配器模式是一种软件设计模式(也称为包装器,与装饰器模式共享的另一种命名方式),该模式允许将现有类的接口用作另一个接口。[1] 它通常用于使现有类与其他类一起使用而无需修改其源代码。 -维基百科

In order to solve the problem of having access to the intermediate concrete classes we can instead create an adapter that exposes all the intermediate interfaces.

为了解决访问中间具体类的问题,我们可以改而创建一个公开所有中间接口的适配器

The adapter in our example could look like this:

我们的示例中的适配器可能如下所示:

public class OutputStreamAdapter extends OutputStream implements Appendable, Closeable {


    private OutputStream os;
    private CheckedOutputStream cos;
    private DeflaterOutputStream dos;
    private Appendable appendable;
    private PrintStream pos;


    public OutputStreamAdapter(OutputStream os) {
        setOutputStream(os);
    }


    public void addOutputStream(OutputStream o) {
        setOutputStream(o);
    }


    private void setOutputStream(OutputStream o) {
        os = o;
        if (o instanceof CheckedOutputStream) {
            cos = (CheckedOutputStream) o;
        }
        if (o instanceof DeflaterOutputStream) {
            dos = (DeflaterOutputStream) o;
        }
        if (o instanceof Appendable) {
            appendable = (Appendable) o;
        }
        if (o instanceof PrintStream) {
            pos = (PrintStream) o;
        }
    }


    @Override
    public void write(int i) throws IOException {
        os.write(i);
    }


    public Checksum getChecksum() {
        if (cos != null) {
            return cos.getChecksum();
        } else if (os instanceof OutputStreamAdapter) {
            return ((OutputStreamAdapter) os).getChecksum();
        }
        throw new UnsupportedOperationException("Decorator method not implemented.");
    }


    public void finish() throws IOException {
        if (dos != null) {
            dos.finish();
        } else if (os instanceof OutputStreamAdapter) {
            ((OutputStreamAdapter) os).finish();
        }
        throw new UnsupportedOperationException("Decorator method not implemented.");
    }


    @Override
    public void flush() throws IOException {
        os.flush();
    }


    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        os.write(b, off, len);
    }


    @Override
    public void write(byte[] b) throws IOException {
        System.out.println("WRITE!");
        os.write(b);
    }


    @Override
    public void close() throws IOException {
        os.close();
    }


    @Override
    public Appendable append(CharSequence cs) throws IOException {
        if (appendable != null) {
            appendable.append(cs);
        }
        throw new UnsupportedOperationException("Decorator method not implemented.");
    }


    @Override
    public Appendable append(CharSequence cs, int i, int i1) throws IOException {
        if (appendable != null) {
            appendable.append(cs, i, i1);
        }
        throw new UnsupportedOperationException("Decorator method not implemented.");
    }


    @Override
    public Appendable append(char c) throws IOException {
        if (appendable != null) {
            appendable.append(c);
        }
        throw new UnsupportedOperationException("Decorator method not implemented.");
    }


    public void println(String x) {
        if (pos != null) {
            pos.println(x);
        } else {
            throw new UnsupportedOperationException("Decorator method not implemented.");
        }
    }


}

I have implemented only some of the methods in this example but the point is to implement all of the decorators that add additional functionality.

在此示例中,我仅实现了一些方法,但是重点是实现所有添加了附加功能的装饰器

With an adapter like this we may access each one directly even if it is not implemented by all the decorators. An exception will be thrown if the implementation does not exist.

使用这样的适配器,即使没有由所有装饰器实现,我们也可以直接访问每个适配器。 如果实现不存在的会抛出异常

This could be used like that:

可以这样使用:

Image for post

This uses the Bloch-based Builder class from before. However, some modification is required. The build() method should look like that:

这从以前开始使用基于Bloch的 Builder类。 但是,需要进行一些修改。 build()方法应如下所示:

Image for post

PROS

优点

  • We can access all the intermediate decorator functionality without leaking references to the intermediate concrete classes.

    我们可以访问所有中间装饰器功能,而不会泄漏对中间具体类的引用。
  • We have control if the implementation is not there to throw an exception and delegate the control to the client, fail silently or graciously somehow.

    如果实现不存在,我们将控制是否抛出异常,并将控制权委派给客户端,以某种方式无声或无故地失败。

CONS

缺点

  • Many more lines of code to implement the adapter.

    还有更多的代码行可实现适配器。
  • Increased maintenance cost. For each new decorator which deviates from the main interface we have to add the new methods to the adapter. For any change we may have to update the adapter accordingly.

    维护成本增加。 对于每个偏离主界面的新装饰器,我们必须向适配器添加新方法。 对于任何更改,我们可能必须相应地更新适配器

  • Builder build() method is kind of more complicated.

    生成器的build()方法更为复杂。

In general however I feel for certain occasions it offers some useful functionality.

但总的来说,我觉得它提供了一些有用的功能。

结论 (Conclusion)

In this article we explored various combinations of the Decorator pattern with the Factory method, Builder and Adapter patterns. Hopefully, we all have a better understanding of what each combination offers.

在本文中,我们探讨了Decorator模式与Factory方法BuilderAdapter模式的各种组合。 希望我们都对每种组合提供的内容有更好的了解。

By all means do not go out and start using every bit of design pattern combination demonstrated here.

绝对不要走动,并开始使用此处演示的所有设计模式组合。

Using the Decorator pattern alone is fine for most of the cases, especially if the total number of decorators is low. Just don’t forget to provide the necessary documentation and maybe group them all together under the same package/directory.

在大多数情况下,仅使用Decorator模式即可,特别是在装饰器总数很少的情况下。 只是不要忘记提供必要的文档,甚至可以将它们归为一个包/目录。

Don’t rush to add things that you are not going to need soon or ever. As you have seen many of the above combinations introduce additional complexity and require more maintenance effort. Keep it simple but have in mind how to solve a problem when it arise.

不要急于添加您很快或永远不需要的东西。 如您所见,以上许多组合带来了额外的复杂性,并需要更多的维护工作。 保持简单,但要记住在问题出现时如何解决。

The only exception to the above rule may be the case where you are building an API that will be distributed for external consumption. Maybe then providing an API what won’t introduce breaking changes in the future is important.

上述规则的唯一例外可能是正在构建将分发给外部使用的API的情况。 也许提供一个API ,将来不会带来重大变化是很重要的。

Finally, some patterns such as the Adapter, Decorator or Proxy share similar implementations. Don’t get confused, sometimes the differences are subtle. The name of the pattern first of all shows the intention. The specific implementation is usually well-defined but comes second.

最后,某些模式(例如AdapterDecoratorProxy)共享相似的实现。 不要感到困惑,有时差异是细微的。 模式的名称首先显示了意图。 具体的实现通常是明确定义的,但排在第二位。

I hope you have found any of the above just a wee bit useful. Until the next time keep coding smart :-)

我希望你已经发现任何上述只是一个凌晨 一点有用的。 在下一次之前,请保持编码聪明 :-)

Originally published at https://masterex.github.io.

最初发布在 https://masterex.github.io

翻译自: https://medium.com/@pntanasis/design-pattern-synergies-exploring-the-decorator-pattern-7c29bdd1ab1b

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值