面向对象设计6大原则

概览

在这里插入图片描述

单一职责

1、单一职责

Single Responsibility Principle,简称是SRP。SRP的英文定义是:

There should never be more than one reason for a class to change.

翻译过来的意思是:

应该有且仅有一个原因引起类的变更。

或许我们可以使用更加白话来解释:

一个类或接口只负责一项职责。

2、为什么需要单一职责?

如果一个类负责一个以上的职责,这些职责就可能耦合在一起,当某个职责发生变化时,可能会影响其它的职责。

3、单一职责优点
  • 降低类的复杂度
  • 提高类的可读性
  • 提高系统的可维护性
  • 变更引起的风险降低:变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
4、注意
  • 单一原则是备受争议的,争议之处就是对职责的定义,什么是类的职责,以及怎么划分类的职责。

  • 单一职责原则提出了一个编写程序的标准,用“职责”或“变化原因”来衡量接口或类设计得是否优良,但是“职责”和“变化原因”都是不可度量的,因项目而异,因环境而异。

  • 单一职责适用于接口、类,同时也适用于方法。

5、建议
  • 接口一定要做到单一职责

  • 类的设计尽量做到只有一个原因引起变化。

对于实现类就需要多方面考虑了。生搬硬套单一职责原则会引起类的剧增,给维护带来非常多的麻烦,而且过分细分类的职责也会人为地增加系统的复杂性。举个例子:

/**
 * Create by SunnyDay 2022/06/08 13:05:35
 **/
public interface IPhone {
    /**
     * 拨号
     */
    void dial(String phoneNumber);

    /**
     * 通话
     */
    void chat(String msg);
    /**
     * 挂机
     * */
    void hangup();
}

如上接口设计的不符合单一职责,接口有两个职责:

  • 协议管理:拨号、挂机属于协议管理。
  • 数据传送:通话属于数据传送。

上述二者功能变化应该是互不影响,因此采用单一职责进行更改:

/**
 * Create by SunnyDay 2022/06/08 13:31:47
 **/
public interface IConnectManager {

    void dial(String phoneNumber);

    void hangup();
}
/**
 * Create by SunnyDay 2022/06/08 13:33:46
 **/
public interface IDataTransfer {
    void chat(String msg);
}

到这里接口就按照单一职责重新设计了,但是实现类要是严格按照单一职责来划分的话我们需要定义ConnectManager实现IConnectManager,定义DataTransfer实现IDataTransfer。然后两实现类采用组合方案作为属性放到Phone类中。

上述方案由于组合是一种强耦合关系,多了新类还不如使用接口实现的方式呢,我们可以修改方案,让Phone类直接实现IConnectManager,IDataTransfer接口。

一个类实现了两个接口,把两个职责融合在一个类中。这样Phone有两个原因引起变化了,但我们是面向接口编程,对外公布的是接口而不是实现类。如果真要实现类的单一职责,这个就必须使用上面的组合模式了,这会引起类间耦合过重、类的数量增加等问题,人为地增加了设计的复杂性。

里氏替换原则

1、里氏替换

Liskov Substitution Principle,简称LSP,这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。因此叫里氏替换原则。LSP的英文定义:

Functions that use pointers or references to base classes mustbe able to use objects of derived classes without knowing it.

翻译过来意思就是:

所有引用基类的地方必须能透明地使用其子类的对象。

或许我们可以使用更加白话来解释:

子类可以扩展父类的功能,但不能改变父类原有的功能。

只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是,反过来就不行了,有子类出现的地方,父类未必就能适应。

2、为啥需要里氏替换原则

这里需要先了解下继承体系的优缺点:

继承的优点:

  • 代码共享,减少创建类的工作量。
  • 提高代码的重用性。
  • 子类可以形似父类,但又异于父类。
  • 提高代码的可扩展性,实现父类的方法就可以“为所欲为”了。

