设计模式 学习 2:

六个创建型模式


简单工厂:



 问题: Sunny软件公司欲基于Java语言开发一套图表库,该图表库可以为应用系统提供各种不同外观的图表,例如柱状图、饼状图、折线图等。Sunny软件公司图表库设计人员希望为应用系统开发人员提供一套灵活易用的图表库,而且可以较为方便地对图表库进行扩展,以便能够在将来增加一些新类型的图表。



//抽象图表接口:抽象产品类

interface Chart {

public void display();

}


//柱状图类:具体产品类

class HistogramChart implements Chart {

public HistogramChart() {

System.out.println("创建柱状图!");

}


public void display() {

System.out.println("显示柱状图!");

}

}


//饼状图类:具体产品类

class PieChart implements Chart {

public PieChart() {

System.out.println("创建饼状图!");

}


public void display() {

System.out.println("显示饼状图!");

}

}


//折线图类:具体产品类

class LineChart implements Chart {

public LineChart() {

System.out.println("创建折线图!");

}


public void display() {

System.out.println("显示折线图!");

}

}


//图表工厂类:工厂类

class ChartFactory {

    //静态工厂方法

public static Chart getChart(String type) {

Chart chart = null;

if (type.equalsIgnoreCase("histogram")) {

chart = new HistogramChart();

System.out.println("初始化设置柱状图!");

}

else if (type.equalsIgnoreCase("pie")) {

chart = new PieChart();

System.out.println("初始化设置饼状图!");

}

else if (type.equalsIgnoreCase("line")) {

chart = new LineChart();

System.out.println("初始化设置折线图!");

}

return chart;

}

}



方案的改进

       Sunny软件公司开发人员发现在创建具体Chart对象时,每更换一个Chart对象都需要修改客户端代码中静态工厂方法的参数,客户端代码将要重新编译,这对于客户端而言,违反了“开闭原则”,有没有一种方法能够在不修改客户端代码的前提下更换具体产品对象呢?答案是肯定的,下面将介绍一种常用的实现方式。

       我们可以将静态工厂方法的参数存储在XML或properties格式的配置文件中,如下config.xml所示:


  1. <?xml version="1.0"?>  
  2. <config>  
  3.     <chartType>histogram</chartType>  
  4. </config>  

       再通过一个工具类XMLUtil来读取配置文件中的字符串参数,XMLUtil类的代码如下所示:


  1. import javax.xml.parsers.*;  
  2. import org.w3c.dom.*;  
  3. import org.xml.sax.SAXException;  
  4. import java.io.*;  
  5.   
  6. public class XMLUtil {  
  7.     //该方法用于从XML配置文件中提取图表类型,并返回类型名  
  8.     public static String getChartType() {  
  9.         try {  
  10.             //创建文档对象  
  11.             DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();  
  12.             DocumentBuilder builder = dFactory.newDocumentBuilder();  
  13.             Document doc;                             
  14.             doc = builder.parse(new File("config.xml"));   
  15.           
  16.             //获取包含图表类型的文本节点  
  17.             NodeList nl = doc.getElementsByTagName("chartType");  
  18.             Node classNode = nl.item(0).getFirstChild();  
  19.             String chartType = classNode.getNodeValue().trim();  
  20.             return chartType;  
  21.         }     
  22.         catch(Exception e) {  
  23.             e.printStackTrace();  
  24.             return null;  
  25.         }  
  26.     }  
  27. }  

       在引入了配置文件和工具类XMLUtil之后,客户端代码修改如下:


  1. class Client {  
  2.     public static void main(String args[]) {  
  3.         Chart chart;  
  4.         String type = XMLUtil.getChartType(); //读取配置文件中的参数  
  5.         chart = ChartFactory.getChart(type); //创建产品对象  
  6.         chart.display();  
  7.     }  
  8. }  

       不难发现,在上述客户端代码中不包含任何与具体图表对象相关的信息,如果需要更换具体图表对象,只需修改配置文件config.xml,无须修改任何源代码,符合“开闭原则”。



多工厂模式


