认识JUnit 5

in Unit Test

JUnit 核心组件

JUnit框架主要有三个核心组件:

JUnit Platform

提供在Jvm上启动测试框架的核心基础,是JUnit和客户端(包含构建工具Maven 或者Gradle或者Eclipse和IntelliJ)之间的接口。它采用了一种可以让外部工具发现,过滤和执行测试的称为“Launcher”的概念。

它还提供了用于开发在JUnit平台上运行测试框架的TestEngine API。使用TestEngine API,Spock,Cucumber和FitNesse第三方测试库可以直接植入和提供他们自定义的TestEngine。

JUnit Jupiter

它为在 JUnit 5 中编写测试和扩展提供了新的编程模型和扩展模型。它有一个全新的注解,用于在 JUnit 5 中编写测试用例。其中一些注解是@BeforeEach@AfterEach@AfterAll@BeforeAll 等。它实现了 JUnit Platform 提供的 TestEngine API,以便可以运行 JUnit 5 测试。

JUnit Vintage

“Vintage”一词的基本意思是经典。因此,该子项目为在 JUnit 4 和 JUnit 3 中编写测试用例提供了广泛的支持。因此,该项目提供了向后兼容性。

image-20220411204229226

原文链接: https://www.educative.io/courses/java-unit-testing-with-junit-5/xV9mMjj74gE

Comment and share

DDD是什么

  1. DDD并不是一种技术架构,而是一种划分业务领域范围的方法论。
  2. DDD不是一种架构,而是一种架构方法论,目的就是将复杂问题领域简单化,帮助我们设计出清晰的领域和边界,可以很好的实现技术架构的演进。

DDD 包括战略设计和战术设计两部分。

  1. DDD战略部分战略设计主要从业务视角出发,建立业务领域模型,划分领域边界,建立通用语言的限界上下文,限界上下文可以作为微服务设计的参考边界。
  2. DDD战术部分战术设计则从技术视角出发,侧重于领域模型的技术实现,完成软件开发和落地,包括:聚合根、实体、值对象、领域服务、应用服务和资源库等代码逻辑的设计和实现。

DDD与微服务

DDD 是一种架构设计方法,微服务是一种架构风格,两者从本质上都是为了追求高响应力,而从 业务视角 去分离应用系统建设复杂度的手段。两者都强调从业务出发,其核心要义是强调根据业务发展,合理划分领域边界,持续调整现有架构,优化现有代码,以保持架构和代码的生命力,也就是我们常说的演进式架构。

  • DDD 主要关注:从业务领域视角划分领域边界,构建通用语言进行高效沟通,通过业务抽象,建立领域模型,维持业务和代码的逻辑一致性。
  • 微服务主要关注:运行时的进程间通信、容错和故障隔离,实现去中心化数据管理和去中心化服务治理,关注微服务的独立开发、测试、构建和部署。

实践步骤:

第一步:在事件风暴中梳理业务过程中的用户操作、事件以及外部依赖关系等,根据这些要素梳理出领域实体等领域对象。

第二步:根据领域实体之间的业务关联性,将业务紧密相关的实体进行组合形成聚合,同时确定聚合中的聚合根、值对象和实体。在这个图里,聚合之间的边界是第一层边界,它们在同一个微服务实例中运行,这个边界是逻辑边界,所以用虚线表示。

第三步:根据业务及语义边界等因素,将一个或者多个聚合划定在一个限界上下文内,形成领域模型。在这个图里,限界上下文之间的边界是第二层边界,这一层边界可能就是未来微服务的边界,不同限界上下文内的领域逻辑被隔离在不同的微服务实例中运行,物理上相互隔离,所以是物理边界,边界之间用实线来表示。

image-20220411183114240

总结

DDD可以理解为:ddd 是一个事件风暴 (分类划分),进而知道组织划分(也就是中台)、系统划分(微服务)、代码划分/设计的思想方法

Comment and share

场景

当被测试方法返回结果是集合,我们应该使用哪个断言,如果一个一个元素比较会比较麻烦。所以使用其他包下的断言。

代码示例

被测试代码如下,是一组经典的合并两个Map<String, List>的代码(可以直接使用当成工具方法,注意引入Guava包):