继承的缺点:

  • 侵入性,继承后就拥有了父类的属性和方法。
  • 降低代码的灵活性,子类必须拥有父类的属性和方法,让子类自由的世界中多了些约束。
  • 增强了耦合性。当父类的常量、变量和方法被修改时,需要考虑子类的修改。

其实里氏替换原则是对继承体系的规范,在继承体系中,父类中已经实现好的方法,如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。里氏替换原则则是规范我们不要去修改非抽象方法。

3、注意
  • 在类中调用其他类时务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
  • 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。来个例子:
class A{
    public int func1(int a, int b){
        return a-b;
    }
}

public class Client{
    public static void main(String[] args){
        A a = new A();
        System.out.println("100-50="+a.func1(100, 50));
        System.out.println("100-80="+a.func1(100, 80));
    }
} 

运行结果:
100-50=50
100-80=20

后来,我们需要增加一个新的功能:完成两数相加,然后再与100求和,由类B来负责。即类B需要完成两个功能:

  • 两数相减。

  • 两数相加,然后再加100。

由于类A已经实现了第一个功能两数相减,所以类B继承类A后只需要再完成第二个功能两数相加然后再加100即可:

class B extends A{
    // 重写了父类的方法
    public int func1(int a, int b){
        return a+b;
    }

    public int func2(int a, int b){
        return func1(a,b)+100;
    }
}

public class Client{
    public static void main(String[] args){
        B b = new B();
        System.out.println("100-50="+b.func1(100, 50));
        System.out.println("100-80="+b.func1(100, 80));
        System.out.println("100+20+100="+b.func2(100, 20));
    }
}B完成后,运行结果:

100-50=150
100-80=180
100+20+100=220

我们发现原本运行正常的相减功能发生了错误。原因就是类B在给方法起名时无意中重写了父类的方法,造成所有运行相减功能的代码全部调用了类B重写后的方法,造成原本运行正常的功能出现了错误。

在本例中,引用基类A完成的功能,换成子类B之后,发生了异常。在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。

如果非要重写父类的非抽象方法,父类的某些方法在子类中已经发生“畸变”,比较通用的做法是:原来的父类和子类都继承一个更通俗的基类,原有的继承关系去掉,采用组合等关系代替。也即常说的使用组合代替继承:

/**
 * Create by SunnyDay 2022/06/07 21:00:00
 * 功能抽象出来。
 **/
public abstract class Base {
    /**
     * 两数相减
     */
    public abstract int func1(int a, int b);
}

/**
 * Create by SunnyDay 2022/06/07 21:01:00
 **/
public class A extends Base {
    /**
     * A类实现两数字相减
     * */
    @Override
    public int func1(int a, int b) {
        return a - b;
    }
}
/**
 * Create by SunnyDay 2022/06/07 21:03:01
 **/
public class B extends Base {

    private A mA;

    public B(A a) {
        mA = a;
    }

    /**
     * B类重写基类方法,然后使用组合替代继承。实现两数相减。
     */
    @Override
    public int func1(int a, int b) {
        return mA.func1(a, b);
    }
    // B类拓展新功能
    public int func2(int a, int b) {
        return a + b + 100;
    }
}
4、再次来理解里氏替换原则
  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。

  • 子类中可以增加自己特有的方法。

  • 当子类的方法重载父类的方法时,方法的形参要比父类方法的输入参数更宽松。

  • 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格。

看上去很不可思议,因为我们会发现在自己编程中常常会违反里氏替换原则,程序照样跑的好好的。所以大家都会产生这样的疑问,假如我非要不遵循里氏替换原则会有什么后果?后果就是,你写的代码出问题的几率将会大大增加。

依赖倒置原则

1、依赖倒置原则

Dependence Inversion Principle,简称DIP。英文定义:

  • High level modules should not depend upon low level modules.Both should depend upon abstractions.

  • Abstractions should not depend upon details.

  • Details should depend upon abstractions.

翻译过来意思就是:

  • 高层模块不应该依赖低层模块,两者都应该依赖其抽象;

  • 抽象不应该依赖细节;

  • 细节应该依赖抽象。