问题:  Sunny软件公司欲开发一个系统运行日志记录器(Logger),该记录器可以通过多种途径保存系统的运行日志,如通过文件记录或数据库记录,用户可以通过修改配置文件灵活地更换日志记录方式。在设计各类日志记录器时,Sunny公司的开发人员发现需要对日志记录器进行一些初始化工作,初始化参数的设置过程较为复杂,而且某些参数的设置有严格的先后次序,否则可能会发生记录失败。如何封装记录器的初始化过程并保证多种记录器切换的灵活性是Sunny公司开发人员面临的一个难题。


简单工厂实际上也违反了开闭原则,如果又增加了一种 实现类,需要修改工场类的逻辑。


//日志记录器接口:抽象产品

interface Logger {

public void writeLog();

}


//数据库日志记录器:具体产品

class DatabaseLogger implements Logger {

public void writeLog() {

System.out.println("数据库日志记录。");

}

}


//文件日志记录器:具体产品

class FileLogger implements Logger {

public void writeLog() {

System.out.println("文件日志记录。");

}

}


//日志记录器工厂接口:抽象工厂

interface LoggerFactory {

public Logger createLogger();

}


//数据库日志记录器工厂类:具体工厂

class DatabaseLoggerFactory implements LoggerFactory {

public Logger createLogger() {

//连接数据库,代码省略

//创建数据库日志记录器对象

Logger logger = new DatabaseLogger(); 

//初始化数据库日志记录器,代码省略

return logger;

}

}


//文件日志记录器工厂类:具体工厂

class FileLoggerFactory implements LoggerFactory {

public Logger createLogger() {

            //创建文件日志记录器对象

Logger logger = new FileLogger(); 

//创建文件,代码省略

return logger;

}

}



class Client {

public static void main(String args[]) {

LoggerFactory factory;

Logger logger;

factory = new FileLoggerFactory(); //可引入配置文件实现

logger = factory.createLogger();

logger.writeLog();

}

}


新问题:  为了让系统具有更好的灵活性和可扩展性,Sunny公司开发人员决定对日志记录器客户端代码进行重构,使得可以在不修改任何客户端代码的基础上更换或增加新的日志记录方式。



     Sunny公司开发人员创建了如下XML格式的配置文件config.xml用于存储具体日志记录器工厂类类名:

  1. <!— config.xml -->  
  2. <?xml version="1.0"?>  
  3. <config>  
  4.     <className>FileLoggerFactory</className>  
  5. </config>  

       为了读取该配置文件并通过存储在其中的类名字符串反射生成对象,Sunny公司开发人员开发了一个名为XMLUtil的工具类,其详细代码如下所示:

  1. //工具类XMLUtil.java  
  2. import javax.xml.parsers.*;  
  3. import org.w3c.dom.*;  
  4. import org.xml.sax.SAXException;  
  5. import java.io.*;  
  6.   
  7. public class XMLUtil {  
  8. //该方法用于从XML配置文件中提取具体类类名,并返回一个实例对象  
  9.     public static Object getBean() {  
  10.         try {  
  11.             //创建DOM文档对象  
  12.             DocumentBuilderFactory dFactory = DocumentBuilderFactory.newInstance();  
  13.             DocumentBuilder builder = dFactory.newDocumentBuilder();  
  14.             Document doc;                             
  15.             doc = builder.parse(new File("config.xml"));   
  16.           
  17.             //获取包含类名的文本节点  
  18.             NodeList nl = doc.getElementsByTagName("className");  
  19.             Node classNode=nl.item(0).getFirstChild();  
  20.             String cName=classNode.getNodeValue();  
  21.               
  22.             //通过类名生成实例对象并将其返回  
  23.             Class c=Class.forName(cName);  
  24.             Object obj=c.newInstance();  
  25.             return obj;  
  26.         }     
  27.         catch(Exception e) {  
  28.             e.printStackTrace();  
  29.             return null;  
  30.          }  
  31.     }  
  32. }  

       有了XMLUtil类后,可以对日志记录器的客户端代码进行修改,不再直接使用new关键字来创建具体的工厂类,而是将具体工厂类的类名存储在XML文件中,再通过XMLUtil类的静态工厂方法getBean()方法进行对象的实例化,代码修改如下:

  1. class Client {  
  2.     public static void main(String args[]) {  
  3.         LoggerFactory factory;  
  4.         Logger logger;  
  5.         factory = (LoggerFactory)XMLUtil.getBean(); //getBean()的返回类型为Object,需要进行强制类型转换  
  6.         logger = factory.createLogger();  
  7.         logger.writeLog();  
  8.     }  
  9. }  


