23种设计模式和使用场景介绍

创建型

单例模式

  • 名词解释
    • 全局只包含一个对象。
  • 使用场景
    • 一些有可能会产生争抢的资源。比如某个文件描述符,某个信号量等,在类内被设置为临界资源;
    • 对象内维护的资源独一份。比如配置文件内的配置,一些全局变量。
  • 编码实现
    • 饿汉式
    public class IdGenerator {
       private AtomicLong id = new AtomicLong(0);
       //类内创建一个自己的对象。  
       private static final IdGenerator instance = new IdGenerator();  
       //构造设置成私有,避免在外部被创建。
       private IdGenerator() {}
       //使用静态函数返回对象。
       public static IdGenerator getInstance() {
           return instance;  
       }  
       public long getId() {
            return id.incrementAndGet();
        }
     }
    
    • 懒汉式
    • 双重检测

工厂模式

  • 名词解释
    • 通过给工厂类对象传递不同的参数,在工厂类中创建继承自同一接口或父类的子类。
    • 是一种依赖反转思想的经典实现模式:如果想添加新的实现子类,可以最小化的修改调用具体业务的框架代码。
  • 使用场景
    • 其实是一种典型的复杂度转移:将复杂的if/else/swtich逻辑转移到工厂中实现。
    • 业务场景是根据不同的前置条件执行不同的子对象逻辑。
  • 编码实现
    public class RuleConfigSource { 
    public RuleConfig load(String ruleConfigFilePath) {
     	String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
     	IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
     	if (parserFactory == null) { 
          	throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath); 
      	}
       	IRuleConfigParser parser = parserFactory.createParser(); 
       	String configText = "";
        //从ruleConfigFilePath文件中读取配置文本到configText中 
        RuleConfig ruleConfig = parser.parse(configText);
         return ruleConfig; 
     } 
     private String getFileExtension(String filePath) {
      //...解析文件名获取扩展名,比如rule.json,返回json return "json"; 
      }
    }
    //因为工厂类只包含方法,不包含成员变量,完全可以复用,
    //不需要每次都创建新的工厂类对象,所以,简单工厂模式的第二种实现思路更加合适。
    public class RuleConfigParserFactoryMap {
     //工厂的工厂  
     private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
     static {    
     			    cachedFactories.put("json", new JsonRuleConfigParserFactory());    
                    cachedFactories.put("xml", new XmlRuleConfigParserFactory());    
                    cachedFactories.put("yaml", new YamlRuleConfigParserFactory());   
                    cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());  
            }  
       public static IRuleConfigParserFactory getParserFactory(String type) {
           if (type == null || type.isEmpty()) {  return null;    }    
          IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
          return parserFactory;  
       }
    }
    

建造者模式

  • 名词解释
    • 构建一个类对象时,先将需要的参数传递给建造者类对象。通过建造者对象判定参数是否合规,多个参数之间是否无悖逆(如min > max),建造者判定参数合规后一口气构建出业务相关类对象。
    • 这样做的好处是避免了业务相关类构造函数过多,构造函数中还需要包含冗余的验证参数程序。建造者就是将这一部分代码抽出来复用的模式。
  • 使用场景
    • 构造函数中包含大量参数。或者类中属性间有关联性,需要赋值后额外检查一下关系。
  • 编码实现

public class ResourcePoolConfig {
  private String name;
  private int maxTotal;
  private int maxIdle;
  private int minIdle;

  private ResourcePoolConfig(Builder builder) {
    this.name = builder.name;
    this.maxTotal = builder.maxTotal;
    this.maxIdle = builder.maxIdle;
    this.minIdle = builder.minIdle;
  }
  //...省略getter方法...

  //我们将Builder类设计成了ResourcePoolConfig的内部类。
  //我们也可以将Builder类设计成独立的非内部类ResourcePoolConfigBuilder。
  public static class Builder {
    private static final int DEFAULT_MAX_TOTAL = 8;
    private static final int DEFAULT_MAX_IDLE = 8;
    private static final int DEFAULT_MIN_IDLE = 0;
    private String name;
    private int maxTotal = DEFAULT_MAX_TOTAL;
    private int maxIdle = DEFAULT_MAX_IDLE;
    private int minIdle = DEFAULT_MIN_IDLE;
    