或许我们可以使用更加白话来解释:

面向抽象或者接口编程。

java中抽象就是接口或者抽象类,实现就是实现类。依赖倒置的三段定义在java中还可以这样理解:

  • 模块间的依赖通过抽象发生,实现类之间不发生直接的依赖关系,其依赖关系是通过接口或抽象类产生的;
  • 接口或抽象类不依赖于实现类;
  • 实现类依赖接口或抽象类。
2、依赖倒置原则好处
  • 减少类间的耦合性,
  • 提高系统的稳定性,
  • 降低并行开发引起的风险,
  • 提高代码的可读性和可维护性。
3、栗子

依赖倒置原则的核心思想是面向抽象编程,我们依旧用一个例子来说明面向抽象编程比相对于面向实现编程好在什么地方。

母亲给孩子讲故事,只要给她一本书,她就可以照着书给孩子讲故事了:

class Book{
    public String getContent(){
        return "很久很久以前有一个阿拉伯的故事……";
    }
}

class Mother{
    public void narrate(Book book){
        System.out.println("妈妈开始讲故事");
        System.out.println(book.getContent());
    }
}

public class Client{
    public static void main(String[] args){
        Mother mother = new Mother();
        mother.narrate(new Book());
    }
} 
运行结果:
妈妈开始讲故事
很久很久以前有一个阿拉伯的故事……

上述是面向实现的编程,即依赖的是Book这个具体的实现类;看起来功能都很OK,也没有什么问题。

运行良好,假如有一天,需求变成这样:不是给书而是给一份报纸,让这位母亲讲一下报纸上的故事,报纸的代码如下:

class Newspaper{
    public String getContent(){
        return "林书豪38+7领导尼克斯击败湖人……";
    }
}

这位母亲却办不到,因为她居然不会读报纸上的故事,这太荒唐了,只是将书换成报纸,居然必须要修改Mother才能读。假如以后需求换成杂志呢?换成网页呢?还要不断地修改Mother,这显然不是好的设计。原因就是Mother与Book之间的耦合性太高了,必须降低他们之间的耦合度才行。

我们引入一个抽象的接口IReader。读物,只要是带字的都属于读物:

interface IReader{
    public String getContent();
} 

Mother类与接口IReader发生依赖关系,而Book和Newspaper都属于读物的范畴,他们各自都去实现IReader接口,这样就符合依赖倒置原则了,代码修改为:

class Newspaper implements IReader {
    public String getContent(){
        return "林书豪17+9助尼克斯击败老鹰……";
    }
}
class Book implements IReader{
    public String getContent(){
        return "很久很久以前有一个阿拉伯的故事……";
    }
}

class Mother{
    public void narrate(IReader reader){
        System.out.println("妈妈开始讲故事");
        System.out.println(reader.getContent());
    }
}

public class Client{
    public static void main(String[] args){
        Mother mother = new Mother();
        mother.narrate(new Book());
        mother.narrate(new Newspaper());
    }
}

运行结果:
妈妈开始讲故事
很久很久以前有一个阿拉伯的故事……
妈妈开始讲故事
林书豪17+9助尼克斯击败老鹰…

这样修改后,无论以后怎样扩展Client类,都不需要再修改Mother类了。实际情况中,代表高层模块的Mother类将负责完成主要的业务逻辑,一旦需要对它进行修改,引入错误的风险极大。所以遵循依赖倒置原则可以降低类之间的耦合性,提高系统的稳定性,降低修改程序造成的风险。

4、小结

采用依赖倒置原则给多人并行开发带来了极大的便利,比如上例中,原本Mother类与Book类直接耦合时,Mother类必须等Book类编码完成后才可以进行编码,因为Mother类依赖于Book类。修改后的程序则可以同时开工,互不影响,因为Mother与Book类一点关系也没有。参与协作开发的人越多、项目越庞大,采用依赖导致原则的意义就越重大。