问题2 : Sunny公司开发人员通过进一步分析,发现可以通过多种方式来初始化日志记录器,例如可以为各种日志记录器提供默认实现;还可以为数据库日志记录器提供数据库连接字符串,为文件日志记录器提供文件路径;也可以将参数封装在一个Object类型的对象中,通过Object对象将配置参数传入工厂类。此时,可以提供一组重载的工厂方法,以不同的方式对产品对象进行创建。当然,对于同一个具体工厂而言,无论使用哪个工厂方法,创建的产品类型均要相同

interface LoggerFactory {

public Logger createLogger();

public Logger createLogger(String args);

public Logger createLogger(Object obj);

}


class DatabaseLoggerFactory implements LoggerFactory {

public Logger createLogger() {

//使用默认方式连接数据库,代码省略

Logger logger = new DatabaseLogger(); 

//初始化数据库日志记录器,代码省略

return logger;

}


public Logger createLogger(String args) {

//使用参数args作为连接字符串来连接数据库,代码省略

Logger logger = new DatabaseLogger(); 

//初始化数据库日志记录器,代码省略

return logger;

}


public Logger createLogger(Object obj) {

//使用封装在参数obj中的连接字符串来连接数据库,代码省略

Logger logger = new DatabaseLogger(); 

//使用封装在参数obj中的数据来初始化数据库日志记录器,代码省略

return logger;

}

}



抽象工厂模式



问题:


Sunny软件公司欲开发一套界面皮肤库,可以对Java桌面软件进行界面美化。为了保护版权,该皮肤库源代码不打算公开,而只向用户提供已打包为jar文件的class字节码文件。用户在使用时可以通过菜单来选择皮肤,不同的皮肤将提供视觉效果不同的按钮、文本框、组合框等界面元素,其结构示意图如图1所示:







该皮肤库需要具备良好的灵活性和可扩展性,用户可以自由选择不同的皮肤,开发人员可以在不修改既有代码的基础上增加新的皮肤。




//在本实例中我们对代码进行了大量简化,实际使用时,界面组件的初始化代码较为复杂,还需要使用JDK中一些已有类,为了突出核心代码,在此只提供框架代码和演示输出。

//按钮接口:抽象产品

interface Button {

public void display();

}


//Spring按钮类:具体产品

class SpringButton implements Button {

public void display() {

System.out.println("显示浅绿色按钮。");

}

}


//Summer按钮类:具体产品

class SummerButton implements Button {

public void display() {

System.out.println("显示浅蓝色按钮。");

}

}


//文本框接口:抽象产品

interface TextField {

public void display();

}


//Spring文本框类:具体产品

class SpringTextField implements TextField {

public void display() {

System.out.println("显示绿色边框文本框。");

}

}


//Summer文本框类:具体产品

class SummerTextField implements TextField {

public void display() {

System.out.println("显示蓝色边框文本框。");

}

}


//组合框接口:抽象产品

interface ComboBox {

public void display();

}


//Spring组合框类:具体产品

class SpringComboBox implements ComboBox {

public void display() {

System.out.println("显示绿色边框组合框。");

}

}


//Summer组合框类:具体产品

class SummerComboBox implements ComboBox {

public void display() {

System.out.println("显示蓝色边框组合框。");

}

}


//界面皮肤工厂接口:抽象工厂

interface SkinFactory {

public Button createButton();

public TextField createTextField();

public ComboBox createComboBox();

}


//Spring皮肤工厂:具体工厂

class SpringSkinFactory implements SkinFactory {

public Button createButton() {

return new SpringButton();

}


public TextField createTextField() {

return new SpringTextField();

}


public ComboBox createComboBox() {

return new SpringComboBox();

}

}


//Summer皮肤工厂:具体工厂

class SummerSkinFactory implements SkinFactory {

public Button createButton() {

return new SummerButton();

}


public TextField createTextField() {

return new SummerTextField();

}


public ComboBox createComboBox() {

return new SummerComboBox();

}

}



