[DDD] ValueObject的一种设计落地及应用

目录

  • 前言
  • 一、ValueObject
  • 二、设计
    • 2.1 接口
    • 2.2 单一值ValueObject
    • 2.3 单一字符串ValueObject
  • 三、实现
    • 3.1 示例
      • 3.1.1 PhoneNumber
      • 3.1.2 SocialCreditCode
  • 四、使用
    • 4.1 异常处理
    • 4.2 Json 反/序列化
      • 4.2.1 请求体
      • 4.2.2 HTTP接口
      • 4.2.3 用例
    • 4.3 JPA/MyBatis
      • 4.3.1 Converter或TypeHandler
      • 4.3.2 Entity
      • 4.3.3 Repository
      • 4.3.4 用例
    • 4.4 CACHE
      • 4.4.1 LocalBasedCache
      • 4.4.2 用例


前言

以前在InfoQ看到过这么一个讲座 Value-Objects-Dan-Bergh-Johnsson.

讲座的细节就不赘述了, 其中举例类似“电话号码”, “货币”在业务中的操作, 如果将这类有业务意义的字符串只是简单通过String/Integer等对象传递, 将丢失其业务意义, 最终编码, 测试都变得更繁琐. 同时程序员还需要在业务流程中时刻关心此类对象是否严格符合业务意义, 比如校验格式, 内容有效性等等. 实际工作看过来, 绝大多数人也都是这样做的.

如果使用ValueObject的设计思想, 设计一个包含“值”和其业务意义的对象, 例如“数量”一定非负之类的. 那么在实际使用中将使得校验, 编码, 测试, 甚至最基本的代码可读性都有明显提高.

本文介绍一种落地设计, 实现最常用的单一字符串值对象, 并参考Springboot环境, 实现接口自动化校验, DAO自动转换落库等等操作, 实现面向对象的编码.

Code Env: JDK21 + SpringBoot3+


一、ValueObject

值对象有两个主要特征:

  • 它们没有任何标识。
    • 没有唯一标识, 可以复用
  • 它们是不可变的。
    • Equals的比较是使用其“值”完成的

二、设计

本文仅对单一字符串值对象的设计作出说明, 因为此类值对象在实现接口, 或者落库时比较容易体会使用ValueObject的好处.

2.1 接口

仅分类, 因为不希望再手动调用校验, 这里就不设计校验的接口了

public interface ValueObject {}

定义单一值ValueObject

  • @JsonValue则提供了通过Jackson实现序列化的能力
    此时Jackson将直接序列化“值”而不是这个ValueObject对象
import com.fasterxml.jackson.annotation.JsonValue;/*** @author hp*/
public interface SingleValueObject<TYPE> extends ValueObject {@JsonValueTYPE value();
}

2.2 单一值ValueObject

实现ValueObject的基本特征

  • 值不可变, 在构造时需要提供值
  • equals, hashcode 通过其值完成, 而非对象本身.
  • @JsonAutoDetect 提供json序列化时获取非公共属性/方法的能力, 如果不提供公共getter, 则通过此注解获取值
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.hp.common.base.exception.NullValueObjectException;
import jakarta.annotation.PostConstruct;import java.util.Objects;/*** 配合jackson方便一些* <p>* 最好不要提供getter, 但是为了日志妥协一下** @author hp* @see JsonAutoDetect;*/
@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.PROTECTED_AND_PUBLIC)
public abstract class AbstractSingleValueObject<TYPE> implements SingleValueObject<TYPE> {protected final TYPE value;@Overridepublic TYPE value() {return value;}protected AbstractSingleValueObject(TYPE value) throws NullValueObjectException {if (Objects.isNull(value)) {throw new NullValueObjectException();}this.value = value;}protected abstract void validate(TYPE value) throws IllegalArgumentException;@Overridepublic String toString() {return this.value.toString();}@Overridepublic boolean equals(Object o) {if (this == o) {return true;}if (o == null || getClass() != o.getClass()) {return false;}AbstractSingleValueObject<?> that = (AbstractSingleValueObject<?>) o;return Objects.equals(value, that.value);}@Overridepublic int hashCode() {return Objects.hash(value);}
}

2.3 单一字符串ValueObject

