Day956.代码现代化 -遗留系统现代化实战

news2024/12/23 15:01:30

代码现代化

Hi,我是阿昌,今天学习记录的是关于代码现代化的内容。

代码现代化的主要模式。

大体的脉络是这样的:

  1. 先对代码做可测试化重构,并添加测试;
  2. 在测试的保护下,安全地重构;
  3. 在测试的保护下,将代码分层。

先来看看如何让代码变得可测,这是遗留系统现代化的基本功,希望重视起来。

一个软件的自动化测试,可以从内部表达这个软件的质量,通常管它叫做内建质量(Build Quality In)。

然而国内的开发人员普遍缺乏编写自动化测试的能力,一方面是认为没什么技术含量,另一方面是觉得质量是测试人员的工作,与自己无关

然而有没有想过,正是因为这样的误区,才导致软件的质量越来越差,系统也一步步沦为了遗留系统。


一、你的代码可测吗?

先来看看不可测的代码都长什么样,分析出它们不可测的原因,再“按方抓药”。可测的代码很相似,而不可测的代码各有各的不可测之处。

在自动化测试举过一个不可测代码的例子,现在来一起复习一下:

public class EmployeeService {
  public EmployeeDto getEmployeeDto(long employeeId) {
    EmployeeDao employeeDao = new EmployeeDao();
    // 访问数据库获取一个Employee
    Employee employee = employeeDao.getEmployeeById(employeeId);
    // 其他代码  
  }
}

这段代码之所以不可测,是因为在方法内部直接初始化了一个可以访问数据库的 Dao 类,要想测试这个方法,就必须访问数据库了。

倒不是说所有测试都不能连接数据库,但大多数直连数据库的测试跑起来都太慢了,而且数据的准备也会相当麻烦。

这属于不可测代码的第一种类型:在方法中构造了不可测的对象


再来看一个例子,与上面的代码非常类似:

public class EmployeeService {
  public EmployeeDto getEmployeeDto(long employeeId) {
    // 访问数据库获取一个Employee
    Employee employee = EmploeeDBHelper.getEmployeeById(employeeId);
    // 其他代码
  }
}

这段代码同样是不可测的,它在方法中调用了不可测的静态方法,因为这个静态方法跟前面的实例方法一样,也访问了数据库。

除了不能在测试中访问真实数据库以外,也不要在测试中访问其他需要部署的中间件、服务等,它们也会给测试带来极大的不便。

在测试中,通常把被测的元素(可能是组件、类、方法等)叫做 SUT(System Under Test),把 SUT 所依赖的组件叫做 DOC(Depended-on Component)。

导致 SUT 无法测试的原因,通常都是 DOC 在当前的测试上下文中不可用


DOC 不可用的原因通常有三种:

  1. 不能访问。比如 DOC 访问了数据库或其他需要部署的中间件、服务等,而本地环境没有这些组件,也很难部署这些组件。
  2. 不是当前测试期望的返回值。即使本地能够访问这些组件,但它们却无法返回我们想要的值。比如我们想要获取 ID 为 1 的员工信息,但数据库中却并没有这条数据。
  3. 执行这些 DOC 的方法会引发不想要的副作用。比如更新数据库,会破坏已有数据,影响其他测试。另外连接数据库,还会导致测试执行时间变长,这也是一种副作用。

要让 SUT 可测,就得让 DOC 可用,有哪些办法呢?


二、如何让代码变得可测?

要让 DOC 的行为可变。这种变化可以让 DOC 在测试时不再直接访问数据库或其他组件,从而变得“可以访问”、“能返回期望的值”以及“不会产生副作用”。

如何才能让 DOC 的行为可变呢?如果 DOC 是静态的或是在 SUT 内构造的,那自然不可改变。所以,要让 DOC 的构造和 SUT 本身分离,SUT 只需使用外部构造好的 DOC 的实例,而不用关心它的构造。

这种可以让 DOC 的行为发生改变的位置,叫做接缝(seam),这是 Michael Feathers 在《修改代码的艺术》这本书里提出来的。

接缝这个隐喻非常形象,如果是一整块没有接缝的布料,就无法做任何改动,它始终就是一块平面的布料。