传递依赖关系有三种方式,以上的例子中使用的方法是接口传递,另外还有两种传递方式:构造方法传递和setter方法传递。

在实际编程中,我们一般需要做到如下几点:

  • 低层模块尽量都要有抽象类或接口,或者两者都有。

  • 变量的声明类型尽量是抽象类或接口。

  • 任何类都不应该从具体类派生

  • 使用继承时遵循里氏替换原则。

接口隔离原则

1、接口隔离原则

英文定义:

  • Clients should not be forced to depend upon interfaces that they don’t use.
  • The dependency of one class to another one should depend on the smallest possible interface.

翻译过来意思就是:

  • 客户端不应该依赖它不需要的接口
  • 类间的依赖关系应该建立在最小的接口上。

二者都是要求接口细化,接口纯洁,定义如出一辙,只是一个事物的两种不同描述。

或许我们可以使用更加白话来解释:

接口尽量细化,同时接口中的方法尽量少。

2、例子

类A通过接口I依赖类B,类C通过接口I依赖类D,如果接口I对于类A和类B来说不是最小接口,则类B和类D必须去实现他们不需要的方法。解决方案是将臃肿的接口I拆分为独立的几个接口,类A和类C分别与他们需要的接口建立依赖关系。也就是采用接口隔离原则

具体来说:类A依赖接口I中的方法1、方法2、方法3,类B是对类A依赖的实现。类C依赖接口I中的方法1、方法4、方法5,类D是对类C依赖的实现。对于类B和类D来说,虽然他们都存在着用不到的方法(也就是图中红色字体标记的方法),但由于实现了接口I,所以也必须要实现这些用不到的方法。对类图不熟悉的可以参照程序代码来理解,代码如下:

interface I {
    public void method1();
    public void method2();
    public void method3();
    public void method4();
    public void method5();
}

//类A通过接口I依赖类B.这里通过方法注入传递B对象。
//A只使用了接口method1、method2、method3
class A{
    public void depend1(I i){
        i.method1();
    }
    public void depend2(I i){
        i.method2();
    }
    public void depend3(I i){
        i.method3();
    }
}

class B implements I{
    public void method1() {
        System.out.println("类B实现接口I的方法1");
    }
    public void method2() {
        System.out.println("类B实现接口I的方法2");
    }
    public void method3() {
        System.out.println("类B实现接口I的方法3");
    }
    //对于类B来说,method4和method5不是必需的,但是由于接口A中有这两个方法,
    //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
    public void method4() {}
    public void method5() {}
}
//类C通过接口I依赖类D.这里通过方法注入传入D对象。
//C只使用了接口method1、method4、method5
class C{
    public void depend1(I i){
        i.method1();
    }
    public void depend2(I i){
        i.method4();
    }
    public void depend3(I i){
        i.method5();
    }
}

class D implements I{
    public void method1() {
        System.out.println("类D实现接口I的方法1");
    }
    //对于类D来说,method2和method3不是必需的,但是由于接口A中有这两个方法,
    //所以在实现过程中即使这两个方法的方法体为空,也要将这两个没有作用的方法进行实现。
    public void method2() {}
    public void method3() {}

    public void method4() {
        System.out.println("类D实现接口I的方法4");
    }
    public void method5() {
        System.out.println("类D实现接口I的方法5");
    }
}

public class Client{
    public static void main(String[] args){
        A a = new A();
        a.depend1(new B());
        a.depend2(new B());
        a.depend3(new B());

        C c = new C();
        c.depend1(new D());
        c.depend2(new D());
        c.depend3(new D());
    }
}

可以看到,如果接口过于臃肿,只要接口中出现的方法,不管对依赖于它的类有没有用处,实现类中都必须去实现这些方法,这显然不是好的设计。如果将这个设计修改为符合接口隔离原则,就必须对接口I进行拆分。在这里我们将原有的接口I拆分为三个接口,I1、I2、I3:

interface I1 {
    public void method1();
}

interface I2 {
    public void method2();
    public void method3();
}

