HeadFirest设计模式学习笔记

设计原则

1、经常会发生变化的部分应该从整体中抽取并封装起来,以便以后可以很轻易的对这部分代码进行改动或者是扩充,而不会影响到不需要变化的其他部分。

2、针对接口编程,而不是针对实现编程。

3、多用组合,少用继承。组合:将适当的行为对象组合起来构成一个整体,善于利用接口编程,使组合建立的系统具有强大的弹性

4、为了交互对象之间的松耦合而努力(当两个对象之间松耦合,他们仍然可以交互,但是不清楚批次的细节)

5、类应该对修改关闭,对扩展开放

6、依赖倒置原则,要依赖抽象,不依赖具体实现

“高层”组件:由其他“低层”组件构成的类 例如:PizzaStore由不同的Pizza对象,准备、烘培、切片、装盒构成

“低层组件”:构成“高层”组件的构件 例如: 各种Pizza类型

“低层”与“高层”应该都依赖于抽象

所谓倒置:低层组件依赖高层的抽象。同样的,高层组件也依赖相同的抽象

工厂方法是依赖倒置的一种应用

7、最少知识原则:只和你的密友谈话

8、好莱坞原则:允许低层组件将自己挂载到高层组件上,但何时被调用以及如何被调用则由高层组件决定(模板方法模式的应用)

9、单一责任:一个类应该只有一个引起变化的原因

策略模式

定义:定义了算法族,分别封装起来,让它们之间可以相互替换,使得算法的变化独立于使用算法的客户

图解:下图有一组代表游戏角色的类和武器行为的类。每个角色一次只能使用一种武器,在游戏的过程中可以变换武器

在这里插入图片描述

WeaponBehavior:定义了使用武器的算法族

Character:使用WeaponBehavior算法族的类,其中的fight方法会调用weapon的useWeapon方法。Character类在运行的过程中可以动态改变useWeapon的行为,而useWeapon行为的变化不会影响到Character的结构

观察者模式

观察者模式就如同报纸的订阅。

1、报社的业务就是出版报纸

2、向某家报社订阅报纸,那么当报社出版新的报纸,就会就给送来。只要你是他们的订阅用户,就会一直受到新报纸

3、当你不想看报纸的时候,取消订阅,他们就不会再给你送新报纸了

4、只有报社还在运营就一直会有人订阅和取消

定义:观察者模式定义了对象之间一对多关系,这样一来,当一个对象的状态发生变化时,他的所有依赖者都会收到通知并自动更新

观察者模式有“推”和“拉”两种方式:

​ 推:主题的状态每次发生改变时,主动把所有信息通知给每一个观察者。

​ 拉:主题的状态每次发送改变时,观察者主动调用主题的getter来获取它们关心的数据。

​ 使用“拉”的好处:主题扩展功能时,不需要修改或更新每位观察者的调用,只需要添加对应数据的getter就可以

图解:下图有一个对象用于获取实时的天气情况,各个布告板对该对象进行监听,以便在天气状况发生变化时能及时的得到通知,更新显示数据

在这里插入图片描述

Subject:主题接口,对象通过此接口注册为观察者

Observer:当主题状态发生改变时调用update方法通知观察者对象

WeatherData:是一个监测天气数据的对象,当获取到最新的天气数据会通知给各个注册了的布告板

XXXDisplay:各个不同的布告板,注册监听WeatherData的消息,当收到新数据时更新显示内容

装饰者模式

装饰者模式是一个“对修改关闭,对扩展开放”的例子

利用继承扩展子类的行为,只能在编译时静态决定(行为不是来自超类,就是子类覆盖后的版本),而使用组合的方式扩展对象的行为可以在运行时动态的扩展对象行为

定义:装饰者模式可以动态的添加责任到对象上,若要扩展功能,装装饰者提供了比继承更加具有弹性的解决方案

例子:

1、一杯深焙咖啡

2、使用一个摩卡对象修饰它(现在,它是一杯摩卡深焙咖啡啦)

3、使用奶泡对象修饰它(现在,它是一杯奶泡摩卡深焙咖啡啦)