空字符串在此场景下理解为无意义的输入, 此时考虑通过直接在构造期间抛出异常的方式中断构造过程, 并返回NULL, 以保证没有合法输入就不构造出值对象的目的.

import cn.hutool.core.util.StrUtil;
import com.hp.common.base.exception.NullValueObjectException;/*** @author hp*/
public abstract class AbstractStringBasedSingleValueObject extends AbstractSingleValueObject<String> {protected AbstractStringBasedSingleValueObject(String value) throws NullValueObjectException {super(value);if (StrUtil.isEmpty(value)) {throw new NullValueObjectException();}validate(value);}
}

三、实现

需要说明的是, 实现类不一定完全实现了此类值在现实生活中包含的所有方面, 可以根据业务场景做简单调整和取舍. 比如下文的电话号码示例就省略了区号的信息.

3.1 示例

  • 私有化构造, 仅通过静态方法创建对象
    • @JsonCreator提供了Jackson在反序列化时指定创建对象方法的入口, 这里指定使用静态方法
  • 当输入NULL或空字符串时, 业务上视为无意义的输入, 将不做实例化
  • 当输入非“空”字符串时, 在构造时将根据子类实现的规则进行校验, 并在校验失败时抛出IllegalArgumentException供捕获

3.1.1 PhoneNumber

import com.fasterxml.jackson.annotation.JsonCreator;
import com.google.common.base.Preconditions;
import com.hp.common.base.exception.NullValueObjectException;
import com.hp.common.base.valueobject.AbstractStringBasedSingleValueObject;
import com.hp.common.base.valueobject.Patterns;import java.util.Optional;/*** @author hp*/
public final class PhoneNumber extends AbstractStringBasedSingleValueObject {private PhoneNumber(String phoneNumber) throws NullValueObjectException {super(phoneNumber);}@JsonCreatorpublic static PhoneNumber of(String value) {try {return new PhoneNumber(value);} catch (NullValueObjectException ignore) {return null;}}@JsonCreatorpublic static PhoneNumber of(Long value) {return Optional.ofNullable(value).map(String::valueOf).map(PhoneNumber::of).orElse(null);}@Overridepublic void validate(String value) throws IllegalArgumentException {Preconditions.checkArgument(Patterns.PHONE_PATTERN.asPredicate().test(value), "手机号码格式错误");}
}

3.1.2 SocialCreditCode

import com.fasterxml.jackson.annotation.JsonCreator;
import com.google.common.base.Preconditions;
import com.hp.common.base.exception.NullValueObjectException;
import com.hp.common.base.valueobject.AbstractStringBasedSingleValueObject;
import com.hp.common.base.valueobject.Patterns;/*** @author hp*/
public final class SocialCreditCode extends AbstractStringBasedSingleValueObject {private SocialCreditCode(String value) throws NullValueObjectException {super(value);}@JsonCreatorpublic static SocialCreditCode of(String value){try {return new SocialCreditCode(value);}catch (NullValueObjectException ignore){return null;}}@Overridepublic void validate(String value) throws IllegalArgumentException {Preconditions.checkArgument(Patterns.CREDIT_CODE_PATTERN.asPredicate().test(value), "统一社会信用代码格式错误");}
}

四、使用

4.1 异常处理

可以根据公司情况, 自定义参数校验失败的自定义异常. 这里用最简单的IllegalArgumentException作示例

package com.hp.valueobject.exception;import com.hp.common.base.model.Returns;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;/*** @author hp*/
@Slf4j
@RestControllerAdvice
public class ExceptionControllerAdvice {@ExceptionHandler(IllegalArgumentException.class)public Returns<?> handleIllegalArgumentsException(IllegalArgumentException e) {log.error("请求参数错误", e);return Returns.fail().message(e.getMessage());}
}

4.2 Json 反/序列化

最常见场景之一, RESTful接口参数的Json序列化场景

4.2.1 请求体

package com.hp.valueobject.request;import com.hp.common.base.model.Request;
import com.hp.common.base.valueobject.contact.PhoneNumber;
import com.hp.common.base.valueobject.socialcreditcode.SocialCreditCode;
import lombok.Data;/*** @author hp*/
@Data
public class ValueObjectPostRequest implements Request {private PhoneNumber phone;private SocialCreditCode socialCreditCode;}