有了接缝,不但可以连接不同的布料,还可以改变一块布的方向,从平面变得立体。

有了接缝,身为“裁缝”才可以充分发挥想象力,制作出各种丰富多彩的成品。

把这种在 SUT 中创建接缝从而使 SUT 变得可测的方式,叫做提取接缝模式

想要用好这个模式,需要了解何处下剪子,针法选什么合适。

也就是接缝的位置和类型,下面就结合代码例子分别看看。


三、接缝的位置

提取接缝模式的应用,也就是把 EmployeeDao 提取成 EmployeeService 的字段,并通过 EmployeeService 的构造函数注入进来。

public class EmployeeService {
  private EmployeeDao employeeDao;
  public EmployeeService(EmployeeDao employeeDao) {
    this.employeeDao = employeeDao;
  }

  public EmployeeDto getEmployeeDto(long employeeId) {
    Employee employee = employeeDao.getEmployeeById(employeeId);
    // 其他代码
  }
}

除了构造函数,接缝也可以位于方法参数中,即:

public class EmployeeService {
  public EmployeeDto getEmployeeDto(long employeeId, EmployeeDao employeeDao) {
    Employee employee = employeeDao.getEmployeeById(employeeId);
    // 其他代码
  }
}

如果使用了依赖注入工具(比如 Spring),也可以给字段加 @Autowired 注解,这样接缝的位置就成了字段。

对于这三种接缝位置,更倾向于构造函数,因为它更方便,而且与具体的工具无关。

学习完接缝的位置,再来看看接缝的类型。


四、接缝的类型

接缝的类型是指,通过什么样的方式来改变 DOC 的行为。

提取完接缝后,创建了一个 EmployeeDao 的子类,这个子类重写了 getEmployeeById 的默认行为,从而让这个 DOC 返回了“期望的值”。

public class InMemoryEmployeeDao extends EmployeeDao {
  @Override
  public Employee getEmployeeById(long employeeId) {
    return null;
  }
}

把这种通过继承 DOC 来改变默认行为的接缝类型叫做对象接缝

除此之外,还可以将原来的 EmployeeDao 类重命名为 DatabaseEmployeeDao,并提取出一个 EmployeeDao 接口。

然后再让 InMemoryEmployeeDao 类实现 EmployeeDao 接口。

public interface EmployeeDao {
    Employee getEmployeeById(long employeeId);
}

在 EmployeeService 中,仍然通过构造函数来提供这个接缝,代码基本上可以保持不变。

这样,和对象接缝一样,只需要在构造 EmployeeService 的时候传入 InMemoryEmployeeDao 就可以改变默认的行为,之后的测试也更方便。

这种通过将 DOC 提取为接口,并用其他实现类来改变默认行为的接缝类型,就叫做接口接缝

如果代码依赖的是一个接口,那么这种依赖或者说耦合就是很松散的。

在接口本身不发生改变的前提下,不管是修改实现了该接口的类,还是添加了新的实现类,使用接口的代码都不会受到影响。


五、新生和外覆

提取了接缝,你就可以对遗留代码添加测试了。

但这时可能会说,虽然接缝很好,但是很多复杂的代码依赖了太多东西,一个个都提取接缝的话,需要很长时间,但无奈工期太紧,不允许这么做啊。

不要紧,《修改代码的艺术》中还介绍了两种不用提取接缝就能编写可测代码的模式,也就是新生(sprout)外覆(wrap)


假设有这样一段代码,根据传入的开始和结束时间,计算这段时间内所有员工的工作时间:

public class EmployeeService {
    public Map<Long, Integer> getWorkTime(LocalDate startDate, LocalDate endDate) {
        EmployeeDao employeeDao = new EmployeeDao();
        List<Employee> employees = employeeDao.getAllEmployees();
        Map<Long, Integer> workTimes = new HashMap<>();
        for(Employee employee : employees) {
            WorkTimeDao workTimeDao = new WorkTimeDao();
            int workTime = workTimeDao.getWorkTimeByEmployeeId(employee.getEmployeeId(), startDate, endDate);
            workTimes.put(employee.getEmployeeId(), workTime);
        }
        return workTimes;
    }
}