使用装饰者模式必须注意的地方:

1、装饰者对象和被装饰者对象必须是相同的类型,因为在任何需要原始对象的场合也可以使用修饰对象替换,就好比一杯深焙咖啡和一杯摩卡深焙咖啡,它们都是一杯咖啡。因此,可以使用修饰者和被修饰者具有相同的超类型来完成这一限定

2、可以用一个或多个修饰者对象包装一个对象

3、装饰者可以在被装饰者的行为之前或之后添加某些操作,来达到特定的目的

4、使用装饰者时程序中不能依赖具体的组件类型,因为当一个对象被装饰后,类型会发生变化

图解:下图有具体的饮料类和各种调味装饰类。由于装饰者与被装饰者必须具有相同的类型,所以它们都继承自共同的超类。

各个调味装饰类通过组合的方式对具体的饮料类进行扩展。例如:

​ new Mocha(new Mike(new DarkRoast())) // DarkRoast 深焙咖啡 被 Mike 牛奶 和 Mocat装饰,这是一杯牛奶摩卡深焙咖啡

在这里插入图片描述

Beverage:装饰类和被装饰类共同的超类

DarRoast、HouseBlend:饮料类的具体

CondimentDecorator:装饰类的抽象

Mike、Mocha、Soy:调味料的具体

简单工厂

简单工厂将创建对象的操作都封装到一个工厂类中,并对各个需要产品实例的客户程序都提供了一个接口获得产品对象。

客户程序通过工厂获取产品,它们无需关心产品的制作过程。日后当产品的制作过程发生变化,也不会对各个使用产品的客户产生影响。

在这里插入图片描述

public class SimpleFactory {    
    public Product createConcreteProduct(String str){        
        Product product = null ;        
        switch (str){            
            case "concrete1":product = new ConcreteProduct() ;break;            
            case "concrete2":product = new ConcreteProduct2();break;        
        }        
        return product ;    
    }
}
public class Product {}
public class ConcreteProduct extends Product {}
public class ConcreteProduct2 extends Product {}
public class Client {    
    private SimpleFactory simpleFactory ;    
    public Client(SimpleFactory simpleFactory){
    	this.simpleFactory = simpleFactory;
    }    
    public void method(){        
        //通过简单工厂来获取产品对象
        Product product = simpleFactory.createConcreteProduct("concrete1") ;  
        
        //do something with the product
    }
}

简单工厂的优点:将产品的“实现”与客户程序的“使用”分离开来,每当需要需要添加新产品或者产品的制作发生变化时,都不会影响到客户程序的使用。

工厂方法模式

工厂方法模式定义了一个创建对象的接口,但由子类决定要实例化哪个类

工厂方法让类把实例化推迟到子类

在这里插入图片描述

Factory类中的doSomething方法通常会包含依赖于抽象产品(Product)的代码,而这些抽象产品的实例由子类创建

Factory类无须了解产品创建的细节,当需要添加产品或者产品发生改变时,Factory类中引用抽象产品(Product)的代码将不会受到影响

public interface Product {}
public class ConcreteProduct1 implements Product {}
public class ConcreteProduct2 implements Product {}
public abstract class Factory {    
    public void doSomething(){        
        Product product = factoryMethod() ;        
        //do something whit the product    
        //以下使用product的代码,在添加产品或改变产品时将不会受到影响
    }    
    public abstract Product factoryMethod();
}
public class ConcreteFactory1 extends Factory {    
    @Override    //子类负责具体实现
    public Product factoryMethod() {        
        return new ConcreteProduct1() ;    
    }
}
public class ConcreteFactory2 extends Factory {    
    @Override    //子类负责具体实现
    public Product factoryMethod() {        
        return new ConcreteProduct2();    
    }
}

抽象工厂

定义:提供一个接口,用于创建相关或依赖对象的家族,而不需要明确指定具体类

抽象工厂允许客户程序使用抽象接口来创建一组相关联的产品,而不依赖于具体的产品对象

在这里插入图片描述