单例模式:


第一种(懒汉,线程不安全):

 1 public class Singleton {  

 2     private static Singleton instance;  

 3     private Singleton (){}   

 4     public static Singleton getInstance() {  

 5     if (instance == null) {  

 6         instance = new Singleton();  

 7     }  

 8     return instance;  

 9     }  

10 }  

第二种 懒汉式单例: 加了线程安全锁,性能下降。


class LazySingleton { 

    private static LazySingleton instance = null; 


    private LazySingleton() { } 


    synchronized public static LazySingleton getInstance() { 

        if (instance == null) {

            instance = new LazySingleton(); 

        }

        return instance; 

    }

}


 该懒汉式单例类在getInstance()方法前面增加了关键字synchronized进行线程锁,以处理多个线程同时访问的问题。但是,上述代码虽然解决了线程安全问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低。如何既解决线程安全问题又不影响系统性能呢?我们继续对懒汉式单例进行改进。事实上,我们无须对整个getInstance()方法进行锁定,只需对其中的代码“instance = new LazySingleton();”进行锁定即可。因此getInstance()方法可以进行如下改进:

第三种  (双重校验锁)


class LazySingleton { 

    private volatile static LazySingleton instance = null; 


    private LazySingleton() { } 


    public static LazySingleton getInstance() { 

        //第一重判断

        if (instance == null) {

            //锁定代码块

            synchronized (LazySingleton.class) {

                //第二重判断

                if (instance == null) {

                    instance = new LazySingleton(); //创建单例实例

                }

            }

        }

        return instance; 

    }

}


以下方式基于classloder机制避免了多线程的同步问题,不过,instance在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用getInstance方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到lazy loading的效果。


第四种:饿汉式单例:不能实现延迟加载,不管将来用不用始终占据内存


class EagerSingleton { 

    private static final EagerSingleton instance = new EagerSingleton(); 

    private EagerSingleton() { } 


    public static EagerSingleton getInstance() {

        return instance; 

    }   

}


第五种(饿汉,变种):表面上看起来差别挺大,其实更第四种方式差不多,都是在类初始化即实例化instance。


 1 public class Singleton {  

 2     private Singleton instance = null;  

 3     static {  

 4     instance = new Singleton();  

 5     }  

 6     private Singleton (){}

 7     public static Singleton getInstance() {  

 8     return this.instance;  

 9     }  

10 }  

11 



第六种(静态内部类):


class Singleton {

private Singleton() {

}

private static class HolderClass {

            private final static Singleton instance = new Singleton();

}

public static Singleton getInstance() {

   return HolderClass.instance;

}

public static void main(String args[]) {

   Singleton s1, s2; 

            s1 = Singleton.getInstance();

   s2 = Singleton.getInstance();

   System.out.println(s1==s2);

}

}



第七种(枚举):



1 public enum Singleton {  

2     INSTANCE;  

3     public void whateverMethod() {  

4     }  

5 }  


原型模式


问题:

Sunny软件公司一直使用自行开发的一套OA (Office Automatic,办公自动化)系统进行日常工作办理,但在使用过程中,越来越多的人对工作周报的创建和编写模块产生了抱怨。追其原因,Sunny软件公司的OA管理员发现,由于某些岗位每周工作存在重复性,工作周报内容都大同小异,如图7-1工作周报示意图。这些周报只有一些小地方存在差异,但是现行系统每周默认创建的周报都是空白报表,用户只能通过重新输入或不断复制粘贴来填写重复的周报内容,极大降低了工作效率,浪费宝贵的时间。如何快速创建相同或者相似的工作周报,成为Sunny公司OA开发人员面临的一个新问题。





答案:


class WeeklyLog implements Cloneable

{

       private  String name;

       private  String date;

       private  String content;

       public  void setName(String name) {

              this.name  = name;

       }

       public  void setDate(String date) {

              this.date  = date;

       }

       public  void setContent(String content) {

              this.content  = content;

       }

       public  String getName() {

              return  (this.name);

       }

       public  String getDate() {

              return  (this.date);

       }

       public  String getContent() {

              return  (this.content);

       }

     //克隆方法clone(),此处使用Java语言提供的克隆机制

       public WeeklyLog clone()