4.2.2 HTTP接口

package com.hp.valueobject.controller;import com.hp.common.base.model.Returns;
import com.hp.common.base.valueobject.contact.PhoneNumber;
import com.hp.valueobject.request.ValueObjectPostRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;/*** @author hp*/
@Slf4j
@RequiredArgsConstructor
@RestController
@RequestMapping("valueobject")
public class ValueObjectController {@PostMapping("postRequest")public Returns<?> postRequest(@RequestBody ValueObjectPostRequest request) {return Returns.success().data(request);}@GetMapping("getRequest")public Returns<?> getRequest(@RequestParam PhoneNumber phone) {return Returns.success().data(phone);}
}

4.2.3 用例

用例格式为Idea http client.

POST Request, phone正确, 信用代码空字符串无意义

# Request
POST http://localhost:9988/valueobject/postRequest
Content-Type: application/json
Content-Length: 54
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.9)
Accept-Encoding: br,deflate,gzip,x-gzip{"phone": "18123123123","socialCreditCode": ""
} # Response
POST http://localhost:9988/valueobject/postRequestHTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 22 Mar 2024 06:17:13 GMT
Keep-Alive: timeout=60
Connection: keep-alive{"code": 200,"message": "操作成功","data": {"phone": "18123123123","socialCreditCode": null}
}
Response code: 200; Time: 37ms (37 ms); Content length: 219 bytes (219 B)

POST phone 参数错误 10 位

POST http://localhost:9988/valueobject/postRequest
Content-Type: application/json
Content-Length: 27
Connection: Keep-Alive
User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.9)
Accept-Encoding: br,deflate,gzip,x-gzip{"phone": "1812323123"
}
###POST http://localhost:9988/valueobject/postRequestHTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 22 Mar 2024 06:12:38 GMT
Keep-Alive: timeout=60
Connection: keep-alive{"code": 500,"message": "手机号码格式错误","data": null
}Response code: 200; Time: 118ms (118 ms); Content length: 45 bytes (45 B)

GET phone格式正确

GET http://localhost:9988/valueobject/getRequest?phone=18123123123HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 22 Mar 2024 06:21:27 GMT
Keep-Alive: timeout=60
Connection: keep-alive{"code": 200,"message": "操作成功","data": "18123123123"
}
Response file saved.
> 2024-03-22T142127.200.jsonResponse code: 200; Time: 10ms (10 ms); Content length: 50 bytes (50 B)

GET phone格式错误

GET http://localhost:9988/valueobject/getRequest?phone=1812312313HTTP/1.1 200 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 22 Mar 2024 06:22:23 GMT
Keep-Alive: timeout=60
Connection: keep-alive{"code": 500,"message": "手机号码格式错误","data": null
}
Response file saved.
> 2024-03-22T142223.200.jsonResponse code: 200; Time: 25ms (25 ms); Content length: 45 bytes (45 B)

4.3 JPA/MyBatis

4.3.1 Converter或TypeHandler

PhoneNumber示例

JPA converter

package com.hp.jpa.convertor;import com.hp.common.base.valueobject.AbstractSingleValueObject;
import com.hp.common.base.valueobject.AbstractStringBasedSingleValueObject;
import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Converter;
import java.util.Optional;@Converter
public abstract class AbstractStringBasedSingleValueObjectConverter<T extends AbstractStringBasedSingleValueObject> implements AttributeConverter<T, String> {public AbstractStringBasedSingleValueObjectConverter() {}public String convertToDatabaseColumn(T attribute) {return (String)Optional.ofNullable(attribute).map(AbstractSingleValueObject::value).orElse("");}
}
package com.hp.valueobject.converter;import com.hp.common.base.valueobject.contact.PhoneNumber;
import com.hp.jpa.convertor.AbstractStringBasedSingleValueObjectConverter;
import jakarta.persistence.Converter;/*** @author hp*/
@Converter
public class PhoneNumberJPAConverter extends AbstractStringBasedSingleValueObjectConverter<PhoneNumber> {@Overridepublic PhoneNumber convertToEntityAttribute(String dbData) {return PhoneNumber.of(dbData);}
}

Mybatis-plus typeHandler