    public ResourcePoolConfig build() {
      // 校验逻辑放到这里来做,包括必填项校验、依赖关系校验、约束条件校验等
      if (StringUtils.isBlank(name)) {
        throw new IllegalArgumentException("...");
      }
      if (maxIdle > maxTotal) {
        throw new IllegalArgumentException("...");
      }
      if (minIdle > maxTotal || minIdle > maxIdle) {
        throw new IllegalArgumentException("...");
      }

      return new ResourcePoolConfig(this);
    }

    public Builder setName(String name) {
      if (StringUtils.isBlank(name)) {
        throw new IllegalArgumentException("...");
      }
      this.name = name;
      return this;
    }

    public Builder setMaxTotal(int maxTotal) {
      if (maxTotal <= 0) {
        throw new IllegalArgumentException("...");
      }
      this.maxTotal = maxTotal;
      return this;
    }

    public Builder setMaxIdle(int maxIdle) {
      if (maxIdle < 0) {
        throw new IllegalArgumentException("...");
      }
      this.maxIdle = maxIdle;
      return this;
    }

    public Builder setMinIdle(int minIdle) {
      if (minIdle < 0) {
        throw new IllegalArgumentException("...");
      }
      this.minIdle = minIdle;
      return this;
    }
  }
}

// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
        .setName("dbconnectionpool")
        .setMaxTotal(16)
        .setMaxIdle(10)
        .setMinIdle(12)
        .build();

原型模式

  • 名词解释
    • 创建对象时直接(深/浅)拷贝当前已经成型的对象。讲白话就是因为重创建一个实例太复杂/太耗时间,所以直接拷贝一份拿来用。
  • 使用场景
    • 多用在重新创建对象不便的场景,比如是将数据库内容映射到内存中的一个map中,或是内存中维护的大关系表。
  • 编码实现

结构型

代理模式

  • 名词解释
    • 在不修改原始类的基础上,新建代理类,并将原始类对象外“包裹”上新的业务,用以拓展业务。
    • c++如何实现动态代理?要了解一下aop
      • 我了解了一下兄弟们,c++想自己搞个反射,如果只是根据字串返回对象其实很简单。但是创建的对象要修改函数我还是没有头绪。有没有董哥私信评论聊一聊。
      • 我自己理了一下思路,静态代理都没差。动态代理如果想实现,我的感觉非常像模板模式:直接在接口中添加计数逻辑,然后在子类中实现业务逻辑。
  • 使用场景
    • 想要对某类拓展非业务功能:添加日志,发送信号,统计计数等。
  • 编码实现
    • 静态代理

public class UserControllerProxy extends UserController {
  private MetricsCollector metricsCollector;

  public UserControllerProxy() {
    this.metricsCollector = new MetricsCollector();
  }

  public UserVo login(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();

    UserVo userVo = super.login(telephone, password);

    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("login", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);

    return userVo;
  }

  public UserVo register(String telephone, String password) {
    long startTimestamp = System.currentTimeMillis();

    UserVo userVo = super.register(telephone, password);

    long endTimeStamp = System.currentTimeMillis();
    long responseTime = endTimeStamp - startTimestamp;
    RequestInfo requestInfo = new RequestInfo("register", responseTime, startTimestamp);
    metricsCollector.recordRequest(requestInfo);

    return userVo;
  }
}
//UserControllerProxy使用举例
UserController userController = new UserControllerProxy();

桥接模式

  • 名词解释
    • 一个类存在两个(或多个)独立变化的维度,我们通过组合的方式,让这两个(或多个)维度可以独立进行扩展。
    • 说白话就是多用组合少用继承。
  • 使用场景
    • 在定义方法/属性时一定注意是has-a关系还是is-a关系。如果是has-a关系尽量使用组合,将接口的职责最小化。
  • 编码实现