AbstractFactory:抽象工厂,提供了一个接口,用来创建一组相关联的产品

ConcreteFactoryA:具体工厂A,创建具体的产品

ConcreteFactoryB:具体工厂B,创建具体的产品

Client:客户程序,通过抽象工厂提供的接口来创建一组相关的产品,而不需要关心实际产出的产品是什么

public abstract class AbstractFactory {    
    public abstract AbstractProductA createProductA();    
    public abstract AbstractProductB createProductB() ;
}
public interface AbstractProductA { }
public interface AbstractProductB { }
public class Client {    
    private AbstractFactory abstractFactory  ;    
    public Client(AbstractFactory abstractFactory){        
        this.abstractFactory = abstractFactory ;    
    }    
    public void method(){        
        AbstractProductA productA = abstractFactory.createProductA();        
        AbstractProductB productB = abstractFactory.createProductB();        
        //do something whit the productA and the productB        
        //让客户端程序完全依赖抽象,而不是具体的产品    
    }    
    //测试    
    public static void main(String[] args) {  
        //要使用某个具体的子类工厂,必须先实例化它,然后将它传入到一些针对抽象工厂类型而写的代码中
        //使用A工厂创建产品        
        Client client1 = new Client(new ConcreteFactoryA()) ;        
        //使用B工厂创建产品        
        Client client2 = new Client(new ConcreteFactoryB()) ;    }
}
public class ConcreteFactoryA extends AbstractFactory {    
    @Override    
    public AbstractProductA createProductA() {        
        return new ProductA2();    
    }    
    @Override    
    public AbstractProductB createProductB() {        
        return new ProductB2() ;     
    }
}
public class ConcreteFactoryB extends AbstractFactory {    
    @Override    
    public AbstractProductA createProductA() {        
        return new ProductA1();    
    }    
    @Override    
    public AbstractProductB createProductB() {        
        return new ProductB1() ;    
    }
}
public class ProductA1 implements AbstractProductA { }
public class ProductA2 implements AbstractProductA {}
public class ProductB1 implements AbstractProductB {}
public class ProductB2 implements AbstractProductB {}

单例模式

定义:确保一个类只有一个实例,并且提供一个全局访问点

使用一个私有构造函数、一个私有静态变量以及一个公有静态函数来实现。

私有构造函数保证了不能通过构造函数来创建对象实例,只能通过公有静态函数返回唯一的私有静态变量。

在这里插入图片描述

实现一、懒惰式-线程不安全

uniqueInstance被延迟到使用该类的时候才进行实例化,从而优化资源的浪费

以下实现是线程不安全的,当有多条线程执行到if(null == uniqueInstance)时,并且此时uniqueInstance 为null,则会有多条线执行到uniqueInstance = new Singleton(); 从而获取多个不一样的uniqueInstance对象

public class Singleton {    
    private static Singleton uniqueInstance;    
    private Singleton(){ }    
    public static Singleton getInstance(){        
        if(null == uniqueInstance){            
            uniqueInstance = new Singleton();        
        }        
        return instance ;     
    }
}

实现二、急切式-线程安全

JVM在加载这个类时马上创建唯一的单例对象,JVM保证在任何线程访问uniqueSingleton 静态变量前,一定先创建此实例

该实现方式丢失了延迟实例化节省资源的好处

public class Singleton {    
    private static Singleton uniqueSingleton = new Singleton() ;
    public static Singleton getInstance(){    
        return uniqueSingleton ;     
    }
}

实现三、懒惰式-线程安全

直接在getInstance方法添加添加(synchronized)同步锁,解决了实现一的线程安全问题,但是存在严重的性能问题。

只有在第一次实例化uniqueInstance对象,才真正需要用到同步锁,一旦uniqueInstance被实例化后,就不再需要同步这个操作了,以后每次调用getInstance方法,同步锁就是一个累赘(当一个线程进入该方法后,其他线程会被阻塞),影响执行效率。

public class Singleton {    
    private static Singleton uniqueInstance ;    
    private Singleton(){}    
    public static synchronized Singleton getInstance(){        
        if(null == uniqueInstance){            
            uniqueInstance = new Singleton() ;        
        }        
        return uniqueInstance ;     
    }
}