1
2
3
4
5
6
7
8
9
10
11
public Map<String, List<Object>> mergeTwoGroupedMapCollection(Map<String, List<Object>> groupedCollectionOne,
Map<String, List<Object>> groupedCollectionTwo) {
Map<String, List<Object>> mapGlobal = Maps.newHashMap();
mapGlobal.putAll(groupedCollectionOne);
groupedCollectionTwo.forEach((k, v) -> mapGlobal.merge(k, v, (v1, v2) -> {
List<Object> data = new ArrayList<>(v1);
data.addAll(v2);
return new ArrayList<>(data);
}));
return mapGlobal;
}

测试如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertIterableEquals;

@Test
@DisplayName("should merge multiple map when given two map has grouped by")
void mergeTwoGroupedMapCollection() {
MergedCollection mergedCollection = MergedCollection.builder().build();
Map<String, List<Object>> groupedOne = Maps.newHashMap();
groupedOne.put("a", Lists.newArrayList("a1", "a2", "a3"));
groupedOne.put("b", Lists.newArrayList("b1", "b2", "b3"));
groupedOne.put("c", Lists.newArrayList("c1", "c2", "c3"));

Map<String, List<Object>> groupedTwo = Maps.newHashMap();
groupedTwo.put("c", Lists.newArrayList("c3", "c4", "c5", "c6", "c7"));

Map<String, List<Object>> newGrouped = mergedCollection.mergeTwoGroupedMapCollection(groupedOne, groupedTwo);
List<Object> except_a = ImmutableList.of("a1", "a2", "a3");
List<Object> except_b = ImmutableList.of("b1", "b2", "b3");
List<Object> except_c = ImmutableList.of("c1", "c2", "c3", "c3", "c4", "c5", "c6", "c7");
assertAll("all elements",
()->assertIterableEquals(except_a, newGrouped.get("a")),
()->assertIterableEquals(except_b, newGrouped.get("b")),
()->assertIterableEquals(except_c, newGrouped.get("c"))
);

总结

对于这种工具性的方法,必须添加更小粒度的测试。以便于在重构的时候能够保证代码正确性,和理解上下文逻辑。哪如果是私有方法呢?测试大于封装

Comment and share

老生常谈之什么是单元测试

  1. 单元测试是低级的,专注于软件系统的最小部分。
  2. 单元测试是程序员自己使用某种测试框架来编写的。
  3. 对于单元测试的期望是运行速度比其它的测试要更快。
    原文参照Martin Fowler博文

Unit Test和TDD的关系

  1. TDD中的T到底是不是Unit Test 存争议,有的人说也可以是集成测试。以下是google测试经理的一段话:

    段念:把 TDD 等同于单元测试,认为 TDD 只是“提前写单元测试”这种想法应该是很多不太了解 TDD 的人容易犯的错误吧。如果把 TDD 放到敏捷开发的大背景下,我倒不觉得 TDD 有什么明显的不足,但如果单独考量 TDD 在企业中的实践,TDD 技术本身不关注代码的质量应该是一个明显的问题。应用 TDD 的企业通常需要采用持续的 Code Review 和 Refactory 方法保证通过 TDD 产生的代码的质量。

原文链接:https://www.infoq.cn/article/virtual-panel-tdd/

  1. 还有一段话我觉得比较好,下来总结下。
    • TDD 并不是石头里蹦出来的孙悟空,DBC(Design By Contract)可以看作是 TDD 的前身。在 DBC 的观点里,设计应该以规约(Contract)的形势体现,规约定义了被开发对象的行为。
    • TDD 中的 T,在表现形式上是“测试”,但其实,它更应该被理解为“对被实现对象”的行为限定,也就是 DBC 中的规约。
    • “测试”只是用来体现规约的形式。 单元测试通常被定义为“对应用最小组成单位的测试”,它的测试对象通常是函数或是类,在对类的设计和实现应用 TDD 时,为类建立的测试通常与类的单元测试相当类似,因此 TDD 中的 T 往往被误认为是单元测试本身。
    • TDD 中的 T 描述的是规约,是设计的一部分;
    • 其次,TDD 中的 T 并不明确要求 T 对实现代码的覆盖率;
    • 第三,TDD 的 T 的侧重点是“描述被实现对象应该具有的行为”,而不仅仅是“验证该类的行为是否正确”。
    • 第三,TDD 的 T 的侧重点是“描述被实现对象应该具有的行为”,而不仅仅是“验证该类的行为是否正确”。当然,TDD 中的 T 在形式上是测试,在重构中也可以作为被实现对象的行为验证框架。
    • 单元测试、集成测试、系统测试、用户验收测试是基于传统软件开发过程的划分,在传统软件开发观点中,这几类测试不仅意味这测试对象的不同,同样也以为着不同的测试在开发周期中处于不同的位置。但在敏捷开发中,如果继续使用这几个名词,最多也只能保留它们在测试对象方面的含义。对于 TDD 来说(ATDD 和 BDD 可以认为是 TDD 的变体),在不同的测试类别中都可以应用之,唯一的区别在于 T 面向的对象不同。

测试框架Mockito 和 PowerMock介绍

  1. Mockito可以让你写出优雅、简洁的单元测试代码。Mockito采用了模拟技术,模拟了一些在应用中依赖的复杂对象,从而把测试对象和依赖对象隔离开来。
  2. PowerMock是在其他单元测试框架的基础上做了增强。PowerMock实现了对静态方法、构造方法、私有方法以及final方法的模拟支持等强大功能。 但是实现方式是通过提供定制的类加载器以及一些字节码篡改技术,会导致部分单元测试用例不会被覆盖率检测工具检测到,所以迫不得已不推荐使用。

利用Spring 搭配 Mockito 编写单元测试

  1. 如下一个典型的用户service服务。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    @Service
    public class UserService {
    @Autowired
    private UserDao userDao;

    /*Id 生成器*/
    @Autowired
    private IdGenerator idGenerator;

    /**
    * 配置参数
    */
    @Value("${userService.canModify}")
    private Boolean canModify;

    /**
    * 创建用户
    *
    * @param userCreate 用户创建
    * @return 用户标识
    */
    public Long createUser(UserVO userCreate) {
    // 获取用户标识
    Long userId = userDAO.getIdByName(userCreate.getName());

    // 根据存在处理
    // 根据存在处理: 不存在则创建
    if (Objects.isNull(userId)) {
    userId = idGenerator.next();
    UserDO create = new UserDO();
    create.setId(userId);
    create.setName(userCreate.getName());
    userDAO.create(create);
    }
    // 根据存在处理: 已存在可修改
    else if (Boolean.TRUE.equals(canModify)) {
    UserDO modify = new UserDO();
    modify.setId(userId);
    modify.setName(userCreate.getName());
    userDAO.modify(modify);
    }
    // 根据存在处理: 已存在禁修改
    else {
    throw new UnsupportedOperationException("不支持修改");
    }

    // 返回用户标识
    return userId;
    }
    }

  2. 针对以上service服务在编写单元测试时,针对它的依赖userDao, idGenerator, canModify我们采用stub来预设存根。这样可以保证我们service只测试自己的逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
@ExtendWith(MockitoExtension.class)
class UserServiceTest {
/** 模拟依赖对象 */
/** 用户DAO */
@Mock
private UserDAO userDAO;
/** 标识生成器 */
@Mock
private IdGenerator idGenerator;

/** 定义被测对象 */
/** 用户服务 */
@InjectMocks
private UserService userService;

/**
* 在测试之前
*/
@Before
public void beforeTest() {
// 注入依赖对象
ReflectionTestUtils.setField(Boolean.class, "canModify", false);
}

/**
*测试方法
*也可以使用 @Display 来标注测试方法名称。保证从测试方法名称中能读懂测试的目的,和简单的上下文。
* 注意此处的Test引用jupiter提供的注解。不然会有注入的空指针
*/
@Test
void should_createUserWithNew_givenInfo_WhenNotExisted(){
//given data; stub
when(userDAO).getIdByName(any()).thenReturn(null);
Long userId = 1L;
when(idGenerator).next().thenReturn(userId);
UserVO userCreate = new UserVo(***,***,***);

//when
Long acturalUserId = userService.createUser(userCreate);

//then
assertEquals(userId, acturalUserId);
// 验证依赖方法
// 验证依赖方法: userDAO.getByName
Mockito.verify(userDAO).getIdByName(userCreate.getName());
// 验证依赖方法: idGenerator.next
Mockito.verify(idGenerator).next();
// 验证依赖方法: userDAO.create
ArgumentCaptor <UserDO> userCreateCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).create(userCreateCaptor.capture());
// 验证依赖对象
Mockito.verifyNoMoreInteractions(idGenerator, userDAO);
}
}