知道这段代码有很多槽点,但更痛的现实状况是:根本没有时间去优化,因为新的需求已经来了,并且明天就要提测。

需求是这样的,业务人员拿到工时的报表后发现,有很多员工的工时都是 0,原来他们早就离职了。

现在要求修改一下代码,过滤掉那些离职的员工。如果不需要写测试,这样的需求对来说就是小事一桩,一定轻车熟路。


可以在 EmployeeDao 中添加一个新的查询数据库的方法 getAllActiveEmployees,只返回在职的 Employee。

也可以仍然使用 getAllEmployees,并在内存中进行过滤

public class EmployeeService {
    public Map<Long, Integer> getWorkTime(LocalDate startDate, LocalDate endDate) {
        EmployeeDao employeeDao = new EmployeeDao();
        List<Employee> employees = employeeDao.getAllEmployees()
                .stream()
                .filter(e -> e.isActive())
                .collect(toList());
        // 其他代码
    }
}

这样的修改不仅在遗留系统中,即使在所谓的新系统中,也是十分常见的。需求要求加一个过滤条件,那就加一个过滤条件就好了。

然而,这样的代码仍然是不可测的,加了几行代码,但加的代码也是不可测的,系统没有因代码而变得更好,反而更糟了。

更好的做法是添加一个新生方法,去执行过滤操作,而不是在原来的方法内去过滤。

public class EmployeeService {
    public Map<Long, Integer> getWorkTime(LocalDate startDate, LocalDate endDate) {
        EmployeeDao employeeDao = new EmployeeDao();
        List<Employee> employees = filterInactiveEmployees(employeeDao.getAllEmployees());
        // 其他代码
    }
    public List<Employee> filterInactiveEmployees(List<Employee> employees) {
        return employees.stream().filter(e -> e.isActive()).collect(toList());
    }
}

这样一来,新生方法是可测的,可以对它添加测试,以验证过滤逻辑的正确性。

原来的方法虽然仍然不可测,但也没有让它变得更糟。


除了新生,还可以使用外覆的方式来让新增加的功能可测。

比如下面这段计算员工薪水的代码。

public class EmployeeService {
    public BigDecimal calculateSalary(long employeeId) {
        EmployeeDao employeeDao = new EmployeeDao();
        Employee employee = employeeDao.getEmployeeById();
        return SalaryEngine.calculateSalaryForEmployee(employee);
    }
}

如果现在要添加一个新的功能,有些调用端在计算完薪水后,需要立即给员工发短信提醒,而且其他调用端则保持不变。

脑子里可能有无数种实现方式,但最简单的还是直接在这段代码里添加一个新生方法,用来通知员工。

public class EmployeeService {
    public BigDecimal calculateSalary(long employeeId, bool needToNotify) {
        EmployeeDao employeeDao = new EmployeeDao();
        Employee employee = employeeDao.getEmployeeById();
        BigDecimal salary = SalaryEngine.calculateSalaryForEmployee(employee);
        notifyEmployee(employee, salary, needToNotify);
        return salary;
    }
}

这的确非常方便,但将 needToNotify 这种标志位一层层地传递下去,是典型的代码坏味道FlagArgument。

也可以在调用端根据情况去通知员工,但那样对调用端的修改又太多太重,是典型的霰弹式修改。

最好的方式是在原有方法的基础上外覆一个新的方法 calculateSalaryAndNotify,它会先调用原有方法,然后再调用通知方法

public BigDecimal calculateSalary(long employeeId) {
    // ...
}
public BigDecimal calculateSalaryAndNotify(long employeeId) {
    BigDecimal salary = calculateSalary(employeeId);
    notifyEmployee(employeeId, salary);
    return salary;
}
public void notifyEmployee(long employeeId, BigDecimal salary) {
    // 通知员工
}

通过这样的修改,调用端只需要根据情况选择调用哪个方法即可,这样的改动量最少。

同时还可以单独测试 notifyEmployee,以确保这部分逻辑是正确的。通过新生和外覆两种模式,新编写的代码就是可测的了。

通过提取接缝,旧代码的可测试化重构也可以基本搞定。

接下来,将通过构造函数注入和接口接缝演示一下,如何为这个 EmployeeService 编写测试。


六、为代码添加测试