实现四、双重检测加锁

将同步操作放在uniqueInstance实例化时进行,如果uniqueInstance已经被实例化则不再需要进行同步操作

public class Singleton {    
    //volatile保证uniqueInstance的有序性
    private static volatile Singleton uniqueInstance ;    
    private Singleton(){}    
    public static Singleton getIntance(){        
        if(null == uniqueInstance){            
            synchronized (Singleton.class){                
                if(null == uniqueInstance){                    
                    uniqueInstance = new Singleton() ;                 
                }            
            }        
        }        
        return  uniqueInstance ;    
    }
}

考虑下面的实现,也就是只是用一个if语句。在uniqueInstance == null的情况下,如果两个线程都执行了if语句

,并且两个线程都进入if语句块内。虽然在if语句块内进行了加锁操作,但是两个线程都会按顺序依次执行uniqueInstance = new Singleton();,从而创建出两个不同的uniqueInstance对象

使用双重判断:

第一个if避免uniqueInstance被实例化后还进行加锁操作

第二个if用来避免创建多个实例。由于第二个if是在同步代码块内,所以只有一个线程进入,不会出现 null == uniqueInstance 时,两个线程执行实例化操作

if (uniqueInstance == null) {
    synchronized (Singleton.class) {
        uniqueInstance = new Singleton();
    }
}

uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:

  1. 为 uniqueInstance 分配内存空间
  2. 初始化 uniqueInstance
  3. 将 uniqueInstance 指向分配的内存地址

但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1>3>2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保证在多线程环境下也能正常运行。

实现五、静态内部类

当 Singleton 类被加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance() 方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次(SingletonHolder在加载进内存时,在任何线程访问前,初始化静态实例域)。

这种方式不仅具有延迟初始化的好处,而且由 JVM 提供了对线程安全的支持。

public class Singleton {
    private Singleton() {}
    
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}

实现六、枚举实现

public enum Singleton {

    INSTANCE;

    private String objName;


    public String getObjName() {
        return objName;
    }


    public void setObjName(String objName) {
        this.objName = objName;
    }