@Test
void should_updateUser_givenUser_whenHasExisted(){
//只需要stub此处修改,然后进行已存在断言
Long userId = 1L;
when(userDAO).getIdByName(any()).thenReturn(userId);

//then
....
}

@Test
void should_exception_givenInvalid(){
//then 断言采用Junit 5提供的
Assert.assertThrows("返回异常不一致",
UnsupportedOperationException.class, () -> userService.createUser(userCreate))
}

如何测试Controller

  1. 保证Api层的访问状态是成功的
  2. 保证入参校验逻辑是可通过的。
  3. 与Service测试不同的是我们要启动web服务,所以必须启动spring容器。但是为了可测service服务我们还是利用Stub技术给出预期返回值。
  4. 利用Spring 提供的@Import({*****.class}}指定我们容器中需要的类,可以方便的避免容器需要加载所有bean很慢的尴尬。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@WebMvcTest(controllers = UserController.class, useDefaultFilters = false)
@Import({UserController.class,
AopAutoConfiguration.class,
DependenceService.class})
@ActiveProfiles("test")
class UserControllerTest extends ControllerBaseTest {

/**
*利用Import和Autowired可以将真是的bean在运行测试的时候注入进来。会调用真是的方法。
*/
@Autowired
private MockMvc mvc;

@Autowired
private DependenceService service;

@MockBean
private UserService service;

@Test
void should_200_when_create() {
Long userId = 1L;
when(service.createUser(any())).thenReturn(userId);

//when
mvc.perform(MockMvcRequestBuilders.post("/user")
.content(jsonString) //given data
.header("X-TIME-ZONE", "Asia/Shanghai")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());//then
}

}
  1. 使用ArgumentCaptor验证代码中间被stub掉方法的参数.

    存在一种情况我们给serviceA中的methodA写单元测试的过程中,发现调用了serviceB的methodB方法,并且为serviceB方法new 了一个ObjectA对象作为调用serviceB.methodB的参数,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    public class ServiceA {
    @Autowired
    public ServiceB serviceB

    public void methodA() {
    /*
    * 其它业务代码
    */
    for (i = 0; i < 3; i++ ){
    ObjectA objA = new ObjectA();
    ObjcetB objB = serviceB.methodB("hello", objA);
    }
    /*
    * 其它业务代码
    */

    }
    }

    此时在测试 methodA的时候,需要使用测试替身代替真实的serviceB.methodB(objA);调用,这个时候我们不能使用 Mockito.when(serviceB.methodB(eq(“hello”), eq(new ObjectA())))来进行替换,因为new出来的对象是不同的对象所以stub不住。这个时候应该使用使用ArgumentCaptor进行捕获后验证,如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    ArgumentCaptor<ObjectA> objACaptor = ArgumentCaptor.forClass(ObjectA.class);
    //利用mockito.when().thenReturn()返回多个对象来stub循环中的三方调用
    Mockito.when(serviceB.methodB(eq("hello"), objACaptor.caputre())).thenReturn(objB1, obj2, obj3);
    //然后获取三次captor捕获的三个参数进行验证。
    List<ObjectA> objAs = objACaptor.getValues();
    //验证三次参数
    Assertions.assertEquals(objAs(0), objB1);
    Assertions.assertEquals(objAs(1), objB2);
    Assertions.assertEquals(objAs(2), objB2);