       {

              Object obj = null;

              try

              {

                     obj = super.clone();

                     return (WeeklyLog)obj;     

              }

              catch(CloneNotSupportedException e)

              {

                     System.out.println("不支持复制!");

                     return null;

              }

       }

}

编写如下客户端测试代码:

class Client

{

       public  static void main(String args[])

       {

              WeeklyLog log_previous = new WeeklyLog();  //创建原型对象

              log_previous.setName("张无忌");

              log_previous.setDate("第12周");

              log_previous.setContent("这周工作很忙,每天加班!");

             

              System.out.println("****周报****");

              System.out.println("周次:" +  log_previous.getDate());

              System.out.println("姓名:" +  log_previous.getName());

              System.out.println("内容:" +  log_previous.getContent());

              System.out.println("--------------------------------");

             

              WeeklyLog  log_new;

              log_new  = log_previous.clone(); //调用克隆方法创建克隆对象

              log_new.setDate("第13周");

              System.out.println("****周报****");

              System.out.println("周次:" + log_new.getDate());

              System.out.println("姓名:" + log_new.getName());

              System.out.println("内容:" + log_new.getContent());

       }

}



通过引入原型模式,Sunny软件公司OA系统支持工作周报的快速克隆,极大提高了工作周报的编写效率,受到员工的一致好评。但有员工又发现一个问题,有些工作周报带有附件,例如经理助理“小龙女”的周报通常附有本周项目进展报告汇总表、本周客户反馈信息汇总表等,如果使用上述原型模式来复制周报,周报虽然可以复制,但是周报的附件并不能复制,这是由于什么原因导致的呢?如何才能实现周报和附件的同时复制呢?



import  java.io.*;


//附件类

class  Attachment implements Serializable

{

       private  String name; //附件名

       public  void setName(String name)

       {

              this.name  = name;

       }

       public  String getName()

       {

              return  this.name;

       }

     public void download()

     {

            System.out.println("下载附件,文件名为" + name);

     }

}

      工作周报类WeeklyLog不再使用Java自带的克隆机制,而是通过序列化来从头实现对象的深克隆,我们需要重新编写clone()方法,修改后的代码如下:

import  java.io.*;


//工作周报类

class  WeeklyLog implements Serializable

{

       private  Attachment attachment;

       private  String name;

       private  String date;

       private  String content;

       public  void setAttachment(Attachment attachment) {

              this.attachment  = attachment;

       }

       public  void setName(String name) {

              this.name  = name;

       }

       public  void setDate(String date) {

              this.date  = date;

       }

       public  void setContent(String content) {

              this.content  = content;

       }

       public  Attachment getAttachment(){

              return  (this.attachment);

       }

       public  String getName() {

              return  (this.name);

       }

       public  String getDate() {

              return  (this.date);

       }

       public  String getContent() {

              return  (this.content);

       }

   //使用序列化技术实现深克隆

       public WeeklyLog deepClone() throws  IOException, ClassNotFoundException, OptionalDataException

       {

              //将对象写入流中

              ByteArrayOutputStream bao=new  ByteArrayOutputStream();

              ObjectOutputStream oos=new  ObjectOutputStream(bao);

              oos.writeObject(this);

             

              //将对象从流中取出

              ByteArrayInputStream bis=new  ByteArrayInputStream(bao.toByteArray());

              ObjectInputStream ois=new  ObjectInputStream(bis);

              return  (WeeklyLog)ois.readObject();

       }

}

      客户端代码如下所示:

class Client

{

       public  static void main(String args[])

       {

              WeeklyLog  log_previous, log_new = null;

              log_previous  = new WeeklyLog(); //创建原型对象

              Attachment  attachment = new Attachment(); //创建附件对象

              log_previous.setAttachment(attachment);  //将附件添加到周报中

              try

              {

                     log_new =  log_previous.deepClone(); //调用深克隆方法创建克隆对象                  

              }

              catch(Exception e)

              {

                     System.err.println("克隆失败!");

              }

              //比较周报

              System.out.println("周报是否相同? " + (log_previous ==  log_new));

              //比较附件

              System.out.println("附件是否相同? " +  (log_previous.getAttachment() == log_new.getAttachment()));

       }

}




建造者模式