    public static void main(String[] args) {

        // 单例测试
        Singleton firstSingleton = Singleton.INSTANCE;
        firstSingleton.setObjName("firstName");
        System.out.println(firstSingleton.getObjName());
        Singleton secondSingleton = Singleton.INSTANCE;
        secondSingleton.setObjName("secondName");
        System.out.println(firstSingleton.getObjName());
        System.out.println(secondSingleton.getObjName());

        // 反射获取实例测试
        try {
            Singleton[] enumConstants = Singleton.class.getEnumConstants();
            for (Singleton enumConstant : enumConstants) {
                System.out.println(enumConstant.getObjName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
firstName
secondName
secondName
secondName

该实现可以防止反射攻击。在其它实现中,通过 setAccessible() 方法可以将私有构造函数的访问级别设置为 public,然后调用构造函数从而实例化对象,如果要防止这种攻击,需要在构造函数中添加防止多次实例化的代码。该实现是由 JVM 保证只会实例化一次,因此不会出现上述的反射攻击。

该实现在多次序列化和序列化之后,不会得到多个实例。而其它实现需要使用 transient 修饰所有字段,并且实现序列化和反序列化的方法。

命令模式

定义:命令模式将“请求”封装成对象,以便使用不同的请求队列或日志来参数化其他对象。命令模式也支持可撤销的操作。

Command:定义了所有命令对象的 “执行 ”和“撤销”操作的接口,调用这两个接口就可以让接收者执行相关操作

ConcreteCommand:绑定动作与接收者之间的关系

Receiver:动作的作用对象

Invoker:通过设置相关的命令对象执行相关的操作

Client:创建具体的命令对象并设置指定的接收者

在这里插入图片描述

public interface Command {    
    void execute() ;    
    void undo() ;
}
public class ConcreteCommand implements Command {    
    private Receiver receiver;    
    public ConcreteCommand(Receiver receiver){        
        this.receiver = receiver ;    
    }    
    @Override    
    public void execute() {        
        System.out.println("do something with the receiver");        
        receiver.on();    
    }    
    @Override    public void undo() {        
        System.out.println("undo something with the  receiver");        
        receiver.off();    }
}
public class Receiver {    
    public  void  on(){    }    
    public void off(){    }
}
public class Invoker {    
    private Command command ;    
    public void setCommand(Command command){        
        this.command = command ;    
    }    
    public void invoke(){        
        command.execute();    
    }
}
public class Client {    
    public static void main(String[] args) {        
        Invoker invoker = new Invoker() ;        
        Receiver receiver = new Receiver() ;        
        ConcreteCommand concreteCommand = new ConcreteCommand(receiver) ;        
        invoker.setCommand(concreteCommand);        
        invoker.invoke();    
    }
}

使用命令模式可以使调用者和接收者之间解耦

命令对象(Command)将动作和接收者包进对象中,对外提供execute方法。调用者不需要关心所拥有的是什么命令对象,只要该命令对象实现了Command接口即可

可以使用栈来记录多次命令从而使得撤销操作可以回到更早的状态

适配器

定义:将一个类的接口,转换成客户期望的另一个接口。适配让原本接口不兼容的类可以合作无间

在这里插入图片描述

客户端需要目标接口,使用适配器去包装被适配对象,使它能够满足客户端的要求

外观模式

外观模式的目标是简化接口,它将一个或数个类复杂的一切都隐藏在背后,只显露出一个干净的外观

定义:提供了一个统一的接口,用来访问子系统中的一群接口。外观定义了一个更高层次的接口,让子系统更容易使用

在这里插入图片描述

适配器更目标是“改变”,外观模式的目标是“简化”

模板方法模式

模板方法定义了一个算法的步骤,并且允许子类对一个或多个步骤提供实现

定义:在一个方法中定义了一个算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以在不改变算法的结构的情况下,重新定义算法中的某个步骤

在这里插入图片描述

在这个抽象类中定义了templateMethod模板方法,该方法定义了一个算法的骨架,并且提供了两个抽象方法(这两个抽象方法分别是算法中的某一个步骤)让子类可以对算法的某个步骤进行改变。

public abstract class AbstractClass {    
    final void templateMethod(){        
        primitiveMethod1() ;        
        otherMethod();        
        primitiveMehtod2() ;        
        hook();    
    }    
    protected abstract void primitiveMethod1() ;    
    protected abstract void primitiveMehtod2() ;    
    private void otherMethod(){        
        System.out.println("do something in the AbstractClass");    
    }    
    public void hook(){        
        System.out.println("do something in the AbstractClass");    
    }
}
public class ConcreteClass extends AbstractClass {    
    @Override    
    protected void primitiveMethod1() {        
        System.out.println("do something in the ConcreteClass ");    
    }    
    @Override    
    protected void primitiveMehtod2() {        
        System.out.println("do something in the ConcreteClass ");    
    }    
    @Override    
    public void hook() {        
        System.out.println("do something in the ConcreteClass ");    
    }
}

迭代器模式

定义:提供了一种方法顺序访问一个聚合对象中的各个元素,而不暴露其内部的表示

可以把不同的元素放进数组、堆栈、队列、散列表等容器中,每一种容器都有其优点和使用的时机,假设现在你的客户想要遍历这个集合中的元素,他不应该去了解这些你是如何实现以及用何种方式去保存这些元素,因为这样做了的话,客户代码可能就会根据你的实现细节进行具体编码,当你存储元素的容器发生改变时,客户代码也不得不跟着改变。

针对具体编程

在这里插入图片描述

使用迭代器模式进行解耦

在这里插入图片描述

代理模式

定义:为另一个对象提供一个替身或占位符,以控制对这个对象的访问

代理对象本身并不提供真正的服务,而是通过调用目标对象的相关方法来提供相应的服务。

真正的业务功能还是由目标对象来完成,但是可以在业务功能执行的前后假如一些额外的功能

使用代理模式的目的

中介隔离作用:在某些情况下,不希望客户直接对目标对象进行访问,而代理对象正是客户与目标对象之间的中介

开闭原则,增强功能:可以通过代理类来扩展目标对象的功能,使用这种方式我们只需要修改代理对象而不需要修改目标对象,符合代码设计的开闭原则。可以让代理对象负责对消息的预处理然后在转发给目标对象,以及事后返回结果的处理。

静态代理

静态代理是由程序员或特定工具自动生成源代码,在对其进行编译。在程序运行之前,代理类.class文件就已经被创建了

要求代理对象与目标对象拥有相同的接口,使得代理类可以扮演目标类

客户程序可以把代理类认为是目标类

原理图:

在这里插入图片描述

Proxy和RealSubject都拥有相同的接口,使得Proxy可以扮演RealSubject的身份

RealSubject是真正做事情的对象

Proxy持有RealSubject的引用,以控制对RealSubject的访问

代码:

public interface Subject {    void request() ;}
public class RealSubject implements Subject {    
    @Override    
    public void request() {        
        System.out.println("提供服务");    
    }
}
public class Proxy implements Subject {    
    private Subject subject  ;    
    public Proxy(Subject subject){        
        this.subject = subject ;    
    }    
    @Override    
    public void request() {        
        System.out.println("提供服务前");        
        subject.request();        
        System.out.println("提供服务后");    
    }
}

静态代理的总结

优点:可以做到在符合开闭原则的情况下对目标对象进行功能扩展

缺点:我们得为每一个服务都创建代理类,工作量大,不易管理。

动态代理

在动态代理中我们不需要手动去创建代理对象,我们只需要编写一个动态处理器就可以了。真正的代理对象由JDK在运行时为我们动态的创建

public interface Subject {    void request();}
public class RealSubject implements Subject {    
    public void request(){        
        System.out.println("处理数据");    
    }
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class DynamicProxyHandler implements InvocationHandler {    
    private Object object;    
                                                               
    public DynamicProxyHandler(Object object){        
        this.object = object ;    
     }    
    @Override    
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable 
    {        
        System.out.println("处理前");        
        Object result = method.invoke(object, args);        
        System.out.println("处理后");        
        return result ;    
    }
}
public class Test {    
    public static void main(String[] args) {        
        RealSubject target = new RealSubject() ;        
        /*        * Proxy.newProxyInstance():        
        * ClassLoder loder:指定当前目标对象的类加载器        
        * Class<?>[] interfaces:指定目标对象实现的接口类型        
        * InvocationHandler:指定动态处理器,执行目标对象的方法时,会触
        * 发事件处理器的方法      
        * 
        */        
        Subject proxyTarget = 
            (Subject)Proxy.newProxyInstance(target.getClass().getClassLoader()   
                                            , new Class[]{Subject.class}   
                                            , new DynamicProxyHandler(target)); 
        //当调用代理对象的request方法时,会执行DynamicProxyHandler对象的invoke方法,并且把目标对象的request方法传递过去,这样就可以在invoke方法里面对目标对象的request方法进行增强
        proxyTarget.request();        
        System.out.println("2" + proxyTarget);    
    }
}

动态代理总结

虽然相对于静态代理,动态代理大大减少了我们的开发任务,同时减少了对业务接口的依赖,降低了耦合度。但是还是有一点点小小的遗憾之处,那就是它始终无法摆脱仅支持interface代理的桎梏,因为它的设计注定了这个遗憾。

CGLIB

静态代理与接口代理都需要目标对象实现一个接口,如果对于没有实现任何接口的目标类,我们也希望对其进行代理,这时候就拥戴CGLib代理。

CGLib代理也称为子类代理,它采用非常底层的字节码技术,通过分析目标类的字节码文件创建一个其子类,并在子类中采用方法拦截的技术,拦截所有父类方法的调用,顺势横切逻辑,但是因为CGLib代理采用的是继承,所以被代理类不能被final修饰。

JDK动态代理与CGLIB动态代理均是实现Spring AOP 的基础

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值