public interface Flyable {
  void fly();
}
public class FlyAbility implements Flyable {
  @Override
  public void fly() { //... }
}
//省略Tweetable/TweetAbility/EggLayable/EggLayAbility

public class Ostrich implements Tweetable, EggLayable {//鸵鸟
  private TweetAbility tweetAbility = new TweetAbility(); //组合
  private EggLayAbility eggLayAbility = new EggLayAbility(); //组合
  //... 省略其他属性和方法...
  @Override
  public void tweet() {
    tweetAbility.tweet(); // 委托
  }
  @Override
  public void layEgg() {
    eggLayAbility.layEgg(); // 委托
  }
}

装饰器模式

  • 名词解释
    • 装饰器类是对功能的增强。
    • 装饰器类和原始类继承同样的父类,这样我们可以对原始类“嵌套”多个装饰器类。
  • 使用场景
    • 比如读文件类read函数要添加缓存功能,又要添加按字节/字符读取等功能。因为装饰器可以将功能随意组合,所以可以有效避免一口气新建太多类。
  • 编码实现
// 代理模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
  void f();
}
public class A impelements IA {
  public void f() { //... }
}
public class AProxy implements IA {
  private IA a;
  public AProxy(IA a) {
    this.a = a;
  }
  
  public void f() {
    // 新添加的代理逻辑
    a.f();
    // 新添加的代理逻辑
  }
}

// 装饰器模式的代码结构(下面的接口也可以替换成抽象类)
public interface IA {
  void f();
}
public class A implements IA {
  public void f() { //... }
}
public class ADecorator implements IA {
  private IA a;
  public ADecorator(IA a) {
    this.a = a;
  }
  
  public void f() {
    // 功能增强代码
    a.f();
    // 功能增强代码
  }
}

适配器模式

