不会利用它来减少ifelse并解耦?来看看这篇文章
引言
状态模式大家可能初听会很陌生,这种模式有什么用?我就是个CRUD BOY,面对不同的状态,我一个状态一个状态的判断,if else、if else...... 不断的来写不同的逻辑它不香吗?
香! 但是作为一个杰出的后浪代表,仅仅如此怎能满足我对知识的欲望!
我们知道面向对象的设计模式有七大基本原则:开闭原则(Open Closed Principle,OCP)单一职责原则(Single Responsibility Principle, SRP)里氏代换原则(Liskov Substitution Principle,LSP)依赖倒转原则(Dependency Inversion Principle,DIP)接口隔离原则(Interface Segregation Principle,ISP)合成/聚合复用原则(Composite/Aggregate Reuse Principle,CARP)最少知识原则(Least Knowledge Principle,LKP)或者迪米特法则(Law of Demeter,LOD)
简单理解就是:开闭原则是总纲,它指导我们要对扩展开放,对修改关闭;单一职责原则指导我们实现类要职责单一;里氏替换原则指导我们不要破坏继承体系;依赖倒置原则指导我们要面向接口编程;接口隔离原则指导我们在设计接口的时候要精简单一;迪米特法则指导我们要降低耦合。
设计模式就是通过这七个原则,来指导我们如何做一个好的设计。但是设计模式不是一套"奇技淫巧",它是一套方法论,一种高内聚、低耦合的设计思想。我们可以在此基础上自由的发挥,甚至设计出自己的一套设计模式。
当然,学习设计模式或者是在工程中实践设计模式,必须深入到某一个特定的业务场景中去,再结合对业务场景的理解和领域模型的建立,才能体会到设计模式思想的精髓。如果脱离具体的业务逻辑去学习或者使用设计模式,那是极其空洞的。
接下来我们将通过业务的实践,来探讨如何用状态设计模式来减少if else,实现可重用、易维护的代码。状态模式
不知道大家在业务中会不会经常遇到这种情况:
产品:开发哥哥来下,你看我这边想加个中间流程,这个流程是要怎样怎样处理.......,还想区分加了这些操作后的用户,其他不符合这个条件的用户不要影响,能不能实现啊!
我:能啊,加个状态就行啊!于是将原流程加了个状态,当用户处于这个状态时会如何如何......,于是改完上线,过了几天。
产品:开发哥哥再来下,你看我这边想加个中间流程,这个流程是要怎样怎样处理.......,还想区分加了这些操作后的用户,其他不符合这个条件的用户不要影响,能不能实现啊!
我:能啊!内心OS: 咦,似曾相识燕归来,这不是之前加过了一个吗,还加啊!于是吭哧吭哧又给加上了。本想就结束了,但是过了几天,又来问了!于是就不断的if else、if else的来判断装个修改原流程!最终一次不小心,动了下之前状态的代码,悲剧发生了,生产环境报错了!
这是每个开发小哥哥都会遇到的问题,随着业务的不断发展,我们定义表的状态会不断的扩展,状态之间的流转也会越来越复杂,原来的一小块if else代码也会更加的多和杂,着实让人看着摸不着头脑。
那有没有一种模式能让这些业务解耦开,涉及事件的产生和随之产生的影响(状态的流转)。可以先将事件和事件产生后的状态变化绑定起来。不同事件产生的状态流转也是不同的,我们可以从全局的角度来进行配置。
有的! 当然是我们今天的主角-状态模式了定义
在状态模式(State Pattern)中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。
在状态模式中,我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象。意图
允许对象在内部状态发生改变时改变它的行为,对象看起来好像修改了它的类。主要解决
对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。何时使用
代码中包含大量与对象状态有关的条件语句。如何解决
将各种具体的状态类抽象出来。关键代码
通常命令模式的接口中只有一个方法。而状态模式的接口中有一个或者多个方法。而且,状态模式的实现类的方法,一般返回值,或者是改变实例变量的值。也就是说,状态模式一般和对象的状态有关。实现类的方法有不同的功能,覆盖接口中的方法。状态模式和命令模式一样,也可以用于消除 if...else 等条件选择语句。优点
1、封装了转换规则。 2、枚举可能的状态,在枚举状态之前需要确定状态种类。 3、将所有与某个状态有关的行为放到一个类中,并且可以方便地增加新的状态,只需要改变对象状态即可改变对象的行为。 4、允许状态转换逻辑与状态对象合成一体,而不是某一个巨大的条件语句块。 5、可以让多个环境对象共享一个状态对象,从而减少系统中对象的个数。缺点
1、状态模式的使用必然会增加系统类和对象的个数。 2、状态模式的结构与实现都较为复杂,如果使用不当将导致程序结构和代码的混乱。 3、状态模式对"开闭原则"的支持并不太好,对于可以切换状态的状态模式,增加新的状态类需要修改那些负责状态转换的源代码,否则无法切换到新增状态,而且修改某个状态类的行为也需修改对应类的源代码。使用场景
1、行为随状态改变而改变的场景。 2、条件、分支语句的代替者。实际使用代码
说了一堆的概念,大家肯定还是模糊的,那么来这个场景看看吧场景
作为一个小up,最大的愿望就是自己写的东西能被更多人看到了。投币,点赞,收藏,一键三联的操作大家应该熟悉吧,大家的热情直接影响up的更新频率,那么此时事件和状态就出现了:事件:投币,点赞,收藏状态:SOMETIME(想起来什么时候更新就什么时候更新),OFTEN(会经常更新下),USUALLY(有事也更新),ALWAYS(没停过的肝)
我们可以得到一个关系:投币:UpSometimeState -> UpOftenState点赞:UpOftenState -> UpUsuallyState收藏:UpUsuallyState -> UpAlwaysState英文频率从低到高:Sometime -> Often -> Usually -> Always
了解基本信息后,我们来基于设计模式原则来面向对象开发吧!代码
我们先定义一个状态的抽象类,用来表示up的更新频率package cn.guess.statemachine.one; import lombok.Data; /** * @program: guess * @description: up主更新频率状态接口 * @author: xingcheng * @create: 2020-05-10 12:18 **/ @Data public abstract class UpState { /** * 当前up状态下的上下文 */ protected BlogContext blogContext; /** * 该状态下的操作 */ protected abstract void doAction(); /** * 切换状态 */ protected abstract void switchState(); }
接着我们定义子类,分别表示每个不同的状态:package cn.guess.statemachine.one; /** * @program: guess * @description: up主Sometime更新状态 * @author: xingcheng * @create: 2020-05-10 12:22 **/ public class UpSometimeState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // 切换状态 动作:投币 状态流转:UpSometimeState -> UpOftenState blogContext.setUpState(new UpOftenState()); // 执行动作 blogContext.getUpState().doAction(); } @Override public String toString() { return "UpSometimeState"; } } package cn.guess.statemachine.one; import lombok.extern.slf4j.Slf4j; /** * @program: guess * @description: up主Often更新状态 * @author: xingcheng * @create: 2020-05-10 12:22 **/ @Slf4j public class UpOftenState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // 切换状态 动作:投币 状态流转:UpOftenState -> UpUsuallyState blogContext.setUpState(BlogContext.UP_USUALLY_STATE); // 执行动作 blogContext.getUpState().doAction(); } @Override public String toString() { return "UpOftenState"; } } package cn.guess.statemachine.one; import lombok.extern.slf4j.Slf4j; /** * @program: guess * @description: up主Usually更新状态 * @author: xingcheng * @create: 2020-05-10 12:22 **/ @Slf4j public class UpUsuallyState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // 切换状态 动作:投币 状态流转:UpUsuallyState -> UpAlwaysState blogContext.setUpState(BlogContext.UP_ALWAYS_STATE); // 执行动作 blogContext.getUpState().doAction(); } @Override public String toString() { return "UpUsuallyState"; } } package cn.guess.statemachine.one; import lombok.extern.slf4j.Slf4j; /** * @program: guess * @description: up主Always更新状态 * @author: xingcheng * @create: 2020-05-10 12:22 **/ @Slf4j public class UpAlwaysState extends UpState { @Override public void doAction() { System.out.println("nowUpState: " + toString()); } @Override protected void switchState() { System.out.println("originUpState: " + blogContext.getUpState().toString()); // 终态,不切换状态 // 执行动作 blogContext.getUpState().doAction(); } @Override public String toString() { return "UpAlwaysState"; } }
我们还需要一个上下文环境来进行状态的流转关联package cn.guess.statemachine.one; import lombok.Data; import lombok.NoArgsConstructor; /** * @program: guess * @description: 博客上下文相关信息包装 * 投币:UpSometimeState -> UpOftenState * 点赞:UpOftenState -> UpUsuallyState * 收藏:UpUsuallyState -> UpAlwaysState * 英文频率从低到高:Sometime -> Often -> Usually -> Always * @author: xingcheng * @create: 2020-05-10 12:17 **/ @Data @NoArgsConstructor public class BlogContext { public final static UpSometimeState UP_SOMETIME_STATE = new UpSometimeState(); public final static UpOftenState UP_OFTEN_STATE = new UpOftenState(); public final static UpUsuallyState UP_USUALLY_STATE = new UpUsuallyState(); public final static UpAlwaysState UP_ALWAYS_STATE = new UpAlwaysState(); /** * 当前up主状态 */ private UpState upState; public BlogContext(UpState upState) { this.upState = upState; this.upState.setBlogContext(this); } /** * 用户对博客内容的动作-投币 */ public static void throwCoin() { new BlogContext(BlogContext.UP_SOMETIME_STATE).getUpState().switchState(); } /** * 用户对博客内容的动作-点赞 */ public static void like() { new BlogContext(BlogContext.UP_OFTEN_STATE).getUpState().switchState(); } /** * 用户对博客内容的动作-收藏 */ public static void collect() { new BlogContext(BlogContext.UP_USUALLY_STATE).getUpState().switchState(); } }
接着我们写一个客户端来模拟调用流程:package cn.guess.statemachine.one; /** * @program: guess * @description: 状态切换执行器 * @author: xingcheng * @create: 2020-05-10 15:36 **/ public class UpStateClient { public static void main(String[] args) { // 开始模拟每个动作事件-会自动进行状态转化 // 投币 System.out.println("投币动作"); BlogContext.throwCoin(); System.out.println("-----------------------------------------------------------------------"); // 点赞 System.out.println("点赞动作"); BlogContext.like(); System.out.println("-----------------------------------------------------------------------"); // 收藏 System.out.println("收藏动作"); BlogContext.collect(); } }
此时,状态模式便完成了,可以看到我们没有用到if else,便完成了判断。
每个状态也是由一个类来代替的,我们对其中一个状态进行的改动,不会影响其他的状态逻辑
通过这样的方式,很好的实现了对扩展开放,对修改关闭的原则。
我们看下输出:
有的小朋友要问了,开发哥哥,我们现在开发环境几乎都是springboot了,能不能结合spring这么强大的生态,来实现这一模式呢?
能! 毋庸置疑,能结合spring强大的IOC和AOP,完全可以实现一个状态自动机啊!!!SpringBoot状态自动机
还是刚刚的场景,我们通过Spring StateMachine来实现下。代码包的引入: org.springframework.statemachine spring-statemachine-core ${spring-boot-statemachine.version}
我这边使用的是2.2.0.RELEASE版本定义状态和事件枚举package cn.guess.statemachine.tow.enums; import java.util.Objects; /** * up状态事件枚举 英文频率从低到高:Sometime -> Often -> Usually -> Always * @program: guess * @author: xingcheng * @create: 2020-05-10 16:12 **/ public enum UpStateEnum { UP_SOMETIME_STATE(0, "SOMETIME"), UP_OFTEN_STATE(10, "OFTEN"), UP_USUALLY_STATE(20, "USUALLY"), UP_ALWAYS_STATE(30, "ALWAYS"), ; /** * 枚举编码 */ private final int code; /** * 枚举描述 */ private final String value; public int getCode() { return code; } public String getValue() { return value; } UpStateEnum(int code, String value) { this.code = code; this.value = value; } /** * 根据枚举key值转化为枚举对象 * * @param key 枚举值 * @return 枚举对象 */ public static UpStateEnum keyOf(int key) { UpStateEnum[] values = values(); for (UpStateEnum stateEnum : values) { if (Objects.equals(stateEnum.getCode(), key)) { return stateEnum; } } return null; } } package cn.guess.statemachine.tow.enums; import java.util.Objects; /** * 博客事件枚举 * @program: guess * @author: xingcheng * @create: 2020-05-10 16:08 **/ public enum BlobEventEnum { THROW_COIN(0, "投币"), LIKE(10, "点赞"), COLLECT(20, "收藏"), ; /** * 枚举编码 */ private final int code; /** * 枚举描述 */ private final String value; public int getCode() { return code; } public String getValue() { return value; } BlobEventEnum(int code, String value) { this.code = code; this.value = value; } /** * 根据枚举key值转化为枚举对象 * * @param key 枚举值 * @return 枚举对象 */ public static BlobEventEnum keyOf(int key) { BlobEventEnum[] values = values(); for (BlobEventEnum stateEnum : values) { if (Objects.equals(stateEnum.getCode(), key)) { return stateEnum; } } return null; } } 创建状态机配置类package cn.guess.statemachine.tow.config; import cn.guess.statemachine.tow.enums.BlobEventEnum; import cn.guess.statemachine.tow.enums.UpStateEnum; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.statemachine.config.EnableStateMachine; import org.springframework.statemachine.config.EnumStateMachineConfigurerAdapter; import org.springframework.statemachine.config.builders.StateMachineConfigurationConfigurer; import org.springframework.statemachine.config.builders.StateMachineStateConfigurer; import org.springframework.statemachine.config.builders.StateMachineTransitionConfigurer; import org.springframework.statemachine.listener.StateMachineListener; import org.springframework.statemachine.listener.StateMachineListenerAdapter; import org.springframework.statemachine.transition.Transition; import java.util.EnumSet; /** * @program: guess * @description: 该注解用来启用Spring StateMachine状态机功能 * @author: xingcheng * @create: 2020-05-10 16:14 **/ @EnableStateMachine @Configuration public class StateMachineConfig extends EnumStateMachineConfigurerAdapter { /** * configure用来初始化当前状态机拥有哪些状态 * * @param states * @throws Exception */ @Override public void configure(StateMachineStateConfigurer states) throws Exception { states .withStates() // 定义了初始状态为UP_SOMETIME_STATE .initial(UpStateEnum.UP_SOMETIME_STATE) //指定UpStateEnum中的所有状态作为该状态机的状态定义 .states(EnumSet.allOf(UpStateEnum.class)); } /** * configure用来初始化当前状态机有哪些状态迁移动作 * 从其中命名中我们很容易理解每一个迁移动作,都有来源状态source,目标状态target以及触发事件event * 事件和状态流转关系绑定:类似BlogContext的throwCoin及UpSometimeState下的switchState的过程 * * @param transitions * @throws Exception */ @Override public void configure(StateMachineTransitionConfigurer transitions) throws Exception { transitions .withExternal() // 投币:UpSometimeState -> UpOftenState .source(UpStateEnum.UP_SOMETIME_STATE).target(UpStateEnum.UP_OFTEN_STATE) .event(BlobEventEnum.THROW_COIN) .and() .withExternal() // 点赞:UpOftenState -> UpUsuallyState .source(UpStateEnum.UP_OFTEN_STATE).target(UpStateEnum.UP_USUALLY_STATE) .event(BlobEventEnum.LIKE) .and() .withExternal() // 收藏:UpUsuallyState -> UpAlwaysState .source(UpStateEnum.UP_USUALLY_STATE).target(UpStateEnum.UP_ALWAYS_STATE) .event(BlobEventEnum.COLLECT); } /** * configure为当前的状态机指定了状态监听器,其中listener()则是调用了下一个函数创建的监听器实例,用来处理各个各个发生的状态迁移事件。 * 这里注释是因为我们有其他更好的方法去替代 */ // @Override // public void configure(StateMachineConfigurationConfigurer config) throws Exception { // config // .withConfiguration() // // 指定状态机的处理监听器 // .listener(listener()); // } /** * listener()方法用来创建StateMachineListener状态监听器的实例, * 在该实例中会定义具体的状态迁移处理逻辑,上面的实现中只是做了一些输出, * 实际业务场景会有更严密的逻辑,所以通常情况下,我们可以将该实例的定义放到独立的类定义中,并用注入的方式加载进来。 * 这里注释是因为我们有其他更好的方法去替代 */ // @Bean // public StateMachineListener listener() { // return new StateMachineListenerAdapter() { // // @Override // public void transition(Transition transition) { // if (transition.getTarget().getId() == UpStateEnum.UP_SOMETIME_STATE) { // System.out.println("up sometime update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_SOMETIME_STATE // && transition.getTarget().getId() == UpStateEnum.UP_OFTEN_STATE) { // System.out.println("user throw coin, up sometime update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_OFTEN_STATE // && transition.getTarget().getId() == UpStateEnum.UP_USUALLY_STATE) { // System.out.println("user like blob, up usually update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_USUALLY_STATE // && transition.getTarget().getId() == UpStateEnum.UP_ALWAYS_STATE) { // System.out.println("user collect blob, up always update blob"); // return; // } // // if (transition.getSource().getId() == UpStateEnum.UP_ALWAYS_STATE) { // System.out.println("up always update blob"); // return; // } // } // // }; // } } 注解监听器package cn.guess.statemachine.tow.config; import org.springframework.statemachine.annotation.OnTransition; import org.springframework.statemachine.annotation.OnTransitionEnd; import org.springframework.statemachine.annotation.OnTransitionStart; import org.springframework.statemachine.annotation.WithStateMachine; /** * @program: guess * @description: 该配置实现了cn.guess.statemachine.tow.config.StateMachineConfig类中定义的状态机监听器实现 * @author: xingcheng * @create: 2020-05-10 16:31 **/ @WithStateMachine public class EventConfig { @OnTransition(target = "UP_SOMETIME_STATE") public void initState() { System.out.println("up sometime update blob"); } @OnTransition(source = "UP_SOMETIME_STATE", target = "UP_OFTEN_STATE") public void throwCoin() { System.out.println("up sometime update blob"); } @OnTransitionStart(source = "UP_SOMETIME_STATE", target = "UP_OFTEN_STATE") public void throwCoinStart() { System.out.println("up sometime update blob start"); } @OnTransitionEnd(source = "UP_SOMETIME_STATE", target = "UP_OFTEN_STATE") public void throwCoinEnd() { System.out.println("up sometime update blob end"); } @OnTransition(source = "UP_OFTEN_STATE", target = "UP_USUALLY_STATE") public void like() { System.out.println("user like blob, up usually update blob"); } @OnTransition(source = "UP_USUALLY_STATE", target = "UP_ALWAYS_STATE") public void collect() { System.out.println("user collect blob, up always update blob"); } } 创建应用Controller来完成流程package cn.guess.statemachine.tow.controller; import cn.guess.common.api.ApiResult; import cn.guess.common.web.controller.BaseController; import cn.guess.statemachine.tow.enums.BlobEventEnum; import cn.guess.statemachine.tow.enums.UpStateEnum; import cn.guess.system.web.res.UserSelfCenterInfoRes; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.statemachine.StateMachine; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @program: guess * @description: state检测相关接口 * @author: xingcheng * @create: 2020-05-10 16:38 **/ @Slf4j @RestController @RequestMapping("/api/state") @Api(value = "state检测相关接口 API", description = "03.state检测相关接口") public class StateController extends BaseController { @Autowired private StateMachine stateMachine; @GetMapping("/v1/run") @ApiOperation(value = "state检测请求") public String stateRun() { // start()就是创建这个up主的发博客流程,根据之前的定义,该up会处于不经常更新(SOMETIME)状态 stateMachine.start(); // 通过调用sendEvent(Events.THROW_COIN)执行投币操作 stateMachine.sendEvent(BlobEventEnum.THROW_COIN); // 通过调用sendEvent(Events.THROW_COIN)执行点赞操作 stateMachine.sendEvent(BlobEventEnum.LIKE); // 通过调用sendEvent(Events.THROW_COIN)执行收藏操作 stateMachine.sendEvent(BlobEventEnum.COLLECT); return "OK"; } } 调用结果
说明
我们可以对如何使用Spring StateMachine做如下小结:定义状态和事件枚举为状态机定义使用的所有状态以及初始状态为状态机定义状态的迁移动作为状态机指定监听处理器状态监听器
通过上面的入门示例以及最后的小结,我们可以看到使用Spring StateMachine来实现状态机的时候,代码逻辑变得非常简单并且具有层次化。
整个状态的调度逻辑主要依靠配置方式的定义,而所有的业务逻辑操作都被定义在了状态监听器中。
其实状态监听器可以实现的功能远不止上面我们所述的内容,它还有更多的事件捕获,我们可以通过查看StateMachineListener接口来了解它所有的事件定义:
总结
状态模式的核心是封装,将状态以及状态转换逻辑封装到类的内部来实现,也很好的体现了"开闭原则"和"单一职责原则"。每一个状态都是一个子类,不管是修改还是增加状态,只需要修改或者增加一个子类即可。在我们的应用场景中,状态数量以及状态转换远比上述例子复杂,通过"状态模式"避免了大量的if-else代码,让我们的逻辑变得更加清晰。同时由于状态模式的良好的封装性以及遵循的设计原则,让我们在复杂的业务场景中,能够游刃有余地管理各个状态。
美股开盘科技股拖累纳指跌近百点区块链概念重挫Coinbase跌逾27金融界5月11日消息,投资者权衡超预期CPI数据并等待美联储官员讲话,同时美债收益率回落提振市场情绪,美股开盘涨跌互现,道指小幅开涨约30点,纳指跌0。8。区块链概念股大型科技股普
燃油车时代的终结截止2021年的数据,欧洲主要国家的新能源渗透率挪威以超过60遥遥领先,其次是冰岛在30左右,第三名为芬兰在20左右。除了个别国家外(芬兰法国葡萄牙爱尔兰和比利时),欧洲主要国家的
AMD前高管加盟英特尔,担任企业发展部门高级副总裁5月11日晚间,英特尔通过官微宣布,任命MattPoirier担任企业发展部门高级副总裁,自2022年5月30日起生效。英特尔表示,Poirier将领导英特尔的企业发展团队,全权负
搜狗地图官宣5月15日下线,将关闭所有服务,此前App已下架Tech星球5月11日消息,据IT之家,搜狗地图官网显示,搜狗地图将于2022年5月15日23点正式下线,届时关闭所有相关服务,并建议用户下载腾讯地图。公开资料显示,搜狗地图原名图
做点实事吧裁员经济下行互联网寒冬充斥着2022年,让每个人心中蒙上一层阴影,每个人都很难。不过,今天我想要鼓励一下你,疫情总会过去,寒冬也总会过去,现在我们应该做的就是,积蓄能量,厚积薄发。
支付宝的转变刚刚睡醒,就突然意识到支付宝又发生了改变,这个改变提醒我们要时刻提高警惕了!就在昨天晚上,支付宝突然之间就变得很简洁,消费者可以自己把首页的活动推荐取消掉,还可以把一些生活频道都关
北京学而思被列入经营异常,好未来正进行法人地址和经营范围变更记者陈振芳近日,据国家企业信用信息公示系统显示,北京学而思网络科技有限公司(下称北京学而思)因登记住所或者经营场所无法联系,被北京市海淀区市场监督管理局列入经营异常名录。天眼查工作
vivo手机广告关闭流程1点击设置,找到安全,找到更多安全设置然后找到广告与隐私,把个性化广告推荐关掉,然后返回上个界面,找到应用安装把应用推荐关掉,然后在设置主页,找到系统管理,在找到全局搜索,然后打开
购买相机储存卡,你需要了解这些一储存卡类型市场上常见的储存卡有三种,分别是SD卡TF卡(MicroSD卡)CFexpress卡SD卡SD卡是最常见的,大部分相机卡片机用的都是SD卡。SD卡最大的特点是有写保护开
大疆重磅新品上线!最适合小白的无人机,超轻巨能飞5月10日晚,大疆创新发布了消费级无人机新成员DJIMini3Pro,极果君体验过后大为震撼。小小的身材,却能实现如此多的功能,这可能是全系里最适合普通人入手的真入门级全能无人机了
久等了!超精致微型书架箱EmbersLab黑烬EL3超详细试用报告在EmbersLab黑烬EL3的开箱视频之后,很多人就在催更这一全新国产品牌第一款产品的评测!但由于这款产品本身在摩大大的工作中就属于插队产品(因为到现在为止,春节前送过来的一些产