interface I3 {
    public void method4();
    public void method5();
}

class A{
    public void depend1(I1 i){
        i.method1();
    }
    public void depend2(I2 i){
        i.method2();
    }
    public void depend3(I2 i){
        i.method3();
    }
}

class B implements I1, I2{
    public void method1() {
        System.out.println("类B实现接口I1的方法1");
    }
    public void method2() {
        System.out.println("类B实现接口I2的方法2");
    }
    public void method3() {
        System.out.println("类B实现接口I2的方法3");
    }
}

class C{
    public void depend1(I1 i){
        i.method1();
    }
    public void depend2(I3 i){
        i.method4();
    }
    public void depend3(I3 i){
        i.method5();
    }
}

class D implements I1, I3{
    public void method1() {
        System.out.println("类D实现接口I1的方法1");
    }
    public void method4() {
        System.out.println("类D实现接口I3的方法4");
    }
    public void method5() {
        System.out.println("类D实现接口I3的方法5");
    }
} 
3、单一职责与接口隔离的区别
  • 单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。
  • 单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节。
  • 接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构建。

举个栗子:

一个接口的职责可能包含10个方法,这10个方法都放在一个接口中,并且提供给多个模块访问,各个模块按照规定的权限来访问,在系统外通过文档约束“不使用的方法不要访问”,按照单一职责原则是允许的,按照接口隔离原则是不允许的,因为它要求“尽量使用多个专门的接口”。专门的接口指什么?就是指提供给每个模块的都应该是单一接口,提供给几个模块就应该有几个接口,而不是建立一个庞大的臃肿的接口,容纳所有的客户端访问。

4、采用接口隔离原则注意点:
  • 接口尽量小:这是接口隔离原则的核心定义,不出现臃肿的接口,但是“小”是有限度的,首先就是不能违反单一职责原则。

  • 接口要高内聚:提高接口、类、模块的处理能力,减少对外的交互。

  • 接口设计是有限度:接口的设计粒度越小,系统越灵活。但灵活的同时也带来了结构的复杂化,开发难度增加,可维护性降低,所以接口设计一定要注意适度。

迪米特法则

1、迪米特法则

Law of Demeter 简称LoD,也称为最少知识原则 ,Least KnowledgePrinciple 简称LKP。

定义:一个对象应该对其他对象有最少的了解。

或许我们可以使用更加白话来解释:

一个类应该对自己需要耦合或调用的类知道得最少。被耦合或调用的类的内部是如何复杂都和我没关系,我就知道你提供的这么多public方法,我就调用这么多,其他的我一概不关心。

2、迪米特发的的理解

迪米特法则是对低耦合的约束,有如下几点

  • 只和直接朋友交流:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
  • 朋友之间是有距离的:迪米特法则要求类“羞涩”一点,尽量不要对外公布太多的public方法和非静态的public变量,尽量内敛,多使用private、package-private、protected等访问权限。
  • 是自己的就是自己的:在实际应用中经常会出现这样一个方法:放在本类中也可以,放在其他类中也没有错,那怎么去衡量呢?你可以坚持这样一个原则:如果一个方法放在本类中,既不增加类间关系,也对本类不产生负面影响,那就放置在本类中。
3、举一个例子

有一个集团公司,下属单位有分公司和直属部门,现在要求打印出所有下属单位的员工ID。先来看一下违反迪米特法则的设计。

//总公司员工
class Employee{
    private String id;
    public void setId(String id){
        this.id = id;
    }
    public String getId(){
        return id;
    }
}

//分公司员工
class SubEmployee{
    private String id;
    public void setId(String id){
        this.id = id;
    }
    public String getId(){
        return id;
    }
}

class SubCompanyManager{
    public List<SubEmployee> getAllEmployee(){
        List<SubEmployee> list = new ArrayList<SubEmployee>();
        for(int i=0; i<100; i++){
            SubEmployee emp = new SubEmployee();
            //为分公司人员按顺序分配一个ID
            emp.setId("分公司"+i);
            list.add(emp);
        }
        return list;
    }
}