相关链接

Java编程技巧之单元测试用例编写流程:https://zhuanlan.zhihu.com/p/371759603

谈一谈单元测试:https://mp.weixin.qq.com/s/ioya1kzdTGPB0oOZ3DUmig

Comment and share

消除if-else之为Enum添加行为

场景描述

Java提供枚举类给了开发者更可读的代码实现,我们可以将很多字段作为枚举类型。并且赋予枚举类行为,可以省略掉根据枚举类判断而实现不同行为的众多if…else…

如下是根据参数而下载不同类型的文件的枚举代码,给每个枚举类型添加了响应的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public enum DownloadFileType {
CSV {
@Override
public void writeToLocal() {
exportUtil.writeCsv();
}
},
EXCEL {
@Override
public void writeToLocal() {
exportUtil.writeExcel();
}
};

public abstract void writeToLocal();

public static DownloadFileType of(String fileType) {
return Arrays.stream(DownloadFileType.values())
.filter(type -> type.toString().equals(fileType.toUpperCase(Locale.ROOT)))
.findFirst()
.orElse(EXCEL);
}
}

调用方可以免除掉if…else 只需要获取到下载的类型来执行不同的写入。

1
2
3
4
//write to local tmp dir
//fileType: "excel" or "csv"
DownloadFileType.of(fileType).writeToLocal();