先来回顾一下现在 EmployeeService 的完整代码:

public class EmployeeService {
    private EmployeeDao employeeDao;
    public EmployeeService(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }
    public EmployeeDto getEmployeeDto(long employeeId) {
        Employee employee = employeeDao.getEmployeeById(employeeId);
        if (employee == null) {
            throw new EmployeeNotFoundException(employeeId);
        }
        return convertToEmployeeDto(employee);
    }
}

要添加的测试是当 EmployeeDao 的 getEmployeeById 方法返回一个 null 的时候,EmployeeService 的 getEmployeeDto 方法会抛出一个异常。

public class EmployeeServiceTest {
    @Test
    public void should_throw_employee_not_found_exception_when_employee_not_exists() {
        EmployeeService employeeService = new EmployeeService(new InMemoryEmployeeDao());
        EmployeeNotFoundException exception = assertThrows(EmployeeNotFoundException.class,
            () -> employeeService.getEmployeeDto(1L));
        assertEquals(exception.getEmployeeId(), 1L);
    }
}

在测试中使用的 InMemoryEmployeeDao,实际上就是一种测试替身(Test Double)。但是它只返回了 null,有点单一,想测试正常的情况就没法用了。

如果想让这个方法返回不同的值,再添加一个 EmployeeDao 的实现着实有点麻烦。

这时可以使用 Mock 框架,让它可以针对不同的测试场景返回不同的值。

@Test
public void should_return_correct_employee_when_employee_exists() {
    EmployeeDao mockEmployeeDao = Mockito.mock(EmployeeDao.class);
    when(mockEmployeeDao.getEmployeeById(1L)).thenReturn(givenEmployee("John Smith"));
    EmployeeService employeeService = new EmployeeService(mockEmployeeDao);
    EmployeeDto employeeDto = employeeService.getEmployeeDto(1L);
    
    assertEquals(1L, employeeDto.getEmployeeId());
    assertEquals("John Smith", employeeDto.getName());
}

这里使用了 Mockito 这个 Java 中最流行的 Mock 框架。

好了,代码也可测了,也知道怎么写测试了,那么应该按什么样的思路去添加测试呢?

上面这种简单的例子,相信你肯定是知道要怎么加测试,但是遗留系统中的那些“祖传”代码真的是什么样的都有,对于这种复杂代码,应该怎么去添加测试呢?


七、决策表模式

以著名的镶金玫瑰重构道场的代码为例,来说明如何为复杂遗留代码添加测试。


public void updateQuality() {
   for (int i = 0; i < items.length; i++) {
       if (!items[i].name.equals("Aged Brie")
               && !items[i].name.equals("Backstage passes to a TAFKAL80ETC concert")) {
           if (items[i].quality > 0) {
               if (!items[i].name.equals("Sulfuras, Hand of Ragnaros")) {
                   items[i].quality = items[i].quality - 1;
               }
           }
       } else {
           if (items[i].quality < 50) {
               items[i].quality = items[i].quality + 1;

               if (items[i].name.equals("Backstage passes to a TAFKAL80ETC concert")) {
                   if (items[i].sellIn < 11) {
                       if (items[i].quality < 50) {
                           items[i].quality = items[i].quality + 1;
                       }
                   }

                   if (items[i].sellIn < 6) {
                       if (items[i].quality < 50) {
                           items[i].quality = items[i].quality + 1;
                       }
                   }
               }
           }
       }

       if (!items[i].name.equals("Sulfuras, Hand of Ragnaros")) {
           items[i].sellIn = items[i].sellIn - 1;
       }

       if (items[i].sellIn < 0) {
           if (!items[i].name.equals("Aged Brie")) {
               if (!items[i].name.equals("Backstage passes to a TAFKAL80ETC concert")) {
                   if (items[i].quality > 0) {
                       if (!items[i].name.equals("Sulfuras, Hand of Ragnaros")) {
                           items[i].quality = items[i].quality - 1;
                       }
                   }
               } else {
                   items[i].quality = items[i].quality - items[i].quality;
               }
           } else {
               if (items[i].quality < 50) {
                   items[i].quality = items[i].quality + 1;
               }
           }
       }
   }
}

