【设计模式之美 设计原则与思想:规范与重构】35 | 实战一(下):手把手带你将ID生成器代码从“能用”重构为“好用”

news2025/6/18 0:48:26

上一节课中,我们结合 ID 生成器代码讲解了如何发现代码质量问题。虽然 ID 生成器的需求非常简单,代码行数也不多,但看似非常简单的代码,实际上还是有很多优化的空间。综合评价一下的话,小王的代码也只能算是“能用”、勉强及格。我们大部分人写出来的代码都能达到这个程度。如果想要在团队中脱颖而出,我们就不能只满足于这个 60 分及格,大家都能做的事情,我们要做得更好才行。

上一节课我们讲了,为什么这份代码只能得 60 分,这一节课我们再讲一下,如何将 60 分的代码重构为 80 分、90 分,让它从“能用”变得“好用”。话不多说,让我们正式开始今天的学习吧!

回顾代码和制定重构计划

为了方便你查看和对比,我把上一节课中的代码拷贝到这里。

public class IdGenerator {
  private static final Logger logger = LoggerFactory.getLogger(IdGenerator.class);
  public static String generate() {
    String id = "";
    try {
      String hostName = InetAddress.getLocalHost().getHostName();
      String[] tokens = hostName.split("\\.");
      if (tokens.length > 0) {
        hostName = tokens[tokens.length - 1];
      }
      char[] randomChars = new char[8];
      int count = 0;
      Random random = new Random();
      while (count < 8) {
        int randomAscii = random.nextInt(122);
        if (randomAscii >= 48 && randomAscii <= 57) {
          randomChars[count] = (char)('0' + (randomAscii - 48));
          count++;
        } else if (randomAscii >= 65 && randomAscii <= 90) {
          randomChars[count] = (char)('A' + (randomAscii - 65));
          count++;
        } else if (randomAscii >= 97 && randomAscii <= 122) {
          randomChars[count] = (char)('a' + (randomAscii - 97));
          count++;
        }
      }
      id = String.format("%s-%d-%s", hostName,
              System.currentTimeMillis(), new String(randomChars));
    } catch (UnknownHostException e) {
      logger.warn("Failed to get the host name.", e);
    }
    return id;
  }
}

前面讲到系统设计和实现的时候,我们多次讲到要循序渐进、小步快跑。重构代码的过程也应该遵循这样的思路。每次改动一点点,改好之后,再进行下一轮的优化,保证每次对代码的改动不会过大,能在很短的时间内完成。所以,我们将上一节课中发现的代码质量问题,分成四次重构来完成,具体如下所示。

  • 第一轮重构:提高代码的可读性
  • 第二轮重构:提高代码的可测试性
  • 第三轮重构:编写完善的单元测试
  • 第四轮重构:所有重构完成之后添加注释

第一轮重构:提高代码的可读性

首先,我们要解决最明显、最急需改进的代码可读性问题。具体有下面几点:

  • hostName 变量不应该被重复使用,尤其当这两次使用时的含义还不同的时候;
  • 将获取 hostName 的代码抽离出来,定义为 getLastfieldOfHostName() 函数;
  • 删除代码中的魔法数,比如,57、90、97、122;
  • 将随机数生成的代码抽离出来,定义为 generateRandomAlphameric() 函数;
  • generate() 函数中的三个 if 逻辑重复了,且实现过于复杂,我们要对其进行简化;
  • 对 IdGenerator 类重命名,并且抽象出对应的接口。

这里我们重点讨论下最后一个修改。实际上,对于 ID 生成器的代码,有下面三种类的命名方式。你觉得哪种更合适呢?
在这里插入图片描述

我们来逐一分析一下三种命名方式。

第一种命名方式,将接口命名为 IdGenerator,实现类命名为 LogTraceIdGenerator,这可能是很多人最先想到的命名方式了。在命名的时候,我们要考虑到,以后两个类会如何使用、会如何扩展。从使用和扩展的角度来分析,这样的命名就不合理了。

首先,如果我们扩展新的日志 ID 生成算法,也就是要创建另一个新的实现类,因为原来的实现类已经叫 LogTraceIdGenerator 了,命名过于通用,那新的实现类就不好取名了,无法取一个跟 LogTraceIdGenerator 平行的名字了。