代码训练

附上我之前实现一个case的代码链接

https://github.com/wenPKtalk/loofah

Comment and share

About this blog

in Tools

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Manage branches

  • Just update view

  • Code source: main

  • Static file: gh-pages

Install all dependents

1
2
3
$ npm install
$ cd themes/tranquilpeak
$ npm install && npm run prod

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
2
$ hexo clean
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment

Comment and share

概述

最近20年软件设计发生了天翻地覆的变化,但是SOLID原则至今仍然是软件设计的最佳实践。

SOLID 原则对于对于创建高质量软件是久经测试的标题。但是在现代多范式编程(函数式编程等)和云计算兴起的年代,它依然能够坚挺吗?我将通过如下文章解释SOLID代表了什么,为什么它依然适用现代软件,并且分享一些例子来解释。

什么是SOLID

SOLID是Robert C. Martin在2000年提取出来的一系列原则。它被建议去作为面向对象(OO)编程质量的特殊思考方式。总得来讲SOLID在这几个方面:代码如何切分,代码私有和对外暴露,代码之间如何调用提出了建议。我下面将深入研究每个字母(S.O.L.I.D)的原始含义,并且扩展到面向对象编程之外的使用。

有哪些改变?

21世纪早期,是Java和C++称霸的时期,理所当然我的大学很多课程都使用Java语言来当作训练。Java的流行催生出来了一些书籍,课程 和其它资料使得人们从写代码过度到写出好的代码。

因此,软件工业发生了深远的影响。有几个值得注意的点:

  • **动态类型语言(Dynamically-typed languages) ** 比如 Python,Ruby, 尤其JavaScript 变的和Java一样流行—甚至在某些行业和某类公司已经超过了Java
  • 非面向对象范式(Non-object-oriented paradigms) 最值得注意的是函数式编程(FP),在这些新语言中也比较常见。甚至Java本身也引入了lambdas!元编程技术(增加和改变对象的方法和特性)等技术也越来越流行。还有拥有“软面向对象”特征的Go语言,它具有静态类型但是没有继承。所有的这些都表明了现代软件中类和集成没有过去重要。
  • 开源软件(Open-source software) 的扩散。早期,更多的通用软件是闭源(closed-source)人们使用的都是编译后的软件,现在通常人们依赖的软件都是开源的。因此,在编写库是曾经对必不可少的逻辑和数据隐藏不再哪么重要。
  • 微服务和软件即服务(Saas) 爆炸式地出现。与其将应用程序部署为将所有依赖项链接在一起的大型可执行文件,不如部署一个与其他服务(自己的活第三方提供支持的服务)调用的小型服务。

整体来看,SOLID真正关心的许多事情——例如类和接口,数据隐藏性和多态——不再是程序员每天都要处理的事情。

有什么没发生变化?