这是非常典型的遗留代码,if/else 满天飞,可谓眼花缭乱;而且分支的规则不统一,有的按名字去判断,有的按数量去判断。对于这种分支条件较多的代码,可以梳理需求文档(如果有的话)和代码,找出所有的路径,根据每个路径下各个字段的数据和最终的值,制定一张决策表,如下图所示。

在这里插入图片描述

比如第一行,要测的是,系统每天会自动给所有商品的保质期和品质都减 1,那么给出的条件是商品类型为 normal,保质期为 4 天,品质为 1,所期望的行为是保质期和品质都减少 1。而第二行则是测试,当保质期减为 0 之后,品质会双倍地减少。

以此类推,一共梳理出 18 个测试场景。这种决策表不但清晰地提供了所有测试用例,而且给出了相应的数据,可以很轻松地基于它来构建整个方法的测试。


八、测试的类型和组织

说完了如何添加测试,接下来看看可以添加哪些类型的测试。

Mike Cohn 在十几年前曾经提出过著名的“测试金字塔”理论,将测试划分为三个层次。

从上到下分别是:

  • UI 测试
  • 服务测试
  • 单元测试。

它们累加在一起,就像一个金字塔一样。

需要指出的是,遗留系统很难应用测试金字塔,但还是有必要先来看看这三层测试都包含哪些内容。

在这里插入图片描述

其中,最顶层的 UI 测试是指从页面点击到数据库访问的端到端测试,用自动化的方式去模拟用户或测试人员的行为。

早年间的所谓自动化测试大多都属于这种。但这样的测试十分不稳定,一个简单的页面调整就可能导致测试失败。

尽管现在很多工具推出了 headless 的方式,可以绕开页面,但它们仍然运行得很慢。而且还需要与很多服务、工具集成起来,环境的搭建也是个问题。

所以 UI 测试位于测试金字塔的顶端,即只需要少量的这种测试,来验证服务和中间件等相互之间的访问是正常的。

需要指出的是,UI 测试并不是针对前端元素或组件的测试。后者其实是前端的单元测试。

中间这层的服务测试也是某种意义的端到端测试,但它避开了 UI 的复杂性,而是直接去测试 UI 会访问的 API。

也有人管这种测试叫集成测试。它可以直接访问数据库,也可以用 H2 或 SQLite 等文件或内存数据库代替;

它也可以直接访问其他服务,也可以用Moco等工具来模拟服务的行为。

这种测试的好处是,可以测试 API 级别的端到端行为,不管内部的代码如何重构,只要 API 的契约保持不变,测试就不需要修改。

最底层的单元测试就是对某个方法的测试,平时开发同学写的大多是这方面的测试。


测试金字塔的建议是,尽可能多地写单元测试,它编写成本低、运行速度快,是整个测试金字塔的基座。

对于方法内用到的 DOC,可以用测试替身来替换。对于在多大程度上使用测试替身,有两种不同的观点。

  • 一种观点认为不管任何依赖都应该使用测试替身来代替;

  • 一种观点则认为只要 DOC 不访问数据库、不访问文件系统、不访问进程外的服务或中间件,就可以不用测试替身。

前者的学名叫 solitary unit test,管它叫“社恐症单元测试”;后者学名叫做 sociable unit test,管它叫“交际花单元测试”。

到底应该使用哪种类型的单元测试呢?这一点并没有定论,支持每种类型的人都不少。

个人更倾向于交际花单测,因为这样写起来更容易,而且对重构很友好。


九、遗留系统中的测试策略

学完了测试金字塔,是不是已经准备按照由少到多的关系,在遗留系统中补测试了呢?

先别急,遗留代码有很多特点,导致它们并不适合完全应用测试金字塔来组织测试。

首先,遗留系统的很多业务逻辑位于数据库的存储过程或函数中,代码只是用来传递参数而已。这样一来单元测试根本测不到什么东西。也不能在服务测试(或 API 测试)中使用内存数据库,因为要在这些数据库中复制原数据库中的存储过程或函数,可能会有很多语法不兼容。

其次,遗留系统的很多业务还位于前端页面中,位于 JSP、PHP、ASP 的标签之中。这部分逻辑也是没法用单元测试来覆盖的。而服务测试脱离了页面,显然也无法覆盖。