问题: Sunny软件公司游戏开发小组决定开发一款名为《Sunny群侠传》的网络游戏,该游戏采用主流的RPG(Role Playing Game,角色扮演游戏)模式,玩家可以在游戏中扮演虚拟世界中的一个特定角色,角色根据不同的游戏情节和统计数据(如力量、魔法、技能等)具有不同的能力,角色也会随着不断升级而拥有更加强大的能力。

作为RPG游戏的一个重要组成部分,需要对游戏角色进行设计,而且随着该游戏的升级将不断增加新的角色。不同类型的游戏角色,其性别、脸型、服装、发型等外部特性都有所差异,例如“天使”拥有美丽的面容和披肩的长发,并身穿一袭白裙;而“恶魔”极其丑陋,留着光头并穿一件刺眼的黑衣。

Sunny公司决定开发一个小工具来创建游戏角色,可以创建不同类型的角色并可以灵活增加新的角色。


class Actor

{

       private  String type; //角色类型

       private  String sex; //性别

       private  String face; //脸型

       private  String costume; //服装

       private  String hairstyle; //发型

      

       public  void setType(String type) {

              this.type  = type;

       }

       public  void setSex(String sex) {

              this.sex  = sex;

       }

       public  void setFace(String face) {

              this.face  = face;

       }

       public  void setCostume(String costume) {

              this.costume  = costume;

       }

       public  void setHairstyle(String hairstyle) {

              this.hairstyle  = hairstyle;

       }

       public  String getType() {

              return  (this.type);

       }

       public  String getSex() {

              return  (this.sex);

       }

       public  String getFace() {

              return  (this.face);

       }

       public  String getCostume() {

              return  (this.costume);

       }

       public  String getHairstyle() {

              return  (this.hairstyle);

       }

}

 

//角色建造器:抽象建造者

abstract class ActorBuilder

{

       protected  Actor actor = new Actor();

      

       public  abstract void buildType();

       public  abstract void buildSex();

       public  abstract void buildFace();

       public  abstract void buildCostume();

       public  abstract void buildHairstyle();

 

    //工厂方法,返回一个完整的游戏角色对象

       public Actor createActor()

       {

              return actor;

       }

}

 

//英雄角色建造器:具体建造者

class HeroBuilder extends ActorBuilder

{

       public  void buildType()

       {

              actor.setType("英雄");

       }

       public  void buildSex()

       {

              actor.setSex("男");

       }

       public  void buildFace()

       {

              actor.setFace("英俊");

       }

       public  void buildCostume()

       {

              actor.setCostume("盔甲");

       }

       public  void buildHairstyle()

       {

              actor.setHairstyle("飘逸");

       }    

}

 

//天使角色建造器:具体建造者

class AngelBuilder extends ActorBuilder

{

       public  void buildType()

       {

              actor.setType("天使");

       }

       public  void buildSex()

       {

              actor.setSex("女");

       }

       public  void buildFace()

       {

              actor.setFace("漂亮");

       }

       public  void buildCostume()

       {

              actor.setCostume("白裙");

       }

       public  void buildHairstyle()

       {

              actor.setHairstyle("披肩长发");

       }    

}

 

//恶魔角色建造器:具体建造者

class DevilBuilder extends ActorBuilder

{

       public  void buildType()

       {

              actor.setType("恶魔");

       }

       public  void buildSex()

       {

              actor.setSex("妖");

       }

       public  void buildFace()

       {

              actor.setFace("丑陋");

       }

       public  void buildCostume()

       {

              actor.setCostume("黑衣");

       }

       public  void buildHairstyle()

       {

              actor.setHairstyle("光头");

       }    

}

 

      指挥者类ActorController定义了construct()方法,该方法拥有一个抽象建造者ActorBuilder类型的参数,在该方法内部实现了游戏角色对象的逐步构建,代码如下所示:

//游戏角色创建控制器:指挥者

class ActorController

{

    //逐步构建复杂产品对象

       public Actor construct(ActorBuilder ab)

       {

              Actor actor;

              ab.buildType();

              ab.buildSex();

              ab.buildFace();

              ab.buildCostume();

              ab.buildHairstyle();

              actor=ab.createActor();

              return actor;

       }

}



















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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值