class CompanyManager{

    public List<Employee> getAllEmployee(){
        List<Employee> list = new ArrayList<Employee>();
        for(int i=0; i<30; i++){
            Employee emp = new Employee();
            //为总公司人员按顺序分配一个ID
            emp.setId("总公司"+i);
            list.add(emp);
        }
        return list;
    }

    public void printAllEmployee(SubCompanyManager sub){
        List<SubEmployee> list1 = sub.getAllEmployee();
        for(SubEmployee e:list1){
            System.out.println(e.getId());
        }

        List<Employee> list2 = this.getAllEmployee();
        for(Employee e:list2){
            System.out.println(e.getId());
        }
    }
}

public class Client{
    public static void main(String[] args){
        CompanyManager e = new CompanyManager();
        e.printAllEmployee(new SubCompanyManager());
    }
} 

现在这个设计的主要问题出在CompanyManager中,根据迪米特法则,只与直接的朋友发生通信,而SubEmployee类并不是CompanyManager类的直接朋友(以局部变量出现的耦合不属于直接朋友),从逻辑上讲总公司只与他的分公司耦合就行了,与分公司的员工并没有任何联系,这样设计显然是增加了不必要的耦合。按照迪米特法则,应该避免类中出现这样非直接朋友关系的耦合。修改后的代码如下:

class SubCompanyManager{
    public List<SubEmployee> getAllEmployee(){
        List<SubEmployee> list = new ArrayList<SubEmployee>();
        for(int i=0; i<100; i++){
            SubEmployee emp = new SubEmployee();
            //为分公司人员按顺序分配一个ID
            emp.setId("分公司"+i);
            list.add(emp);
        }
        return list;
    }
    public void printEmployee(){
        List<SubEmployee> list = this.getAllEmployee();
        for(SubEmployee e:list){
            System.out.println(e.getId());
        }
    }
}

class CompanyManager{
    public List<Employee> getAllEmployee(){
        List<Employee> list = new ArrayList<Employee>();
        for(int i=0; i<30; i++){
            Employee emp = new Employee();
            //为总公司人员按顺序分配一个ID
            emp.setId("总公司"+i);
            list.add(emp);
        }
        return list;
    }

    public void printAllEmployee(SubCompanyManager sub){
        sub.printEmployee();
        List<Employee> list2 = this.getAllEmployee();
        for(Employee e:list2){
            System.out.println(e.getId());
        }
    }
}

修改后,为分公司增加了打印人员ID的方法,总公司直接调用来打印,从而避免了与分公司的员工发生耦合。

迪米特法则的初衷是降低类之间的耦合,由于每个类都减少了不必要的依赖,因此的确可以降低耦合关系。但是凡事都有度,虽然可以避免与非直接的类通信,但是要通信,必然会通过一个“中介”来发生联系,例如本例中,总公司就是通过分公司这个“中介”来与分公司的员工发生联系的。过分的使用迪米特原则,会产生大量这样的中介和传递类,导致系统复杂度变大。所以在采用迪米特法则时要反复权衡,既做到结构清晰,又要高内聚低耦合。

开闭原则

开闭原则就是说对扩展开放,对修改关闭。不修改原有代码的情况下进行扩展。

所以为了使程序的扩展性好,易于维护和升级。想要达到这样的效果,需要面向接口编程。

开闭原则比较抽象,只要我们对前面几项原则遵守的好了,设计出的软件自然是符合开闭原则的。

总结

  • 单一职责原则告诉我们实现类要职责单一;
  • 里氏替换原则告诉我们尽量不要破坏继承体系;
  • 依赖倒置原则告诉我们要面向接口编程;
  • 接口隔离原则告诉我们在设计接口的时候要精简单一;
  • 迪米特法则告诉我们要降低耦合。
  • 而开闭原则是总纲,他告诉我们要对扩展开放,对修改关闭。

参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值