现在工业界有很多不一样了,但是也还有一些东西未发生改变。包括:

  • 代码是由人类编写和修改的 。代码被编写一次并且被读很多很多次。所以对内部和外部需要很好的代码说明文档,尤其很好的API文档。
  • 代码被组织成模块。在某些语言中,这些是类。在其他情况下,他们可能是单独的源文件。在JavaScript中,它们可能是导出对象。无论如何,总是存在某种方式去隔离和组织代码成为独立,有界的单元。因此,总是需要决定如何最好的将代码组织在一起。
  • 代码可以是内部的或者外部的。一些编写出来的代码是被你自己或者你的团队使用,一些可能会被其它团队甚至其他顾客通过API的方式使用。这也意味着需要某种方式来决定哪些代码是“可见的”哪些是隐藏的。

“现代”SOLID

在接下来的文章中,我将把SOLID中的每一项原则都表述为更一般的描述,并且说明是如何应用在OO,FP,多范式编程中的,并距离说明。在许多情况下,这些原则甚至可以应用在整个服务或者系统中。

需要注意的是我将使用“模块”代指一组代码,可以是类,包,文件等等。

单一职责 (Single responsibility principle)

原始定义: “一个类改变的原因不会超过一个”

如果您编写的类有很多关注点或“更改的原因”,那么这些关注点中任何一个需要更改,您就需要更改相同的代码。这增加了对一个特性功能的修改就会破坏另外一个特性功能的可能性。

一个例子,如下是一个永远不应该应用在生产环境的Franken-class:

1
2
3
4
5
6
7
8
9
10
11
12
13
class FrankenClass {
public void savaUserDetail(User user) {
//...
}

public void performOrder(Order order) {
//...
}

public void shipItem(Item item, String address) {
//...
}
}

新的定义: “每个模块应该做一件事,并且做好”。

这个原则和高内聚(high cohesion)话题紧密联系。本质上,您的代码不应该将很多角色或者用途混在一起。

如下是使用JavaScript的同一示例的函数式编程(FP)版本:

1
2
3
4
5
const saveUserDetails = (user) => {...}
const performOrder = (order) => { ...}
const shipItem = (item, address) => { ... }
export { saveUserDetails, performOrder, shipItem };
import { saveUserDetails, performOrder, shipItem } from "allActions";

这也适用在微服务设计;如果你有一个单独服务来处理所有这三个功能,它就会尝试做太多事情。

开闭原则(Open-closed principle)

原始定义: “软件实体应该对扩展开放,对修改关闭。”

这也是Java语言设计的一部分—你可以创建一个子类来继承一个类,但是不能去修改原始的类。

“对扩展开放”的原因之一是限制了对类作者的依赖——如果你需要改动一个类,你不得不去等待类的原始作者去修改,或者你深入研究这个类后再去修改。更重要的是这个类承担了太多的关注点这将打破单一职责原则。

“对修改关闭”的原因是我们不相信下游的使用者能够完全理解我们所有的“private”私有代码,我们希望保护它免收不熟练的人的修改带来的损害。

1
2
3
4
5
6
7
8
9
10
11
12
class Notifier {
public void notify(String message) {
//send an e-mail
}
}

class LoggingNotifier extends Notifier {
public void notify(String message) {
super.notify(message);//keep parent behaiver
//also log the message;
}
}

新定义: “应该能够在不重写模块的情况下使用和添加模块”。

这在面向对象领域是免费的(AOP)。在函数式编程代码必须明确定义Hook point以允许修改。这事一个示例,其中不仅仅允许使用前后hooks,而且甚至可以通过将函数传递给您的函数来覆盖其基本行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//library code

const saveRecord = (record, save, beforeSave, afterSave) => {
const defaultSave = (record) => {
//default save function;
}

if (beforeSave) beforeSave(record);
if (save) {
save(record);
} else {
defaultSave(record);
}
if(afterSave) afterSave(record);
//calling code
}

//calling code
const customSave = (record) => {...}
saveRecord(myRecord, customSave);

里氏替换原则

原始定义: “如果类型S是T的子类型,那么类型T可以被替换为S类型而不用修改程序的任何所需属性”。

这也是面向对象语言的基础属性。它意味着你可以使用任意子类替换它们的父类。所以你可以对这一个约定充满信心:你可以安全的使用任何 “is a” type T的对象而像T一样去使用。如下例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Vehicle {
public int getNumberOfWheels() {
return 4;
}
}