因此,如果遗留系统在前端和数据库中都有不少业务逻辑,就可以多写一些 UI 测试,它们可以端到端地覆盖这些业务逻辑。可以从系统中最重要的业务开始编写 UI 测试。

然而,UI 测试毕竟十分不稳定,运行起来也很慢,当数量上来后这些缺点就会被放大。这时候可以多编写一些服务测试。

对于数据库中的业务逻辑,可以搭建一些基础设施,让开发人员可以在测试中直连数据库,并方便地编写集成测试。

这些基础设施包括:

  • 一个数据库镜像,可以快速在本地或远端做初始化;需要将数据库容器化,满足开发和测试人员使用个人数据库的需求。
  • 一个数据复制工具,可以方便地从其他测试环境拉取数据到这个镜像,以方便数据的准备;可以考虑通过 CI/CD 来实现数据的复制。

除此之外,可能还需要一个数据对比工具,用来帮你在重构完代码后,比较数据库的状态。比如将一个存储过程或函数中的逻辑迁移到 Java 代码中的时候,要验证迁移的正确性,只跑通集成测试是远远不够的,还要全方位地比较数据库中相关表的数据,以防漏掉一些不起眼的地方。

对于前端中的业务逻辑,可以先重构这些逻辑,将它们迁移到后端中,然后再编写单元测试或服务测试。

这时的测试策略有点像一个钻石的形状。

在这里插入图片描述

确定好了测试的类型,还有一些测试编写方面。

  • 第一个细节是测试的命名。关于测试命名,不同书籍中都有不同的推荐,但更倾向于像如何降低认知负载中介绍的那样,用“实例化需求”的方式,从业务角度来命名测试,使得测试可以和代码一起演进,成为活文档。
  • 第二个细节是测试的组织。当测试变多时,如果不好好对测试进行分组,很快就会变得杂乱无章。这样的测试即使是活文档,也会增加认知负载。最好的方法是,将单个类的测试都放在同一个包中,将不同方法的测试放在单独的测试类里。而对于同一个方法,要先写它 Happy path 的测试,再写 Sad path。记住一个口诀:先简单,再复杂;先正常,再异常

也就是测试的场景要先从简单的开始,逐步递进到复杂的情况;而测试的用例要先写正常的 Case,再逐步递进到异常的 Case。


十、总结

首先学习了接缝的位置和接缝的类型。

接缝的位置是指那些可以让 DOC 的行为发生改变的位置,有构造函数、方法参数、字段三种;而接缝的类型则是说改变 DOC 行为的方式,包括对象接缝和接口接缝。

遗留代码中有了接缝,就可以着手写测试了。然而复杂的遗留代码很难去梳理清楚头绪,想你推荐用决策表的方式,将测试用例一一列出来。

在遗留系统中,如果存储过程中包含大量的业务逻辑,传统的金字塔型的测试策略可能并不适合,可以多写一些端到端的 UI 测试,以及与数据库交互的集成测试(服务测试)。这时的测试策略呈现一个钻石的形状。

在这里插入图片描述

自动化测试是代码不可或缺的一部分,忽视了测试,即使是新系统,也在向着遗留系统的不归路上冲刺。

然而技术社区对于测试的态度总是十分漠视,多年来不曾改观。

在某群里询问,是否有人愿意给一个开源框架添加测试,然而大家想的却是什么“技术进步性”。

在这里插入图片描述

开发人员如果忽视编写自动化测试,就放弃了将质量内建到软件(也就是自己证明自己质量)的机会,把质量的控制完全托付给了测试人员。这种靠人力去保证质量的方式,永远也不可能代表“技术先进性”。

有的时候可能觉得,就是写了一行代码,加不加测试无所谓吧?反正原来也没有测试。但是,希望不要这么想,更不要这么做。

犯罪心理学中有一个“破窗效应”,意思是说如果一栋楼有几扇窗户是破的,用不了几天所有的窗户都会破掉。这是一个加速熵增的过程,没有测试的系统,就是那座破了窗户的大楼。

要记住的是“童子军原则”,也就是当露营结束离开的时候,要打扫营地,让它比你来的时候更干净。