package com.hp.mybatisplus.convertor;import com.hp.common.base.valueobject.AbstractStringBasedSingleValueObject;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import org.apache.ibatis.type.JdbcType;public abstract class AbstractStringBasedSingleValueObjectConverter<T extends AbstractStringBasedSingleValueObject> implements TypeHandlerCodeGenAdapter<T, String> {public AbstractStringBasedSingleValueObjectConverter() {}public void setParameter(PreparedStatement ps, int i, T t, JdbcType jdbcType) throws SQLException {ps.setString(i, (String)t.value());}
}
package com.hp.valueobject.converter;import com.hp.common.base.valueobject.contact.PhoneNumber;
import com.hp.mybatisplus.convertor.AbstractStringBasedSingleValueObjectConverter;import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.SQLException;/*** @author hp*/
public class PhoneNumberMybatisTypeHandler extends AbstractStringBasedSingleValueObjectConverter<PhoneNumber> {@Overridepublic PhoneNumber getResult(ResultSet rs, String columnName) throws SQLException {return PhoneNumber.of(rs.getString(columnName));}@Overridepublic PhoneNumber getResult(ResultSet rs, int columnIndex) throws SQLException {return PhoneNumber.of(rs.getString(columnIndex));}@Overridepublic PhoneNumber getResult(CallableStatement cs, int columnIndex) throws SQLException {return PhoneNumber.of(cs.getString(columnIndex));}
}

4.3.2 Entity

@Entity
@Table(name = "unified_social_credit_code")
@Getter
@Setter
public class UnifiedSocialCreditCode {@Id@GeneratedValue(strategy = GenerationType.AUTO)private Long id;@Convert(converter = PhoneNumberConverter.class)private PhoneNumber username;@Convert(converter = SocialCreditCodeConverter.class)private SocialCreditCode socialCreditCode;

4.3.3 Repository

直接传递ValueObject类型参数即可, QueryDSL也可以正常使用
注: MyBatis省略, 其低版本无法在自定义查询中自动通过typeHandler提取值, 需要手动 ValueObject.value();

@Repository
public interface JpaBasedUnifiedSocialCreditCodeDao extends BaseRepository<UnifiedSocialCreditCode, Long> {List<UnifiedSocialCreditCode> findAllBySocialCreditCodeIn(Collection<SocialCreditCode> codes);
}

4.3.4 用例

JPA

@Test
public void givenUSCC_whenQueryInDB_thenReturnsNonnull() {// givenfinal String unifiedSocialCreditCode = "91510115MABRCTYM2W";final SocialCreditCode socialCreditCode = SocialCreditCode.of(unifiedSocialCreditCode);// whenfinal List<UnifiedSocialCreditCode> list = unifiedSocialCreditCodeRepository.findAllBySocialCreditCode(Lists.newArrayList(socialCreditCode));// thenassertThat(list).isNotEmpty().size().isGreaterThanOrEqualTo(1);final UnifiedSocialCreditCode first = list.getFirst();assertThat(first.getSocialCreditCode()).isEqualTo(socialCreditCode);assertThat(first.getUsername()).isNotNull();
}

4.4 CACHE

缓存场景, 这里主要是针对服务内缓存的说明, 例如使用Redis等中间件时, 都需要序列化, 此时使用jackson序列化即可

4.4.1 LocalBasedCache

例如使用Map作为容器的场景, 因为在AbstractSingleValueObject中已经重写了hashCode和equals, 使得ValueObject可以直接作为键完成存储和比较

@Slf4j
@Component
public class LocalBasedCache implements USCCCache {private final static Map<SocialCreditCode, List<UserCacheModel>> CACHE = Maps.newConcurrentMap();@Overridepublic boolean exist(SocialCreditCode socialCreditCode) {return CACHE.containsKey(socialCreditCode);}@Overridepublic void put(SocialCreditCode socialCreditCode, UserCacheModel model) {CACHE.compute(socialCreditCode, (key, value) -> {if (Objects.isNull(value)) {return Lists.newArrayList(model);} else {value.add(model);return value;}});}@Overridepublic List<UserCacheModel> get(SocialCreditCode socialCreditCode) {return CACHE.getOrDefault(socialCreditCode, Collections.emptyList());}@Overridepublic void remove(SocialCreditCode socialCreditCode) {CACHE.remove(socialCreditCode);}
}

4.4.2 用例