class Bicycle extends Vehicle {
public int getNumberOfWheels() {
return 2;
}
}

// calling code
public static int COST_PER_TIRE = 50;

public int tireCost(Vehicle vehicle) {
return COST_PER_TIRE * vehicle.getNumberOfWheels();
}

Bicycle bicycle = new Bicycle();
System.out.println(tireCost(bicycle)); //100

新定义: 你可以使用一个东西代替另一个东西只要它们声明的行为方式一样。

在动态语言中,重要的是如果你的程序“promises”去做某些事情(例如实现一个接口或者函数),你需要遵守你的“promises”不要给客户端不符合“promises”的东西。

许多动态语言使用“duck typing”(不知道什么意思 戳这儿 )去达到这一点。本质上,你的function正式或者非正式的声明它期望其输入以特定方式运行并根据该假设运行。

例如Ruby:

1
2
3
4
# @param input [#to_s]
def split_lines(input)
input.to_s.split("\n")
end

在这个例子中,这个function并不在乎输入类型——只关心它有一个to_s函数,它的行为方式与所有to_s函数的行为方式相同:即把输入变成字符串。许多动态语言没有办法去强制这种行为,因此这更像一种纪律问题而不是一种形式化技术。

接下来是函数式编程TypeScript例子,在这个例子中高阶函数引入了一个过滤器输入一个数字返回一个boolean值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const isEven = (x: number) : boolean => x % 2 == 0;
const isOdd = (x: number) : boolean => x % 2 == 1;

const printFiltered = (arr: number[], filterFunc: (int) => boolean) => {
arr.forEach((item) => {
if (filterFunc(item)) {
console.log(item);
}
})
}

const array = [1,2,3,4,5,6];
printFiltered(array, isEven);
printFiltered(array, isOdd);

接口隔离原则(Interface segregation principle)

原始定义: “许多客户端(client-specific)接口要好于一个通用的(general-purpose)接口。”

在面向对象语言中,你可以理解为是为你的类提供了一个“视图”(view)。与其提供给你一个大而全的实现给你的客户端,而是仅使用与该客户端相关的方法在它们之上创建接口,并要求您的客户端使用这些接口。

正如单一职责原则一样,接口隔离原则隔离了系统之间的耦合,并且确保客户端不需要了解它所依赖的无关功能。

如下例子通过SPR测试:

1
2
3
4
5
class PrintRequest {
public void createRequest() {}
public void deleteRequest() {}
public void workOnRequest() {}
}

这段代码通常只有一个“原因去更改”——它都与打印请求有关,它们都是同一个域的一部分,并且所有三种方法都可能会更改相同的状态。但是,创建请求的客户端不太可能是处理请求的客户端。将这些接口隔离开会更有意义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface PrintRequestModifier {
public void createRequest();
public void deleteRequest();
}

interface PrintRequestWorker {
public void workOnRequest()
}

class PrintRequest implements PrintRequestModifier, PrintRequestWorker {
public void createRequest() {}
public void deleteRequest() {}
public void workOnRequest() {}
}

新的定义: “不要向客户端展示它不需要看到的东西。”

只记录你客户端需要知道的内容。这意味着使用文档生成器只输入“public”function 或路由,而“private”没必要输出。

在微服务时代,你可以使用文档或者真正的隔离增加清晰度。例如,你外部客户可能只能以用户身份登录,但你的内部服务可能获取用户列表或者其他属性。你也可以创建一个单独的“仅限外部”用户服务来调用你的主服务,或者你可以只为隐藏内部路由的外部用户输出特定文档。

依赖倒置原则(Dependency inversion principle)

原始定义: “依赖抽象,而不是依赖具体实现”

在面向对象语言中,这意味着客户端应该尽可能依赖接口而不是具体实现类。这确保了代码应该依赖尽可能小的面积——事实上,客户端不必要依赖所有的代码,只需要依赖一个定义代码应该如何表现的契约。和其它原则一样这降低了修改一处而导致破坏了其它功能的风险。下面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Logger {
public void write(String message);
}

class FileLogger implements Logger {
public void write(String message) {
//write to file
}
}

class StandardOutLogger implements Logger {
public void write(String message) {
//write to standard out
}
}