  • 名词解释
    • 是一种补救措施。它将不兼容的接口转换为可兼容的接口,让原本由于接口不兼容而不能一起工作的类可以一起工作。
  • 使用场景
    • 系统想引入新的框架,但因为前期规划不好,好多接口不太兼容,则可以使用适配器调整。
  • 编码实现

// 类适配器: 基于继承
public interface ITarget {
 void f1();
 void f2();
 void fc();
}

public class Adaptee {
 public void fa() { //... }
 public void fb() { //... }
 public void fc() { //... }
}

public class Adaptor extends Adaptee implements ITarget {
 public void f1() {
   super.fa();
 }
 
 public void f2() {
   //...重新实现f2()...
 }
 
 // 这里fc()不需要实现,直接继承自Adaptee,这是跟对象适配器最大的不同点
}

// 对象适配器:基于组合
public interface ITarget {
 void f1();
 void f2();
 void fc();
}

public class Adaptee {
 public void fa() { //... }
 public void fb() { //... }
 public void fc() { //... }
}

public class Adaptor implements ITarget {
 private Adaptee adaptee;
 
 public Adaptor(Adaptee adaptee) {
   this.adaptee = adaptee;
 }
 
 public void f1() {
   adaptee.fa(); //委托给Adaptee
 }
 
 public void f2() {
   //...重新实现f2()...
 }
 
 public void fc() {
   adaptee.fc();
 }
}

门面模式

  • 名词解释
    • 为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。
    • 就是将更细粒度的接口合并成粗粒度的接口给客户端调用,减少调用频次,降低复杂度。
  • 使用场景
    • 远程调用或者数据库操作中,每次过程都会通过网络交互。使用门面模式,将多个请求/回应打包到一起,减少了资源消耗。
  • 编码实现

组合模式

  • 名词解释
    • 将逻辑上有 包含/上下级 关系的对象抽象出统一的处理逻辑复用,让程序更简练更易读。
  • 使用场景
    • 有树状结构的业务模型,比如文件和文件夹,员工和部门。
  • 编码实现

public abstract class FileSystemNode {
 protected String path;

 public FileSystemNode(String path) {
   this.path = path;
 }

 public abstract int countNumOfFiles();
 public abstract long countSizeOfFiles();

 public String getPath() {
   return path;
 }
}

public class File extends FileSystemNode {
 public File(String path) {
   super(path);
 }

 @Override
 public int countNumOfFiles() {
   return 1;
 }

 @Override
 public long countSizeOfFiles() {
   java.io.File file = new java.io.File(path);
   if (!file.exists()) return 0;
   return file.length();
 }
}

public class Directory extends FileSystemNode {
 private List<FileSystemNode> subNodes = new ArrayList<>();

 public Directory(String path) {
   super(path);
 }

 @Override
 public int countNumOfFiles() {
   int numOfFiles = 0;
   for (FileSystemNode fileOrDir : subNodes) {
     numOfFiles += fileOrDir.countNumOfFiles();
   }
   return numOfFiles;
 }

 @Override
 public long countSizeOfFiles() {
   long sizeofFiles = 0;
   for (FileSystemNode fileOrDir : subNodes) {
     sizeofFiles += fileOrDir.countSizeOfFiles();
   }
   return sizeofFiles;
 }

 public void addSubNode(FileSystemNode fileOrDir) {
   subNodes.add(fileOrDir);
 }

 public void removeSubNode(FileSystemNode fileOrDir) {
   int size = subNodes.size();
   int i = 0;
   for (; i < size; ++i) {
     if (subNodes.get(i).getPath().equalsIgnoreCase(fileOrDir.getPath())) {
       break;
     }
   }
   if (i < size) {
     subNodes.remove(i);
   }
 }
}

享元模式

  • 名词解释
    • 为了节省内存/硬盘等资源,将各对象中维护的统一的,不变的属性抽取出来,在对象中使用 指针/引用。
  • 使用场景
    • 对象中的映射表,长的字串都可以使用享元只维护一份。
  • 编码实现

行为型

观察者模式

  • 名词解释
    • 被观察者和观察者维护一种类似信号与槽的关系。当有事件发生,被观察者将通知观察者执行后续动作。
  • 使用场景
    • 一个极端的理解,在函数1中调用函数2,函数2就可以被称作观察者。
    • 更多的时候观察者模式视为一种解偶手段。使用消息中间键之类在层与层之间,模块与模块之间通讯。
  • 编码实现

public interface Subject {
 void registerObserver(Observer observer);
 void removeObserver(Observer observer);
 void notifyObservers(Message message);
}

public interface Observer {
 void update(Message message);
}

public class ConcreteSubject implements Subject {
 private List<Observer> observers = new ArrayList<Observer>();

 @Override
 public void registerObserver(Observer observer) {
   observers.add(observer);
 }

 @Override
 public void removeObserver(Observer observer) {
   observers.remove(observer);
 }

 @Override
 public void notifyObservers(Message message) {
   for (Observer observer : observers) {
     observer.update(message);
   }
 }

}

public class ConcreteObserverOne implements Observer {
 @Override
 public void update(Message message) {
   //TODO: 获取消息通知,执行自己的逻辑...
   System.out.println("ConcreteObserverOne is notified.");
 }
}

public class ConcreteObserverTwo implements Observer {
 @Override
 public void update(Message message) {
   //TODO: 获取消息通知,执行自己的逻辑...
   System.out.println("ConcreteObserverTwo is notified.");
 }
}

public class Demo {
 public static void main(String[] args) {
   ConcreteSubject subject = new ConcreteSubject();
   subject.registerObserver(new ConcreteObserverOne());
   subject.registerObserver(new ConcreteObserverTwo());
   subject.notifyObservers(new Message());
 }
}

模板模式

  • 名词解释
    • 将方法流程固定成模板放入到 接口/实现类中,一般写框架骨架会用到的模式。
  • 使用场景
    • 有固定流程的场景。比如操作任何数据库(连接,发送请求,获取内容,断开连接)、操作文件(open,read,write,close)等,都可以将流程抽象出接口,通过继承的方式在子类中根据具体情况(比如各种数据库,比如设备符文件)具体实现。
  • 编码实现

public abstract class AbstractClass {
 public final void templateMethod() {
   //...
   method1();
   //...
   method2();
   //...
 }
 
 protected abstract void method1();
 protected abstract void method2();
}

public class ConcreteClass1 extends AbstractClass {
 @Override
 protected void method1() {
   //...
 }
 
 @Override
 protected void method2() {
   //...
 }
}

public class ConcreteClass2 extends AbstractClass {
 @Override
 protected void method1() {
   //...
 }
 
 @Override
 protected void method2() {
   //...
 }
}

AbstractClass demo = ConcreteClass1();
demo.templateMethod();

策略模式

  • 名词解释
    • 非常类似工厂模式,定义接口然后不同的实现类继承后提供不同实现。不同点在于策略侧重于选出 方法 去解决问题,而工厂则是选出符合条件的子类对现应付后续的流程
  • 使用场景
    • 将项目中存在的大量的if/else削除,将复杂度转移到策略类中去。
  • 编码实现

// 策略的定义
public interface DiscountStrategy {
 double calDiscount(Order order);
}
// 省略NormalDiscountStrategy、GrouponDiscountStrategy、PromotionDiscountStrategy类代码...

// 策略的创建
public class DiscountStrategyFactory {
 private static final Map<OrderType, DiscountStrategy> strategies = new HashMap<>();

 static {
   strategies.put(OrderType.NORMAL, new NormalDiscountStrategy());
   strategies.put(OrderType.GROUPON, new GrouponDiscountStrategy());
   strategies.put(OrderType.PROMOTION, new PromotionDiscountStrategy());
 }

 public static DiscountStrategy getDiscountStrategy(OrderType type) {
   return strategies.get(type);
 }
}

// 策略的使用
public class OrderService {
 public double discount(Order order) {
   OrderType type = order.getType();
   DiscountStrategy discountStrategy = DiscountStrategyFactory.getDiscountStrategy(type);
   return discountStrategy.calDiscount(order);
 }
}

职责链模式

  • 名词解释
    • 多个处理器依次处理同一个请求。一个请求先经过 A 处理器处理,然后再把请求传递给 B 处理器,B 处理器处理完后再传递给 C 处理器,以此类推,形成一个链条。链条上的每个处理器各自承担各自的处理职责,所以叫作职责链模式。
  • 使用场景
    • 过滤器,拦截器,dpi处理逻辑都可以使用。使用链表或者数组都可以。说白了就是挨个都试一遍,简单的实现是直接用迭代器,复杂一点用模板执行完自己的业务逻辑后自己调用下一个判定。
  • 编码实现

迭代器模式

  • 名词解释
    • 现在编程语言中包含很多的数据结构:数组、链表、树、图等。遍历方式不尽相同,迭代器模式就是抽象出next动作,将实现隐藏在各自的实现中。
  • 使用场景
    • 各种数据结构遍历都可以用迭代器迭代。c++中STL已经包含实现。
  • 编码实现

状态模式

  • 名词解释
    • 有限状态机。包含触发事件,状态列表,和状态转移三个部分。经典的流程是 :通过事件触发当前状态转移到状态列表的其他状态。
  • 使用场景
    • 有限状态的使用场景。比如超级玛丽奥对象就可以视为一个有限状态机
      • 吃蘑菇、星星就可以视为触发事件。
      • 小玛丽奥,大玛丽奥,弹珠玛丽奥,无敌玛丽奥,死玛丽奥就可以视为状态列表。
      • 状态转移就是实践发生时在对象在状态列表中切换。
  • 编码实现

访问者模式

  • 名词解释
    • 当类A和其他类有继承关系时,将类对象作为参数传入到函数,函数重载参数中将类替换为子类对象或父类对象。这种情况编译都过不了,因为运行时不能确定调用哪个函数了,就是我们常说的有二意性。访问者模式就是为了解决这个问题,方式也很简单,将函数(或包含函数的对象B)传入到类A对象中去,在类A内再调用B的方法即可。
  • 使用场景
    • 当多个实现相同接口或父类(A)的子类(CDEF)包含在数组中,遍历数组,将子类作为参数传入类B中的函数时,类B的函数无法区分CDEF的对象进而调用不同的处理函数,索性在类CDEF中都要实现将B作为参数传入的接口,在接口内调用类B针对该子类的实现逻辑。将类B称作访问者。
  • 编码实现
public abstract class ResourceFile {
 protected String filePath;
 public ResourceFile(String filePath) {
   this.filePath = filePath;
 }
 abstract public void accept(Extractor extractor);
}

public class PdfFile extends ResourceFile {
 public PdfFile(String filePath) {
   super(filePath);
 }

 @Override
 public void accept(Extractor extractor) {
   extractor.extract2txt(this);
 }

 //...
}

//...PPTFile、WordFile跟PdfFile类似,这里就省略了...
//...Extractor代码不变...

public class ToolApplication {
 public static void main(String[] args) {
   Extractor extractor = new Extractor();
   List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
   for (ResourceFile resourceFile : resourceFiles) {
     resourceFile.accept(extractor);
   }
 }

 private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
   List<ResourceFile> resourceFiles = new ArrayList<>();
   //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
   resourceFiles.add(new PdfFile("a.pdf"));
   resourceFiles.add(new WordFile("b.word"));
   resourceFiles.add(new PPTFile("c.ppt"));
   return resourceFiles;
 }
}

备忘录模式

  • 名词解释
    • 对敏感的数据时刻保留缓存,当异常发生时将导致异常的输入删除后将数据快速恢复到之前的状态。
  • 使用场景
    • 多个动作合并成一个事务,未执行完有异常发生但原始数据已无法简单恢复时,可以使用备忘录模式提前记录当前状态。
  • 编码实现

public class InputText {
 private StringBuilder text = new StringBuilder();

 public String getText() {
   return text.toString();
 }

 public void append(String input) {
   text.append(input);
 }

 public Snapshot createSnapshot() {
   return new Snapshot(text.toString());
 }

 public void restoreSnapshot(Snapshot snapshot) {
   this.text.replace(0, this.text.length(), snapshot.getText());
 }
}

public class Snapshot {
 private String text;

 public Snapshot(String text) {
   this.text = text;
 }

 public String getText() {
   return this.text;
 }
}

public class SnapshotHolder {
 private Stack<Snapshot> snapshots = new Stack<>();

 public Snapshot popSnapshot() {
   return snapshots.pop();
 }

 public void pushSnapshot(Snapshot snapshot) {
   snapshots.push(snapshot);
 }
}

public class ApplicationMain {
 public static void main(String[] args) {
   InputText inputText = new InputText();
   SnapshotHolder snapshotsHolder = new SnapshotHolder();
   Scanner scanner = new Scanner(System.in);
   while (scanner.hasNext()) {
     String input = scanner.next();
     if (input.equals(":list")) {
       System.out.println(inputText.toString());
     } else if (input.equals(":undo")) {
       Snapshot snapshot = snapshotsHolder.popSnapshot();
       inputText.restoreSnapshot(snapshot);
     } else {
       snapshotsHolder.pushSnapshot(inputText.createSnapshot());
       inputText.append(input);
     }
   }
 }
}

命令模式

  • 名词解释
    • 命令模式将请求(命令)封装为一个对象,这样可以使用不同的请求参数化其他对象(将不同请求依赖注入到其他对象),并且能够支持请求(命令)的排队执行、记录日志、撤销等(附加控制)功能。
  • 使用场景
    • 非常类似策略模式,我的理解是命令模式不同处理逻辑之间没有关系而已
  • 编码实现

解释器模式

  • 名词解释
    • 解释器模式为某个语言定义它的语法(或者叫文法)表示,并定义一个解释器用来处理这个语法。
  • 使用场景
    • 系统中设置一些flag来代表复杂的意思,就是最简单的解释器。
    • 说高端点,甚至可以自己规定一门语言用解释器解释。
  • 编码实现

中介模式

  • 名词解释
    • 中介模式定义了一个单独的(中介)对象,来封装一组对象之间的交互。将这组对象之间的交互委派给与中介对象交互,来避免对象之间的直接交互。
  • 使用场景
    • 类似观察者模式。但观察者/被观察者的关系是一种客户/服务的关系,中介模式体现的是一种平等的关系。
    • 也是为了解偶罢了。
  • 编码实现
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值