写了一行代码,并把这一行代码的测试加上,你就没有去打破一扇新的窗户,而是让系统比你写代码之前变得更好了。

这便是引入了一个负熵,让系统从无序向着有序迈出了一步。

莫以恶小而为之,莫以善小而不为。


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

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

相关文章

数据链路层相关知识

文章目录 一.数据链路层简介二.MAC地址为什么有了ip地址,我们还要使用mac地址呢? 三.以太网什么是以太网以太网封装帧格式认识MTU谈谈 MTU对IP协议的影响. 一.数据链路层简介 数据链路层的位置和角色:位于物理层和网络层之间,负责实现两个直接相连的节点(主机/路由器)之间的可…

SpringCloud源码之OpenFeign

OpenFeign 基于 OpenFeign 2.2.6.RELEASE版本进行源码阅读 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId><version>2.2.6.RELEASE</version> </dependen…

Linux_红帽8学习笔记分享_6(yum软件仓库技术)

Linux_红帽8学习笔记分享_6(yum软件仓库技术) 文章目录 Linux_红帽8学习笔记分享_6(yum软件仓库技术)1. RPM软件包的使用技巧1.1如何查询指定软件包是否安装1.2如何删除指定软件包1.3如何安装指定软件包1.5依赖关系 2. YUM软件仓库的配置及使用2.1修改YUM软件仓库的配置文件 3.…

张量、标量、向量和矩阵

张量、标量、向量和矩阵 https://github.com/bovem/publications/tree/master/Linear%20Algebra 张量是一个数据数组(数字、函数等)&#xff0c;它以任意数量(0 或更大)的维度展开。维数称为张量秩。 秩 0 张量 没有维度(0)的张量。 A 是 0 维张量 秩 1 张量 仅在一维中展开的张…

MySQL 字段为 NULL 的5大坑,99%人踩过

数据库字段允许空值(null)的问题&#xff0c;你遇到过吗&#xff1f; 在验证问题之前&#xff0c;我们先建一张测试表及测试数据。 数据库字段允许空值(null)的问题&#xff0c;你遇到过吗&#xff1f; 在验证问题之前&#xff0c;我们先建一张测试表及测试数据。 构建的测试…

如何实现U盘低格?这样操作快速搞定!

案例&#xff1a;怎么对U盘进行低级格式化&#xff1f; 【我的U盘出现了异常&#xff0c;我想对它进行低级格式化处理&#xff0c;有没有小伙伴知道怎么操作&#xff1f;】 随着电脑和移动设备的普及&#xff0c;U盘已经成为我们生活中必不可少的存储工具。当我们使用U盘的时…

xml注入漏洞

一、先认识XML XML有两个先驱——SGML&#xff08;标准通用标记语言&#xff09;和HTML&#xff08;超文本标记语言&#xff09;&#xff0c;这两个语言都是非常成功的标记语言。SGML多用于科技文献和政府办公文件中&#xff0c;SGML非常复杂&#xff0c;其复杂程度对于网络上…

windows中vscode配置C/C++环境

首先要把MinGW的环境安装完&#xff0c;我一般是下载带有MinGW的codeblocks&#xff0c;这样省去自己安装MinGW。因为安装MinGW还挺麻烦的。 安装完codeblocks&#xff0c;找到其安装目录&#xff0c;把bin文件配置到环境变量去&#xff1a; 将bin添加到环境变量 然后打开vsco…

【MySQL高级】——索引数据结构

一、全表遍历 每个数据页一次加载搜索 二、Hash结构 1. Hash简介 2. 两种常见加快查找速度的数据结构 3. 为什么不采用Hash结构 <1> Hash索引仅能满足&#xff0c;!和IN查询。如果进行范围查询&#xff0c;哈希型的索引&#xff0c;时间复杂度会退化为o(n);而树型的“…

PLUS模型和InVEST模型生态系统服务多情景模拟预测、ArcGIS空间数据处理、空间分析与制图、土地利用时空变化

查看原文>>>基于”PLUS模型“生态系统服务多情景模拟预测实践技术应用 目录 第一章、理论基础与软件讲解 第二章、数据获取与制备 第三章、土地利用格局模拟 第四章、生态系统服务评估 第五章、时空变化及驱动机制分析 第六章、论文撰写技巧及案例分析 基于Ar…