// call
public void doStuff(Logger logger) {
//do stuff
logger.write("some message");
}

如果你正在写的代码需要一个logger, 你不想去限制自己仅仅只是去写入文件中,因为你不在乎。你仅仅只需要调用 write方法而让具体实现去输出。

新的定义: “依赖抽象,而不是具体实现。”

对的,这个例子我将定义保留原样!保持东西依赖抽象依然是重要的,即使现代代码中的抽象机制不像严格的面向对象世界那样强大。

尤其,这和上面讨论的里氏替换原则是一样的。主要的区别是它没有默认实现。因此,该部分中涉及鸭子类型和钩子函数的讨论通用适用于依赖倒置。

你也可以使用抽象对于微服务。比如,你可以将服务之间的直接通信替换为消息总线活队列平台,例如Kafka或者其他消息中间件。这样允许服务将消息发送到单个通用位置,而无需关心哪个特定服务将接收这些消息并执行其任务。

总结

再次重新梳理“现代SOLID”一次:

  • 不要惊讶别人读到你的代码。
  • 不要惊讶别人使用你的代码。
  • 不要让阅读你代码的人感到迷惑。
  • 为你的代码使用合理的边界。
  • 使用正确的耦合级别——尘归尘,土归土。

好的代码就是好的代码——从未改变,SOLID也是,实践是坚实的基础。

原文连接: https://stackoverflow.blog/2021/11/01/why-solid-principles-are-still-the-foundation-for-modern-software-architecture/?utm_source=programmingdigest&utm_medium=web&utm_campaign=447

Comment and share

缅怀袁老

in Daily Life

今天听闻噩耗,袁隆平院士我们称之为袁老去世了享年 91 岁,随之朋友圈也炸锅了,大家都在缅怀,悼念。

不得不说袁老对人类的贡献是伟大的,他是一个高尚的人,纯粹的人,脱离了低级趣味的人。找到了能够为之奋斗一生的事业,并在历史的篇章中留下了属于自己的篇章。

我虽然还在自己家的一亩三分地上挣扎,但是偶尔还是会仰望星空,怎么说呢保持激情,不要锤头丧气就是很好的生活状态。

Comment and share

Netty减少内存使用技巧:

  1. 能使用基本类型就不使用包装类型

  2. 减少对象本身大小 -> 应该定义成类变量的不要定义为实例变量

  3. Zero-copy

  4. Netty内存池使用

    • 内存池/非内存池的默认选择及切换方式

      io.netty.channel.defaultChannelConfig#allocator

    • 内存池实现 io.netty.buffer.pooledDirectByteBuf

    • 对外内存/堆内内存的默认选择及切换方式

    • 对外内存的分配本质

Comment and share

粘包原因

  • 发送方每次写入数据 < 套接字缓冲区大小
  • 接受方读取套接字缓冲区数据不够及时

半包原因

  • 发送方每次写入数据 > 套接字缓冲区大小
  • 发送方的数据大于协议的MTU(Maximum Transmission Unit 最大传输单元)必须拆包

根本原因

TCP是流式协议,消息无边际

udp像邮寄包裹,虽然是一次运输多个,但是每个包裹都有“界限”,一个一个签收,所以无粘包,拆包问题。

解决方式

  1. 改成短链接:一次链接发送一个数据包
  2. 封装成数据帧(fram)
    1. 固定长度—空间浪费
    2. 分割符—内容里出现分隔符时需要转义
    3. 固定长度字段存储内容长度信息—长度理论上有限制,需提前预知可能的最大长度,从而定义长度占用字节数 推荐
    4. 其它 如json里的{}

netty解码方式

假设我们把解决粘包/半包的问题的常用三种解码器叫一次解码

  • 固定长度 FixedLengthFrameDecoder
  • 分割符 DelimiterBasedFrameDecoder
  • 固定长度字段存内容长度信心 LengthFieldBasedFrameDecoder LengthFieldPrepender

二次解码:将字节转换成为实际使用的对象

Comment and share

Copyrights © 2025 Topsion. All Rights Reserved.
Author's picture

Topsion

Fullstack Developer


Coder


Xi'an China