大家好,我是程序羊。 平时我们写代码呢,多数情况都是流水线式写代码,基本就可以实现业务逻辑了。那如何在写代码中找到乐趣呢,我觉得一个有效的方式就是:尝试使用设计模式来优化自己的业务代码,以提高质量! 所以今天我们就结合具体业务场景,来梳理一下日常工作中,高频使用的几种设计模式,相信对大家平时的工作会很有帮助。 文章很长,大家也可以收藏备用。工作中常用到哪些设计模式
1.策略模式
1.1 业务场景
假设有这样的业务场景,大数据系统把文件推送过来,根据不同类型采取不同的解析方式。多数的小伙伴就会写出以下的代码:
if(type=="A"){
//按照A格式解析
}elseif(type=="B"){
//按B格式解析
}else{
//按照默认格式解析
}
这个代码可能会存在哪些问题呢?
- 如果分支变多,这里的代码就会变得臃肿,难以维护,可读性低。
- 如果你需要接入一种新的解析类型,那只能在原有代码上修改。
说得专业一点的话,就是以上代码,违背了面向对象编程的开闭原则以及单一原则。
- 开闭原则(对于扩展是开放的,但是对于修改是封闭的):增加或者删除某个逻辑,都需要修改到原来代码
- 单一原则(规定一个类应该只有一个发生变化的原因):修改任何类型的分支逻辑代码,都需要改动当前类的代码。
如果你的代码就是酱紫:有多个if...else
等条件分支,并且每个条件分支,可以封装起来替换的,我们就可以使用策略模式来优化。
1.2 策略模式定义
策略模式定义了算法族,分别封装起来,让它们之间可以相互替换,此模式让算法的变化独立于使用算法的的客户。这个策略模式的定义是不是有点抽象呢?那我们来看点通俗易懂的比喻:
假设你跟不同性格类型的小姐姐约会,要用不同的策略,有的请电影比较好,有的则去吃小吃效果不错,有的去逛街买买买最合适。当然,目的都是为了得到小姐姐的芳心,请看电影、吃小吃、逛街就是不同的策略。
策略模式针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换。
1.3 策略模式使用
策略模式怎么使用呢?酱紫实现的:
- 一个接口或者抽象类,里面两个方法(一个方法匹配类型,一个可替换的逻辑实现方法)
- 不同策略的差异化实现(就是说,不同策略的实现类)
- 使用策略模式
1.3.1 一个接口,两个方法
publicinterfaceIFileStrategy{
//属于哪种文件解析类型
FileTypeResolveEnumgainFileType();
//封装的公用算法(具体的解析方法)
voidresolve(Objectobjectparam);
}
1.3.2 不同策略的差异化实现
A 类型策略具体实现
@Component
publicclassAFileResolveimplementsIFileStrategy{
@Override
publicFileTypeResolveEnumgainFileType(){
returnFileTypeResolveEnum.File_A_RESOLVE;
}
@Override
publicvoidresolve(Objectobjectparam){
logger.info("A 类型解析文件,参数:{}",objectparam);
//A类型解析具体逻辑
}
}
B 类型策略具体实现
@Component
publicclassBFileResolveimplementsIFileStrategy{
@Override
publicFileTypeResolveEnumgainFileType(){
returnFileTypeResolveEnum.File_B_RESOLVE;
}
@Override
publicvoidresolve(Objectobjectparam){
logger.info("B 类型解析文件,参数:{}",objectparam);
//B类型解析具体逻辑
}
}
默认类型策略具体实现
@Component
publicclassDefaultFileResolveimplementsIFileStrategy{
@Override
publicFileTypeResolveEnumgainFileType(){
returnFileTypeResolveEnum.File_DEFAULT_RESOLVE;
}
@Override
publicvoidresolve(Objectobjectparam){
logger.info("默认类型解析文件,参数:{}",objectparam);
//默认类型解析具体逻辑
}
}
1.3.3 使用策略模式
如何使用呢?我们借助spring
的生命周期,使用ApplicationContextAware
接口,把对用的策略,初始化到map
里面。然后对外提供resolveFile
方法即可。
@Component
publicclassStrategyUseServiceimplementsApplicationContextAware{
privateMapiFileStrategyMap=newConcurrentHashMap<>();
publicvoidresolveFile(FileTypeResolveEnumfileTypeResolveEnum,ObjectobjectParam){
IFileStrategyiFileStrategy=iFileStrategyMap.get(fileTypeResolveEnum);
if(iFileStrategy!=null){
iFileStrategy.resolve(objectParam);
}
}
//把不同策略放到map
@Override
publicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{
MaptmepMap=applicationContext.getBeansOfType(IFileStrategy.class);
tmepMap.values().forEach(strategyService->iFileStrategyMap.put(strategyService.gainFileType(),strategyService));
}
}
2. 责任链模式
2.1 业务场景
我们来看一个常见的业务场景,下订单。下订单接口,基本的逻辑,一般有参数非空校验、安全校验、黑名单校验、规则拦截等等。很多伙伴会使用异常来实现:
publicclassOrder{
publicvoidcheckNullParam(Objectparam){
//参数非空校验
thrownewRuntimeException();
}
publicvoidcheckSecurity(){
//安全校验
thrownewRuntimeException();
}
publicvoidcheckBackList(){
//黑名单校验
thrownewRuntimeException();
}
publicvoidcheckRule(){
//规则拦截
thrownewRuntimeException();
}
publicstaticvoidmain(String[]args){
Orderorder=newOrder();
try{
order.checkNullParam();
order.checkSecurity();
order.checkBackList();
order2.checkRule();
System.out.println("ordersuccess");
}catch(RuntimeExceptione){
System.out.println("orderfail");
}
}
}
这段代码使用了异常来做逻辑条件判断,如果后续逻辑越来越复杂的话,会出现一些问题:如异常只能返回异常信息,不能返回更多的字段,这时候需要自定义异常类。
并且,阿里开发手册规定:禁止用异常做逻辑判断。
【强制】 异常不要用来做流程控制,条件控制。说明:异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多。
如何优化这段代码呢?可以考虑责任链模式
2.2 责任链模式定义
当你想要让一个以上的对象有机会能够处理某个请求的时候,就使用责任链模式。
责任链模式为请求创建了一个接收者对象的链。执行链上有多个对象节点,每个对象节点都有机会(条件匹配)处理请求事务,如果某个对象节点处理完了,就可以根据实际业务需求传递给下一个节点继续处理或者返回处理完毕。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。
责任链模式实际上是一种处理请求的模式,它让多个处理器(对象节点)都有机会处理该请求,直到其中某个处理成功为止。责任链模式把多个处理器串成链,然后让请求在链上传递:
责任链模式打个比喻:
假设你晚上去上选修课,为了可以走点走,坐到了最后一排。来到教室,发现前面坐了好几个漂亮的小姐姐,于是你找张纸条,写上:“你好, 可以做我的女朋友吗?如果不愿意请向前传”。纸条就一个接一个的传上去了,后来传到第一排的那个妹子手上,她把纸条交给老师,听说老师40多岁未婚...
2.3 责任链模式使用
责任链模式怎么使用呢?
- 一个接口或者抽象类
- 每个对象差异化处理
- 对象链(数组)初始化(连起来)
2.3.1 一个接口或者抽象类
这个接口或者抽象类,需要:
- 有一个指向责任下一个对象的属性
- 一个设置下一个对象的set方法
- 给子类对象差异化实现的方法(如以下代码的doFilter方法)
publicabstractclassAbstractHandler{
//责任链中的下一个对象
privateAbstractHandlernextHandler;
/**
*责任链的下一个对象
*/
publicvoidsetNextHandler(AbstractHandlernextHandler){
this.nextHandler=nextHandler;
}
/**
*具体参数拦截逻辑,给子类去实现
*/
publicvoidfilter(Requestrequest,Responseresponse){
doFilter(request,response);
if(getNextHandler()!=null){
getNextHandler().filter(request,response);
}
}
publicAbstractHandlergetNextHandler(){
returnnextHandler;
}
abstractvoiddoFilter(RequestfilterRequest,Responseresponse);
}
2.3.2 每个对象差异化处理
责任链上,每个对象的差异化处理,如本小节的业务场景,就有参数校验对象、安全校验对象、黑名单校验对象、规则拦截对象
/**
*参数校验对象
**/
@Component
@Order(1)//顺序排第1,最先校验
publicclassCheckParamFilterObjectextendsAbstractHandler{
@Override
publicvoiddoFilter(Requestrequest,Responseresponse){
System.out.println("非空参数检查");
}
}
/**
*安全校验对象
*/
@Component
@Order(2)//校验顺序排第2
publicclassCheckSecurityFilterObjectextendsAbstractHandler{
@Override
publicvoiddoFilter(Requestrequest,Responseresponse){
//invokeSecuritycheck
System.out.println("安全调用校验");
}
}
/**
*黑名单校验对象
*/
@Component
@Order(3)//校验顺序排第3
publicclassCheckBlackFilterObjectextendsAbstractHandler{
@Override
publicvoiddoFilter(Requestrequest,Responseresponse){
//invokeblacklistcheck
System.out.println("校验黑名单");
}
}
/**
*规则拦截对象
*/
@Component
@Order(4)//校验顺序排第4
publicclassCheckRuleFilterObjectextendsAbstractHandler{
@Override
publicvoiddoFilter(Requestrequest,Responseresponse){
//checkrule
System.out.println("checkrule");
}
}
2.3.3 对象链连起来(初始化)&& 使用
@Component("ChainPatternDemo")
publicclassChainPatternDemo{
//自动注入各个责任链的对象
@Autowired
privateListabstractHandleList;
privateAbstractHandlerabstractHandler;
//spring注入后自动执行,责任链的对象连接起来
@PostConstruct
publicvoidinitializeChainFilter(){
for(inti=0;iif(i==0){
abstractHandler=abstractHandleList.get(0);
}else{
AbstractHandlercurrentHander=abstractHandleList.get(i-1);
AbstractHandlernextHander=abstractHandleList.get(i);
currentHander.setNextHandler(nextHander);
}
}
}
//直接调用这个方法使用
publicResponseexec(Requestrequest,Responseresponse){
abstractHandler.filter(request,response);
returnresponse;
}
publicAbstractHandlergetAbstractHandler(){
returnabstractHandler;
}
publicvoidsetAbstractHandler(AbstractHandlerabstractHandler){
this.abstractHandler=abstractHandler;
}
}
运行结果如下:
非空参数检查
安全调用校验
校验黑名单
checkrule
3. 模板方法模式
3.1 业务场景
假设我们有这么一个业务场景:内部系统不同商户,调用我们系统接口,去跟外部第三方系统交互(http方式)。走类似这么一个流程,如下:
一个请求都会经历这几个流程:
- 查询商户信息
- 对请求报文加签
- 发送http请求出去
- 对返回的报文验签
这里,有的商户可能是走代理出去的,有的是走直连。假设当前有A,B商户接入,不少伙伴可能这么实现,伪代码如下:
//商户A处理句柄
CompanyAHandlerimplementsRequestHandler{
Resphander(req){
//查询商户信息
queryMerchantInfo();
//加签
signature();
//http请求(A商户假设走的是代理)
httpRequestbyProxy()
//验签
verify();
}
}
//商户B处理句柄
CompanyBHandlerimplementsRequestHandler{
Resphander(Rreq){
//查询商户信息
queryMerchantInfo();
//加签
signature();
//http请求(B商户不走代理,直连)
httpRequestbyDirect();
//验签
verify();
}
}
假设新加一个C商户接入,你需要再实现一套这样的代码。显然,这样代码就重复了,一些通用的方法,却在每一个子类都重新写了这一方法。
如何优化呢?可以使用模板方法模式。
3.2 模板方法模式定义
定义一个操作中的算法的骨架流程,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。它的核心思想就是:定义一个操作的一系列步骤,对于某些暂时确定不下来的步骤,就留给子类去实现,这样不同的子类就可以定义出不同的步骤。
打个通俗的比喻:
模式举例:追女朋友要先“牵手”,再“拥抱”,再“接吻”, 再“拍拍..额..手”。至于具体你用左手还是右手牵,无所谓,但是整个过程,定了一个流程模板,按照模板来就行。
3.3 模板方法使用
- 一个抽象类,定义骨架流程(抽象方法放一起)
- 确定的共同方法步骤,放到抽象类(去除抽象方法标记)
- 不确定的步骤,给子类去差异化实现
我们继续那以上的举例的业务流程例子,来一起用 模板方法优化一下哈:
3.3.1 一个抽象类,定义骨架流程
因为一个个请求经过的流程为一下步骤:
- 查询商户信息
- 对请求报文加签
- 发送http请求出去
- 对返回的报文验签
所以我们就可以定义一个抽象类,包含请求流程的几个方法,方法首先都定义为抽象方法哈:
/**
*抽象类定义骨架流程(查询商户信息,加签,http请求,验签)
*/
abstractclassAbstractMerchantService{
//查询商户信息
abstractqueryMerchantInfo();
//加签
abstractsignature();
//http请求
abstracthttpRequest();
//验签
abstractverifySinature();
}
3.3.2 确定的共同方法步骤,放到抽象类
abstractclassAbstractMerchantService{
//模板方法流程
ResphandlerTempPlate(req){
//查询商户信息
queryMerchantInfo();
//加签
signature();
//http请求
httpRequest();
//验签
verifySinature();
}
//Http是否走代理(提供给子类实现)
abstractbooleanisRequestByProxy();
}
3.3.3 不确定的步骤,给子类去差异化实现
因为是否走代理流程是不确定的,所以给子类去实现。
商户A的请求实现:
CompanyAServiceImplextendsAbstractMerchantService{
Resphander(req){
returnhandlerTempPlate(req);
}
//走http代理的
booleanisRequestByProxy(){
returntrue;
}
商户B的请求实现:
CompanyBServiceImplextendsAbstractMerchantService{
Resphander(req){
returnhandlerTempPlate(req);
}
//公司B是不走代理的
booleanisRequestByProxy(){
returnfalse;
}
4. 观察者模式
4.1 业务场景
登陆注册应该是最常见的业务场景了。就拿注册来说事,我们经常会遇到类似的场景,就是用户注册成功后,我们给用户发一条消息,又或者发个邮件等等,因此经常有如下的代码:
voidregister(Useruser){
insertRegisterUser(user);
sendIMMessage();
sendEmail();
}
这块代码会有什么问题呢?如果产品又加需求:现在注册成功的用户,再给用户发一条短信通知。于是你又得改register方法的代码了。。。这是不是违反了开闭原则啦。
voidregister(Useruser){
insertRegisterUser(user);
sendIMMessage();
sendMobileMessage();
sendEmail();
}
并且,如果调发短信的接口失败了,是不是又影响到用户注册了?!这时候,是不是得加个异步方法给通知消息才好。。。
实际上,我们可以使用观察者模式优化。
4.2 观察者模式定义
观察者模式定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被完成业务的更新。
观察者模式属于行为模式,一个对象(被观察者)的状态发生改变,所有的依赖对象(观察者对象)都将得到通知,进行广播通知。它的主要成员就是观察者和被观察者。
- 被观察者(Observerable):目标对象,状态发生变化时,将通知所有的观察者。
- 观察者(observer):接受被观察者的状态变化通知,执行预先定义的业务。
使用场景:完成某件事情后,异步通知场景。如,登陆成功,发个IM消息等等。
4.3 观察者模式使用
观察者模式实现的话,还是比较简单的。
- 一个被观察者的类Observerable ;
- 多个观察者Observer ;
- 观察者的差异化实现
- 经典观察者模式封装:EventBus实战
4.3.1 一个被观察者的类Observerable 和 多个观察者Observer
publicclassObserverable{
privateListobservers
=newArrayList();
privateintstate;
publicintgetState(){
returnstate;
}
publicvoidsetState(intstate){
notifyAllObservers();
}
//添加观察者
publicvoidaddServer(Observerobserver){
observers.add(observer);
}
//移除观察者
publicvoidremoveServer(Observerobserver){
observers.remove(observer);
}
//通知
publicvoidnotifyAllObservers(intstate){
if(state!=1){
System.out.println(“不是通知的状态”);
return;
}
for(Observerobserver:observers){
observer.doEvent();
}
}
}
4.3.2 观察者的差异化实现
//观察者
interfaceObserver{
voiddoEvent();
}
//Im消息
IMMessageObserverimplementsObserver{
voiddoEvent(){
System.out.println("发送IM消息");
}
}
//手机短信
MobileNoObserverimplementsObserver{
voiddoEvent(){
System.out.println("发送短信消息");
}
}
//EmailNo
EmailObserverimplementsObserver{
voiddoEvent(){
System.out.println("发送email消息");
}
}
4.3.3 EventBus实战
自己搞一套观察者模式的代码,还是有点小麻烦。实际上,Guava EventBus
就封装好了,它 提供一套基于注解的事件总线,api可以灵活的使用,爽歪歪。
我们来看下EventBus
的实战代码哈,首先可以声明一个EventBusCenter类,它类似于以上被观察者那种角色Observerable
。
publicclassEventBusCenter{
privatestaticEventBuseventBus=newEventBus();
privateEventBusCenter(){
}
publicstaticEventBusgetInstance(){
returneventBus;
}
//添加观察者
publicstaticvoidregister(Objectobj){
eventBus.register(obj);
}
//移除观察者
publicstaticvoidunregister(Objectobj){
eventBus.unregister(obj);
}
//把消息推给观察者
publicstaticvoidpost(Objectobj){
eventBus.post(obj);
}
}
然后再声明观察者EventListener
publicclassEventListener{
@Subscribe//加了订阅,这里标记这个方法是事件处理方法
publicvoidhandle(NotifyEventnotifyEvent){
System.out.println("发送IM消息"+notifyEvent.getImNo());
System.out.println("发送短信消息"+notifyEvent.getMobileNo());
System.out.println("发送Email消息"+notifyEvent.getEmailNo());
}
}
//通知事件类
publicclassNotifyEvent{
privateStringmobileNo;
privateStringemailNo;
privateStringimNo;
publicNotifyEvent(StringmobileNo,StringemailNo,StringimNo){
this.mobileNo=mobileNo;
this.emailNo=emailNo;
this.imNo=imNo;
}
}
使用demo测试:
publicclassEventBusDemoTest{
publicstaticvoidmain(String[]args){
EventListenereventListener=newEventListener();
EventBusCenter.register(eventListener);
EventBusCenter.post(newNotifyEvent("13372817283","123@qq.com","666"));
}
}
运行结果:
发送IM消息666
发送短信消息13372817283
发送Email消息123@qq.com
5. 工厂模式
5.1 业务场景
工厂模式一般配合策略模式一起使用。用来去优化大量的if...else...
或switch...case...
条件语句。
我们就取第一小节中策略模式那个例子吧。根据不同的文件解析类型,创建不同的解析对象
IFileStrategygetFileStrategy(FileTypeResolveEnumfileType){
IFileStrategyfileStrategy;
if(fileType=FileTypeResolveEnum.File_A_RESOLVE){
fileStrategy=newAFileResolve();
}elseif(fileType=FileTypeResolveEnum.File_A_RESOLV){
fileStrategy=newBFileResolve();
}else{
fileStrategy=newDefaultFileResolve();
}
returnfileStrategy;
}
其实这就是工厂模式,定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。
策略模式的例子,没有使用上一段代码,而是借助spring的特性,搞了一个工厂模式,哈哈,小伙伴们可以回去那个例子细品一下,我把代码再搬下来,小伙伴们再品一下吧:
@Component
publicclassStrategyUseServiceimplementsApplicationContextAware{
privateMapiFileStrategyMap=newConcurrentHashMap<>();
//把所有的文件类型解析的对象,放到map,需要使用时,信手拈来即可。这就是工厂模式的一种体现啦
@Override
publicvoidsetApplicationContext(ApplicationContextapplicationContext)throwsBeansException{
MaptmepMap=applicationContext.getBeansOfType(IFileStrategy.class);
tmepMap.values().forEach(strategyService->iFileStrategyMap.put(strategyService.gainFileType(),strategyService));
}
}
5.2 使用工厂模式
定义工厂模式也是比较简单的:
- 一个工厂接口,提供一个创建不同对象的方法。
- 其子类实现工厂接口,构造不同对象
- 使用工厂模式
5.3.1 一个工厂接口
interfaceIFileResolveFactory{
voidresolve();
}
5.3.2 不同子类实现工厂接口
classAFileResolveimplementsIFileResolveFactory{
voidresolve(){
System.out.println("文件A类型解析");
}
}
classBFileResolveimplementsIFileResolveFactory{
voidresolve(){
System.out.println("文件B类型解析");
}
}
classDefaultFileResolveimplementsIFileResolveFactory{
voidresolve(){
System.out.println("默认文件类型解析");
}
}
5.3.3 使用工厂模式
//构造不同的工厂对象
IFileResolveFactoryfileResolveFactory;
if(fileType=“A”){
fileResolveFactory=newAFileResolve();
}elseif(fileType=“B”){
fileResolveFactory=newBFileResolve();
}else{
fileResolveFactory=newDefaultFileResolve();
}
fileResolveFactory.resolve();
一般情况下,对于工厂模式,你不会看到以上的代码。工厂模式会跟配合其他设计模式如策略模式一起出现的。
6. 单例模式
6.1 业务场景
单例模式,保证一个类仅有一个实例,并提供一个访问它的全局访问点。I/O与数据库的连接,一般就用单例模式实现de的。Windows里面的Task Manager(任务管理器)也是很典型的单例模式。
来看一个单例模式的例子
publicclassLanHanSingleton{
privatestaticLanHanSingletoninstance;
privateLanHanSingleton(){
}
publicstaticLanHanSingletongetInstance(){
if(instance==null){
instance=newLanHanSingleton();
}
returninstance;
}
}
以上的例子,就是懒汉式的单例实现。实例在需要用到的时候,才去创建,就比较懒。如果有则返回,没有则新建,需要加下synchronized
关键字,要不然可能存在线性安全问题。
6.2 单例模式的经典写法
其实单例模式还有有好几种实现方式,如饿汉模式,双重校验锁,静态内部类,枚举等实现方式。
6.2.1 饿汉模式
publicclassEHanSingleton{
privatestaticEHanSingletoninstance=newEHanSingleton();
privateEHanSingleton(){
}
publicstaticEHanSingletongetInstance(){
returninstance;
}
}
饿汉模式,它比较饥饿、比较勤奋,实例在初始化的时候就已经建好了,不管你后面有没有用到,都先新建好实例再说。这个就没有线程安全的问题,但是呢,浪费内存空间呀。
6.2.2 双重校验锁
publicclassDoubleCheckSingleton{
privatevolatile staticDoubleCheckSingletoninstance;
privateDoubleCheckSingleton(){}
publicstaticDoubleCheckSingletongetInstance(){
if(instance==null){
synchronized(DoubleCheckSingleton.class){
if(instance==null){
instance=newDoubleCheckSingleton();
}
}
}
returninstance;
}
}
双重校验锁实现的单例模式,综合了懒汉式和饿汉式两者的优缺点。以上代码例子中,在synchronized关键字内外都加了一层 if
条件判断,这样既保证了线程安全,又比直接上锁提高了执行效率,还节省了内存空间。
6.2.3 静态内部类
publicclassInnerClassSingleton{
privatestaticclassInnerClassSingletonHolder{
privatestaticfinalInnerClassSingletonINSTANCE=newInnerClassSingleton();
}
privateInnerClassSingleton(){}
publicstaticfinalInnerClassSingletongetInstance(){
returnInnerClassSingletonHolder.INSTANCE;
}
}
静态内部类的实现方式,效果有点类似双重校验锁。但这种方式只适用于静态域场景,双重校验锁方式可在实例域需要延迟初始化时使用。
6.2.4 枚举
publicenumSingletonEnum{
INSTANCE;
publicSingletonEnumgetInstance(){
returnINSTANCE;
}
}
枚举实现的单例,代码简洁清晰。并且它还自动支持序列化机制,绝对防止多次实例化。
好了,以上就是今天的内容分享,感谢大家的收看,我们下期见。
审核编辑 :李倩
-
算法
+关注
关注
23文章
4606浏览量
92807 -
逻辑
+关注
关注
2文章
833浏览量
29462 -
代码
+关注
关注
30文章
4776浏览量
68507
原文标题:看了我最常用的6种代码技巧,同事也开始悄悄模仿了...
文章出处:【微信号:CodeSheep,微信公众号:CodeSheep】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论