B端产品如何搭建用户帮助体系

用户帮助体系可以提升用户的使用体验&#xff0c;引导用户正确的使用产品&#xff0c;并且体验产品的各个功能&#xff0c;B端产品因为其特殊的业务属性和复杂度&#xff0c;通常其学习成本不低。这些成本不仅仅体现在对于复杂业务概念及流程的认知方面&#xff0c;同时体现在整…

零信任网络安全:为什么越来越多的企业选择采用这一方法?

第一章&#xff1a;引言 随着信息化技术的发展和普及&#xff0c;网络安全问题日益严峻&#xff0c;黑客攻击、病毒感染、数据泄露等问题层出不穷&#xff0c;给企业带来了极大的损失。为了保障企业信息安全&#xff0c;企业采取了一系列安全措施&#xff0c;比如加强防火墙、…

【JavaScript】8.DOM的节点操作

DOM的节点操作 获取元素通常使用两种方式&#xff1a; 利用 DOM 提供的方法获取元素利用节点层级关系获取元素 利用 DOM 提供的方法获取元素 document.getElementById()document.getElementsByTagName()document.querySelector 等逻辑性不强、繁琐 利用节点层级关系获取元素 利…

vite中使用html2canvas 将img、svg和div转canvas

目录 div转canvas svg转canvas img转canvas div转canvas 使用 html2canvas 插件&#xff0c;其官网&#xff1a;html2canvas - Screenshots with JavaScripthttp://html2canvas.hertzen.com/ 安装html2canvas&#xff1a; npm i -S html2canvas 引入&#xff1a; import h…

zotero文献管理软件应用

文章目录 基础教学快速安装与入门第二部分&#xff0c;比较详细的教学与补充文献管理工具比较浏览器插件获取文献与文献pdfZotero 界面添加文件的其他方式管理题录在论文中插入引文文献同步插件与常见问题茉莉花PDFTranslator常见问题1.CNKI页面批量抓取题录失败2.文献类型标识…

Python入门教程+项目实战-11.4节: 元组与列表的区别

目录 11.4.1 不可变数据类型 11.4.2 可变数据类型 11.4.3 元组与列表的区别 11.4.4 知识要点 11.4.5 系统学习python 11.4.1 不可变数据类型 不可变数据类型是指不可以对该数据类型进行修改&#xff0c;即只读的数据类型。迄今为止学过的不可变数据类型有字符串&#xff…

leetcode 494. 目标和 (01 背包

。。最近背包问题做得好崩溃 这题的解法和 分割子集、石头 差不多 分成两个集合 &#xff08;注意这里计算的时候是不带上符号的&#xff0c;只是单纯的数字 a. 正号 的数字集合 P b. 负号 的数字集合 N 所以就有以下公式 sum&#xff08;P&#xff09; sum&#xff08;N&…

XGBoost学习总结

XGBoost有两种实现&#xff0c;原生版本和Sklearn版本 &#xff0c;我选的是sklearn的主要是都用sklearn方便 参数 参数表 class xgboost.XGBRegressor ( max_depth3, learning_rate0.1, n_estimators100, silentTrue, objectivereg:linear, boostergbtree, n_jobs1, nthre…

超简单有趣的模拟算法:元胞自动机(CA)原理简介与 matlab 代码实现

很久之前就就听说了元胞自动机&#xff08;cellular automata&#xff0c;CA&#xff09;&#xff0c;但是一直没有尝试。得知2023年美赛春季赛只有两道赛题的时候&#xff0c;怕考到这个&#xff0c;所以出一篇元胞自动机的博客&#xff0c;权且当一篇学习笔记。 尝试过后才发…

亚科转债,鹿山转债上市价格预测

亚科转债 基本信息 转债名称&#xff1a;亚科转债&#xff0c;评级&#xff1a;AA&#xff0c;发行规模&#xff1a;11.59亿元。 正股名称&#xff1a;亚太科技&#xff0c;今日收盘价&#xff1a;5.58元&#xff0c;转股价格&#xff1a;6.46元。 当前转股价值 转债面值 / 转…