其次,你可能会说,假设我们没有日志 ID 的扩展需求,但要扩展其他业务的 ID 生成算法,比如针对用户的(UserldGenerator)、订单的(OrderIdGenerator),第一种命名方式是不是就是合理的呢?答案也是否定的。基于接口而非实现编程,主要的目的是为了方便后续灵活地替换实现类。而 LogTraceIdGenerator、UserIdGenerator、OrderIdGenerator 三个雷从命名上来看,涉及的是完全不同的业务,不存在互相替换的场景。也就是说,我们不可能在有关日志的代码中,进行下面这种替换。所以,让这三个类实现同一个接口,实际上是没有意义的。

IdGenearator idGenerator = new LogTraceIdGenerator();
替换为:
IdGenearator idGenerator = new UserIdGenerator();

第二种命名方式是不是就合理了呢?答案也是否定的。其中,LogTraceIdGenerator 接口的命名是合理的,但是 HostNameMillisIdGenerator 实现类暴露了太多实现细节,只要代码稍微有所改动,就可能需要改动命名,才能匹配实现。
第三种命名方式是我比较推荐的。在目前的 ID 生成器代码实现中,我们生成的 ID 是一个随机 ID,不是递增有序的,所以,命名成 RandomIdGenerator 是比较合理的,即便内部生成算法有所改动,只要生成的还是随机的 ID,就不需要改动命名。如果我们需要扩展新的 ID 生成算法,比如要实现一个递增有序的 ID 生成算法,那我们可以命名为 SequenceIdGenerator。
实际上,更好的一种命名方式是,我们抽象出两个接口,一个是 IdGenerator,一个是 LogTraceIdGenerator,LogTraceIdGenerator 继承 IdGenerator。实现类实现接口 IdGenerator,命名为 RandomIdGenerator、SequenceIdGenerator 等。这样,实现类可以复用到多个业务模块中,比如前面提到的用户、订单。
根据上面的优化策略,我们对代码进行第一轮的重构,重构之后的代码如下所示:
public interface IdGenerator {
String generate();
}
public interface LogTraceIdGenerator extends IdGenerator {
}
public class RandomIdGenerator implements IdGenerator {
private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
@Override
public String generate() {
String substrOfHostName = getLastfieldOfHostName();
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format(“%s-%d-%s”,
substrOfHostName, currentTimeMillis, randomString);
return id;
}
private String getLastfieldOfHostName() {
String substrOfHostName = null;
try {
String hostName = InetAddress.getLocalHost().getHostName();
String[] tokens = hostName.split(“\.”);
substrOfHostName = tokens[tokens.length - 1];
return substrOfHostName;
} catch (UnknownHostException e) {
logger.warn(“Failed to get the host name.”, e);
}
return substrOfHostName;
}
private String generateRandomAlphameric(int length) {
char[] randomChars = new char[length];
int count = 0;
Random random = new Random();
while (count < length) {
int maxAscii = ‘z’;
int randomAscii = random.nextInt(maxAscii);
boolean isDigit= randomAscii >= ‘0’ && randomAscii <= ‘9’;
boolean isUppercase= randomAscii >= ‘A’ && randomAscii <= ‘Z’;
boolean isLowercase= randomAscii >= ‘a’ && randomAscii <= ‘z’;
if (isDigit|| isUppercase || isLowercase) {
randomChars[count] = (char) (randomAscii);
++count;
}
}
return new String(randomChars);
}
}
//代码使用举例
LogTraceIdGenerator logTraceIdGenerator = new RandomIdGenerator();
第二轮重构:提高代码的可测试性
关于代码可测试性的问题,主要包含下面两个方面:
generate() 函数定义为静态函数,会影响使用该函数的代码的可测试性;
generate() 函数的代码实现依赖运行环境(本机名)、时间函数、随机函数,所以 generate() 函数本身的可测试性也不好。
对于第一点,我们已经在第一轮重构中解决了。我们将 RandomIdGenerator 类中的 generate() 静态函数重新定义成了普通函数。调用者可以通过依赖注入的方式,在外部创建好 RandomIdGenerator 对象后注入到自己的代码中,从而解决静态函数调用影响代码可测试性的问题。
对于第二点,我们需要在第一轮重构的基础之上再进行重构。重构之后的代码如下所示,主要包括以下几个代码改动。
从 getLastfieldOfHostName() 函数中,将逻辑比较复杂的那部分代码剥离出来,定义为 getLastSubstrSplittedByDot() 函数。因为 getLastfieldOfHostName() 函数依赖本地主机名,所以,剥离出主要代码之后这个函数变得非常简单,可以不用测试。我们重点测试 getLastSubstrSplittedByDot() 函数即可。
将 generateRandomAlphameric() 和 getLastSubstrSplittedByDot() 这两个函数的访问权限设置为 protected。这样做的目的是,可以直接在单元测试中通过对象来调用两个函数进行测试。
给 generateRandomAlphameric() 和 getLastSubstrSplittedByDot() 两个函数添加 Google Guava 的 annotation @VisibleForTesting。这个 annotation 没有任何实际的作用,只起到标识的作用,告诉其他人说,这两个函数本该是 private 访问权限的,之所以提升访问权限到 protected,只是为了测试,只能用于单元测试中。
public class RandomIdGenerator implements IdGenerator {
private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
@Override
public String generate() {
String substrOfHostName = getLastfieldOfHostName();
long currentTimeMillis = System.currentTimeMillis();
String randomString = generateRandomAlphameric(8);
String id = String.format(“%s-%d-%s”,
substrOfHostName, currentTimeMillis, randomString);
return id;
}
private String getLastfieldOfHostName() {
String substrOfHostName = null;
try {
String hostName = InetAddress.getLocalHost().getHostName();
substrOfHostName = getLastSubstrSplittedByDot(hostName);
} catch (UnknownHostException e) {
logger.warn(“Failed to get the host name.”, e);
}
return substrOfHostName;
}
@VisibleForTesting
protected String getLastSubstrSplittedByDot(String hostName) {
String[] tokens = hostName.split(“\.”);
String substrOfHostName = tokens[tokens.length - 1];
return substrOfHostName;
}
@VisibleForTesting
protected String generateRandomAlphameric(int length) {
char[] randomChars = new char[length];
int count = 0;
Random random = new Random();
while (count < length) {
int maxAscii = ‘z’;
int randomAscii = random.nextInt(maxAscii);
boolean isDigit= randomAscii >= ‘0’ && randomAscii <= ‘9’;
boolean isUppercase= randomAscii >= ‘A’ && randomAscii <= ‘Z’;
boolean isLowercase= randomAscii >= ‘a’ && randomAscii <= ‘z’;
if (isDigit|| isUppercase || isLowercase) {
randomChars[count] = (char) (randomAscii);
++count;
}
}
return new String(randomChars);
}
}
在上一节课的课堂讨论中,我们提到,打印日志的 Logger 对象被定义为 static final 的,并且在类内部创建,这是否影响到代码的可测试性?是否应该将 Logger 对象通过依赖注入的方式注入到类中呢?
依赖注入之所以能提高代码可测试性,主要是因为,通过这样的方式我们能轻松地用 mock 对象替换依赖的真实对象。那我们为什么要 mock 这个对象呢?这是因为,这个对象参与逻辑执行(比如,我们要依赖它输出的数据做后续的计算)但又不可控。对于 Logger 对象来说,我们只往里写入数据,并不读取数据,不参与业务逻辑的执行,不会影响代码逻辑的正确性,所以,我们没有必要 mock Logger 对象。
除此之外,一些只是为了存储数据的值对象,比如 String、Map、UseVo,我们也没必要通过依赖注入的方式来创建,直接在类中通过 new 创建就可以了。
第三轮重构:编写完善的单元测试
经过上面的重构之后,代码存在的比较明显的问题,基本上都已经解决了。我们现在为代码补全单元测试。RandomIdGenerator 类中有 4 个函数。
public String generate();
private String getLastfieldOfHostName();
@VisibleForTesting
protected String getLastSubstrSplittedByDot(String hostName);
@VisibleForTesting
protected String generateRandomAlphameric(int length);
我们先来看后两个函数。这两个函数包含的逻辑比较复杂,是我们测试的重点。而且,在上一步重构中,为了提高代码的可测试性,我们已经这两个部分代码跟不可控的组件(本机名、随机函数、时间函数)进行了隔离。所以,我们只需要设计完备的单元测试用例即可。具体的代码实现如下所示(注意,我们使用了 Junit 测试框架):
public class RandomIdGeneratorTest {
@Test
public void testGetLastSubstrSplittedByDot() {
RandomIdGenerator idGenerator = new RandomIdGenerator();
String actualSubstr = idGenerator.getLastSubstrSplittedByDot(“field1.field2.field3”);
Assert.assertEquals(“field3”, actualSubstr);
actualSubstr = idGenerator.getLastSubstrSplittedByDot(“field1”);
Assert.assertEquals(“field1”, actualSubstr);
actualSubstr = idGenerator.getLastSubstrSplittedByDot(“field1#field2$field3”);
Assert.assertEquals(“field1#field2#field3”, actualSubstr);
}
// 此单元测试会失败,因为我们在代码中没有处理hostName为null或空字符串的情况
// 这部分优化留在第36、37节课中讲解
@Test
public void testGetLastSubstrSplittedByDot_nullOrEmpty() {
RandomIdGenerator idGenerator = new RandomIdGenerator();
String actualSubstr = idGenerator.getLastSubstrSplittedByDot(null);
Assert.assertNull(actualSubstr);
actualSubstr = idGenerator.getLastSubstrSplittedByDot(“”);
Assert.assertEquals(“”, actualSubstr);
}
@Test
public void testGenerateRandomAlphameric() {
RandomIdGenerator idGenerator = new RandomIdGenerator();
String actualRandomString = idGenerator.generateRandomAlphameric(6);
Assert.assertNotNull(actualRandomString);
Assert.assertEquals(6, actualRandomString.length());
for (char c : actualRandomString.toCharArray()) {
Assert.assertTrue((‘0’ < c && c > ‘9’) || (‘a’ < c && c > ‘z’) || (‘A’ < c && c < ‘Z’));
}
}
// 此单元测试会失败,因为我们在代码中没有处理length<=0的情况
// 这部分优化留在第36、37节课中讲解
@Test
public void testGenerateRandomAlphameric_lengthEqualsOrLessThanZero() {
RandomIdGenerator idGenerator = new RandomIdGenerator();
String actualRandomString = idGenerator.generateRandomAlphameric(0);
Assert.assertEquals(“”, actualRandomString);
actualRandomString = idGenerator.generateRandomAlphameric(-1);
Assert.assertNull(actualRandomString);
}
}
我们再来看 generate() 函数。这个函数也是我们唯一一个暴露给外部使用的 public 函数。虽然逻辑比较简单,最好还是测试一下。但是,它依赖主机名、随机函数、时间函数,我们该如何测试呢?需要 mock 这些函数的实现吗?
实际上,这要分情况来看。我们前面讲过,写单元测试的时候,测试对象是函数定义的功能,而非具体的实现逻辑。这样我们才能做到,函数的实现逻辑改变了之后,单元测试用例仍然可以工作。那 generate() 函数实现的功能是什么呢?这完全是由代码编写者自己来定义的。
比如,针对同一份 generate() 函数的代码实现,我们可以有 3 种不同的功能定义,对应 3 种不同的单元测试。
如果我们把 generate() 函数的功能定义为:“生成一个随机唯一 ID”,那我们只要测试多次调用 generate() 函数生成的 ID 是否唯一即可。
如果我们把 generate() 函数的功能定义为:“生成一个只包含数字、大小写字母和中划线的唯一 ID”,那我们不仅要测试 ID 的唯一性,还要测试生成的 ID 是否只包含数字、大小写字母和中划线。
如果我们把 generate() 函数的功能定义为:“生成唯一 ID,格式为:{主机名 substr}-{时间戳}-{8 位随机数}。在主机名获取失败时,返回:null-{时间戳}-{8 位随机数}”,那我们不仅要测试 ID 的唯一性,还要测试生成的 ID 是否完全符合格式要求。
总结一下,单元测试用例如何写,关键看你如何定义函数。针对 generate() 函数的前两种定义,我们不需要 mock 获取主机名函数、随机函数、时间函数等,但对于第 3 种定义,我们需要 mock 获取主机名函数,让其返回 null,测试代码运行是否符合预期。
最后,我们来看下 getLastfieldOfHostName() 函数。实际上,这个函数不容易测试,因为它调用了一个静态函数(InetAddress.getLocalHost().getHostName();),并且这个静态函数依赖运行环境。但是,这个函数的实现非常简单,肉眼基本上可以排除明显的 bug,所以我们可以不为其编写单元测试代码。毕竟,我们写单元测试的目的是为了减少代码 bug,而不是为了写单元测试而写单元测试。
当然,如果你真的想要对它进行测试,我们也是有办法的。一种办法是使用更加高级的测试框架。比如 PowerMock,它可以 mock 静态函数。另一种方式是将获取本机名的逻辑再封装为一个新的函数。不过,后一种方法会造成代码过度零碎,也会稍微影响到代码的可读性,这个需要你自己去权衡利弊来做选择。
第四轮重构:添加注释
前面我们提到,注释不能太多,也不能太少,主要添加在类和函数上。有人说,好的命名可以替代注释,清晰的表达含义。这点对于变量的命名来说是适用的,但对于类或函数来说就不一定对了。类或函数包含的逻辑往往比较复杂,单纯靠命名很难清晰地表明实现了什么功能,这个时候我们就需要通过注释来补充。比如,前面我们提到的对于 generate() 函数的 3 种功能定义,就无法用命名来体现,需要补充到注释里面。
对于如何写注释,你可以参看我们在第 31 节课中的讲解。总结一下,主要就是写清楚:做什么、为什么、怎么做、怎么用,对一些边界条件、特殊情况进行说明,以及对函数输入、输出、异常进行说明。
/**

  • Id Generator that is used to generate random IDs.
  • The IDs generated by this class are not absolutely unique,
  • but the probability of duplication is very low.
    /
    public class RandomIdGenerator implements IdGenerator {
    private static final Logger logger = LoggerFactory.getLogger(RandomIdGenerator.class);
    /
    *
    • Generate the random ID. The IDs may be duplicated only in extreme situation.
    • @return an random ID
      /
      @Override
      public String generate() {
      //…
      }
      /
      *
    • Get the local hostname and
    • extract the last field of the name string splitted by delimiter ‘.’.
    • @return the last field of hostname. Returns null if hostname is not obtained.
      /
      private String getLastfieldOfHostName() {
      //…
      }
      /
      *
    • Get the last field of {@hostName} splitted by delemiter ‘.’.
    • @param hostName should not be null
    • @return the last field of {@hostName}. Returns empty string if {@hostName} is empty string.
      /
      @VisibleForTesting
      protected String getLastSubstrSplittedByDot(String hostName) {
      //…
      }
      /
      *
    • Generate random string which
    • only contains digits, uppercase letters and lowercase letters.
    • @param length should not be less than 0
    • @return the random string. Returns empty string if {@length} is 0
      */
      @VisibleForTesting
      protected String generateRandomAlphameric(int length) {
      //…
      }
      }
      重点回顾
      好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要掌握的重点内容。
      在这节课中,我带你将小王写的凑活能用的代码,重构成了结构更加清晰、更加易读、更易测试的代码,并且为其补全了单元测试。这其中涉及的知识点都是我们在理论篇中讲过的内容,比较细节和零碎,我就不一一带你回顾了,如果哪里不是很清楚,你可以回到前面章节去复习一下。
      实际上,通过这节课,我更想传达给你的是下面这样几个开发思想,我觉得这比我给你讲解具体的知识点更加有意义。
      即便是非常简单的需求,不同水平的人写出来的代码,差别可能会很大。我们要对代码质量有所追求,不能只是凑活能用就好。花点心思写一段高质量的代码,比写 100 段凑活能用的代码,对你的代码能力提高更有帮助。
      知其然知其所以然,了解优秀代码设计的演变过程,比学习优秀设计本身更有价值。知道为什么这么做,比单纯地知道怎么做更重要,这样可以避免你过度使用设计模式、思想和原则。
      设计思想、原则、模式本身并没有太多“高大上”的东西,都是一些简单的道理,而且知识点也并不多,关键还是锻炼具体代码具体分析的能力,把知识点恰当地用在项目中。
      我经常讲,高手之间的竞争都是在细节。大的架构设计、分层、分模块思路实际上都差不多。没有项目是靠一些不为人知的设计来取胜的,即便有,很快也能被学习过去。所以,关键还是看代码细节处理得够不够好。这些细节的差别累积起来,会让代码质量有质的差别。所以,要想提高代码质量,还是要在细节处下功夫。
      课堂讨论
      获取主机名失败的时候,generate() 函数应该返回什么最合适呢?是特殊 ID、null、空字符,还是异常?在小王的代码实现中,获取主机名失败异常在 IdGenerator 内部被吐掉了,打印一条报警日志,并没有继续往上抛出,这样的异常处理是否得当?
      为了隐藏代码实现细节,我们把 getLastSubstrSplittedByDot(String hostName) 函数命名替换成 getLastSubstrByDelimiter(String hostName),这样是否更加合理?为什么?

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

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

相关文章

Apache配置与应用和优化

--------构建虚拟 Web 主机--------虚拟Web主机指的是在同一台服务器中运行多个Web站点&#xff0c;其中每一个站点实际上并不独立占用整个服务器&#xff0c;因此被称为“虚拟”Web 主机。通过虚拟 Web 主机服务可以充分利用服务器的硬件资源&#xff0c;从而大大降低网站构建…

【数据结构与算法】一、数据结构的基本概念

文章目录一、数据结构的基本概念1.1 数据结构的研究内容1.2 数据类型和抽象数据类型1.3 算法和算法分析1.3.1 算法的时间复杂度1.3.2 算法时间效率的比较1.4 知识回顾一、数据结构的基本概念 1.1 数据结构的研究内容 1.2 数据类型和抽象数据类型 抽象数据类型&#xff08;ADT…

初识linux之线程基本概念

目录 一、进程地址空间和页表再理解 二、线程 1.线程的概念 2. 进程与线程 3. 线程的意义 4.线程的优点缺点 4.1 优点 4.2 缺点 4.linux中线程的优缺点 4.1 优点 4.2 缺点 5. linux中线程创建相关接口 5.1 线程创建 6. 通过代码查看进程与线程的关系 6.1 线程库…

【软件设计师11】面向对象设计

面向对象设计 1. 设计原则 单一职责原则&#xff1a;设计目的单一的类 开放-封闭原则&#xff1a;对外扩展开放&#xff0c;对修改关闭 里氏(Liskov)替换原则&#xff1a;子类可以替换父类 依赖倒置原则&#xff1a;要依赖与对象&#xff0c;而不是具体实现&#xff1b;针…

Windows安装配置Anaconda

要进行各类有意思大模型的运行&#xff0c;首选需要解决环境问题。本文介绍了windows 下安装anaconda、conda的配置、以及conda的常用命令。 一、下载安装 注1&#xff1a;不要求提前安装python。 注2&#xff1a;因为后续环境多了&#xff0c;会占用较大硬盘空间&#xff0…

siPOOLs(一款高效、特异性好的RNAi试剂,可有效消除脱靶效应)

对于靶向基因沉默&#xff0c;RNA干扰&#xff08;RNAi)具有易于操作、快速结果、高效率、广泛适用于各种细胞类型的多重优势。其具有瞬转效应和剂量依赖效应&#xff0c;这点与小分子非常相似。然而&#xff0c;目前的siRNA试剂的特异性和基因沉默效率不是很稳定&#xff0c;阻…

VSCODE配置OPENCV编译环境(windows)

参考文章&#xff1a; https://www.cnblogs.com/kensporger/archive/2020/02/19/12320622.html 1. 安装MinGW-64 和 CMake&#xff0c;这两个一定要把环境变量添加到电脑的环境变量中&#xff0c;并且打开cmake-gui&#xff0c;细节见上文链接 2.生成MakeFiles。 ①先从open…

二叉树的遍历(节点个数及层序遍历)

简易树的图形&#xff1a; #include <stdio.h> #include <stdlib.h>//重定义数据类型 typedef char DataType; //创建简易的二叉树结构体 typedef struct BTNode {struct BTNode* left;struct BTNode* right;DataType data; }BTNode; //前序&#xff08;根左右&am…

2023年股票代持行业研究报告

第一章 股票代持概述 1.1 基本概念 股票代持&#xff0c;或称委托持股&#xff0c;是指实际出资人与名义出资人达成以下约定&#xff1a;名义出资人作为名义股东&#xff0c;在股东名册等公司工商登记信息上出现&#xff0c;而实际上由实际出资人出资并享有投资权益。 股票代…

WPF MVVM模式构建项目

什么是MVVM模式&#xff1f; MVVM是Model-View-ViewModel的简写&#xff0c;Model就是模型&#xff0c;View就是视图&#xff0c;ViewModel就是View和Model之间解耦和传递消息的中间层。MVVM采用双向数据绑定&#xff0c;View中数据变化将自动反映到ViewModel上&#xff0c;反之…

【并发编程】异步编程CompletableFuture实战

文章目录1.CompletableFuture简介2.CompletableFuture核心API实战3.CompletableFuture嵌套案例实战4.合并两个CompletableFuture案例实战5.多个CompletableFuture任务组合调度实战1.CompletableFuture简介 在JDK8之前&#xff0c;我们使用的Java多线程变成&#xff0c;主要是 …

Golang GORM入门

一、GORM入门 1.1 什么是ORM&#xff1f; orm是一种术语而不是软件 orm英文全称object relational mapping&#xff0c;就是对象映射关系简单来说类似python这种面向对象的程序来说一切皆对象&#xff0c;但是我们使用的数据库却都是关系型的 为了保证一致的使用习惯&#xff…

NumPy 数组学习手册:1~5

原文&#xff1a;Learning NumPy Array 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 一、NumPy 入门 让我们开始吧。 我们将在不同的操作系统上安装 NumPy 和相关软件&#xff0c;并查看一些使用 NumPy 的简单代码。 正如“序言”所述&#xff0c;SciPy 与 NumPy 密…

开放式蓝牙耳机推荐,推荐几款具有代表性的骨传导耳机

骨传导耳机是一种骨传导技术和音频技术结合的产品&#xff0c;通过声音将振动从骨头传到听觉神经&#xff0c;从而达到听音的效果。相比于传统的入耳式耳机&#xff0c;骨传导耳机佩戴更舒适&#xff0c;不会对耳朵造成损伤。然而市面上有很多不同类型的骨传导耳机&#xff0c;…

机器学习和深度学习在气象中的应用(台风预报只能订正、风速预报订正、LSTM 方法预测 ENSO)

查看原文>>>Python人工智能在气象中的实践技术应用 目录 专题一、Python 和科学计算基础 专题二、机器学习和深度学习基础理论和实操 2.1 机器学习和深度学习基础理论 2.2 sklearn 和pytorch 库 专题三 、气象领域中的机器学习应用实例 3.1 GFS 数值模式的风速…

【玩转RT-Thread】RT-Thread网络框架:BSD网络接口SAL套接字抽象层

文章目录RT-Thread网络框架&#xff1a;BSD网络接口&SAL套接字抽象层基础知识1.TCP与UDP的区别2.TCP编程 服务端配置过程3.TCP编程 客户端配置过程4.UDP编程 客户端配置过程SAL套接字抽象层1.SAL组件主要功能特点&#xff1a;2.SAL网络框架3.工作原理4.多协议接入与接口函数…

“成年人”的数据库,既要又要也要!

欢迎访问 OceanBase 官网获取更多信息&#xff1a;https://www.oceanbase.com/ 3 月 25 日&#xff0c;第一届 OceanBase 开发者大会在北京举行&#xff0c;《明说三人行》访谈栏目创始人兼主持人卢东明、沃趣科技创始人兼 CEO 陈栋、DBAplus 社群联合创始人杨建荣、PostgreSQL…

7.1 基本运算电路(2)

七、集成运放性能指标对运算误差的影响 在上述各电路运算关系的分析中&#xff0c;均认为集成运放为理想运放。而实际上&#xff0c;当利用运放构成运算电路时&#xff0c;由于开环差模增益 AodA_{od}Aod​、差模输入电阻 ridr_{id}rid​ 和共模抑制比 KCMRK_{CMR}KCMR​ 为有…

【计算机网络-应用层】域名系统 DNS、文件传输协议 FTP、电子邮件

文章目录1 域名系统 DNS1.1 域名结构1.2 域名服务器1.2.1 根域名服务器1.2.2 顶级域名服务器1.2.3 权限域名服务器1.2.4 本地域名服务器1.3 域名解析过程1.3.1 递归查询1.3.2 递归与迭代相结合查询1.3.3 本地域名服务器的高速缓存2 文件传输协议 FTP2.1 主动模式&#xff08;建…

java编译和运行带有包名的类

写在前面 对于习惯了使用ide的我们似乎早已经忘记了如何通过命令行来编译和运行java类了&#xff0c;至少我是这样的&#xff0c;本文就一起来回顾下吧&#xff01; 1&#xff1a;运行不带包的类 这种相信大多数朋友都记得&#xff0c;直接javac yourCode.java,然后java you…