 @Testpublic void givenSocialCreditCode_whenCallPutAndExist_thenSuccess() {// givenfinal LocalBasedCache cache = new LocalBasedCache();final SocialCreditCode socialCreditCode = SocialCreditCode.of("915101007130091284");final SocialCreditCode socialCreditCode2 = SocialCreditCode.of("915101007130091284");final SocialCreditCode socialCreditCode3 = SocialCreditCode.of("915101007130091283");// whencache.put(socialCreditCode, new UserCacheModel(1L,"1"));// thenassertThat(cache.exist(socialCreditCode)).isTrue();assertThat(cache.exist(socialCreditCode2)).isTrue();assertThat(cache.exist(socialCreditCode3)).isFalse();}

测试结果
在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/772638.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

202449读书笔记|《悦读诗 乐生活》——跌倒,是生活的偶然 发光,是生命的态度

202449读书笔记|《悦读诗 乐生活》——跌倒&#xff0c;是生活的偶然 发光&#xff0c;是生命的态度 第一篇 全都喜欢上第二篇 生如夏花第三篇 “希望”是带有羽毛之物第四篇 我来到这世界…… 《悦读诗 乐生活&#xff1a;小学生经典现代诗歌鉴赏集》赵迁编著&#xff0c;是小…

【JavaEE初阶系列】——多线程案例三——定时器

目录 &#x1f6a9;定时器是什么 &#x1f6a9;标准库中的定时器 &#x1f6a9;自定义定时器 &#x1f388;构造Task类 &#x1f4dd;相对时间和绝对时间 &#x1f388;构造MyTime类 &#x1f4dd;队列空和队列不为空 &#x1f4dd;wait(带参)解决消耗资源问题 &#…

docker配置镜像加速后容器和镜像消失

一、问题描述 根据阿里云给docker配置镜像加速器 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json <<-EOF {"registry-mirrors": ["https://gt6j98xi.mirror.aliyuncs.com"] } EOF sudo systemctl daemon-reload sudo systemctl rest…

NO9 蓝桥杯单片机串口通信之进阶版

1 回顾 串口通信的代码编写结构还是与中断一样&#xff0c;不同的是&#xff1a; 初始中断函数条件涉及到串口通信相关的寄存器和定时器1相关的寄存器&#xff08;定时器1用于产生波特率&#xff09;&#xff0c;但初始条件中的中断寄存器只考虑串口通信而不考虑定时器1。 v…

基于springboot+vue+Mysql的网上图书商城

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

多功能的RSS工具RSS Funnel

什么是 RSS Funnel &#xff1f; RSS Funnel 是一个模块化的 RSS 处理管道系统。它可以以多种方式操作 RSS/Atom 源。 例如&#xff1a; 提取完整内容从 HTML 页面生成 RSS 订阅删除不需要的元素和文本使用正则表达式进行文字编辑或替换保留或删除与关键词或模式匹配的文章突出…

2024年【熔化焊接与热切割】考试报名及熔化焊接与热切割找解析

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 熔化焊接与热切割考试报名考前必练&#xff01;安全生产模拟考试一点通每个月更新熔化焊接与热切割找解析题目及答案&#xff01;多做几遍&#xff0c;其实通过熔化焊接与热切割实操考试视频很简单。 1、【单选题】 下…

SE注意力模块学习笔记《Squeeze-and-Excitation Networks》

Squeeze-and-Excitation Networks 摘要引言什么是全局平均池化&#xff1f; 相关工作Deep architectures Squeeze-and-Excitation Blocks3.1. Squeeze: Global Information Embedding3.2. Excitation: Adaptive Recalibration3.3. Exemplars: SE-Inception and SE-ResNet 5. Im…

2024.3.26学习笔记

今日学习韩顺平java0200_韩顺平Java_对象机制练习_哔哩哔哩_bilibili 今日学习p273-p285 包 包的本质实际上就是创建不同的文件夹/目录来保存类文件 包的三大作用 区分相同名字的类 当类很多时&#xff0c;可以很好的管理类 控制访问范围 包的基本语法 package com.xx…

出席2024亚太内容分发大会,火山引擎边缘云“加速”游戏体验升级

3月26日&#xff0c;第十四届亚太内容分发大会暨CDN峰会在北京成功举办&#xff0c;火山引擎边缘云产品架构高级总监许思安出席并以《火山引擎边缘云游戏行业解决方案&#xff0c;“加速”游戏体验升级》为主题&#xff0c;分享了火山引擎边缘云在游戏行业的思考和实践。同时&a…

面试经典150题【91-100】

文章目录 面试经典150题【91-100】70.爬楼梯198.打家劫舍139.单词拆分322.零钱兑换300.递增最长子序列77.组合46.全排列39.组合总和&#xff08;※&#xff09;22.括号生成79.单词搜索 面试经典150题【91-100】 五道一维dp题五道回溯题。 70.爬楼梯 从递归到动态规划 public …

idea中Git项目遇到“Filename too long”错误 与 配置Git的ssh证书

一&#xff1a;“Filename too long”问题解决办法 错误信息&#xff1a; fatal: cannot create directory at xxxx: Filename too long warning: Clone succeeded, but checkout failed. You can inspect what was checked out with git status and retry with git restore …

24/03/26总结

面向对象练习题&#xff1a;&#xff08;封装&#xff0c;继承&#xff0c;多态) 封装&#xff1a;对象代表什么&#xff0c;就得封装对应的数据&#xff0c;并提供数据对应的行为,(把零散的数据和行为封装成一个整体&#xff1a;也就是我们说的对象&#xff09; 继承:当封装…

latex中的算法algorithm报错Undefined control sequence.

这里写目录标题 1. 错误原因2. 进行改正3. 爱思唯尔期刊与施普林格期刊对于算法的格式不太一样&#xff0c;不能直接套用总结 1. 错误原因 我在算法中使用\State 2. 进行改正 换成\STATE 3. 爱思唯尔期刊与施普林格期刊对于算法的格式不太一样&#xff0c;不能直接套用 总…

多线程执行一半后不往后走的坑

场景简单演示 首先演示一个简单的场景。 采用ThreadPoolExecutor提交线程的方式&#xff0c;直接在多线程中执行的某个地方抛出一个异常。 用submit方法提交的情况&#xff1a; 调用的地方&#xff1a; 发现一直卡在那&#xff0c;没有任何错误日志。 改成execute方法提交多…

优秀电源工程师需要的必备技能

随着电源市场的不断扩张,开关电源行业飞速发展,企业对电源工程师的需求日益增加,对电源工程师的技能要求也日渐提高,相信没有一位电源工程师会错过让自己变得更优秀的机会。作为一名数字电源从业者,今天就带大家细数一下优秀电源工程师具备的那些技能。 一、新手必备课程…

#GIT|Git Flow#Gitflow工作流程

Gitflow是一种使用功能分支和多个主分支的Git分支模型&#xff0c;它适用于有预定发布周期的项目&#xff0c;也适用于DevOps最佳实践中的持续交付。这个工作流程不会添加任何新的概念或命令&#xff0c;而是为不同的分支分配了非常具体的角色&#xff0c;并定义了它们应该如何…

串口通信标准RS232 RS485 RS422的区别

RS-232、RS-422、RS-485是关于串口通讯的一个机械和电气接口标准&#xff08;顶多是网络协议中的物理层&#xff09;&#xff0c;不是通讯协议&#xff0c;它们之间的几个不同点如下&#xff1a; 一、硬件管脚接口定义不同 二、工作方式不同 RS232&#xff1a; 3线全双工 RS…

干货分享之反射笔记

入门级笔记-反射 一、利用反射破泛型集合二、Student类三、获取构造器的演示和使用1.getConstructors只能获取当前运行时类的被public修饰的构造器2.getDeclaredConstructors:获取运行时类的全部修饰符的构造器3.获取指定的构造器3.1得到空构造器3.2得到两个参数的有参构造器&a…

Vue2(十一):全局事件总线、消息订阅与发布pubsub、TodoList的编辑功能、$nextTick、过渡与动画

一、全局事件总线 1、思路解析 一种组件间通信的方式&#xff0c;适用于任意组件间通信。通俗理解就是一个定义在所有组件之外的公共x&#xff0c;这个x可以有vm或vc上的同款$on、$off、$emit&#xff0c;也可以让所有组件都访问到。 第一个问题&#xff1a;那怎样添加这个x才…