`

衡量java代码质量的指标

 
阅读更多

今天这堂培训课讲什么呢?我既不讲Spring,也不讲Hibernate,更不讲Ext,我不讲任何一个具体的技术。我们抛开任何具体的技术,来谈谈如何提高代码质量。如何提高代码质量,相信不仅是在座所有人苦恼的事情,也是所有软件项目苦恼的事情。如何提高代码质量呢,我认为我们首先要理解什么是高质量的代码。

高质量代码的三要素

我们评价高质量代码有三要素:可读性、可维护性、可变更性。我们的代码要一个都不能少地达到了这三要素的要求才能算高质量的代码。

1.可读性强

一提到可读性似乎有一些老生常谈的味道,但令人沮丧的是,虽然大家一而再,再而三地强调可读性,但我们的代码在可读性方面依然做得非常糟糕。由于工作的需要,我常常需要去阅读他人的代码,维护他人设计的模块。每当我看到大段大段、密密麻麻的代码,而且还没有任何的注释时常常感慨不已,深深体会到了这项工作的重要。由于分工的需要,我们写的代码难免需要别人去阅读和维护的。而对于许多程序员来说,他们很少去阅读和维护别人的代码。正因为如此,他们很少关注代码的可读性,也对如何提高代码的可读性缺乏切身体会。有时即使为代码编写了注释,也常常是注释语言晦涩难懂形同天书,令阅读者反复斟酌依然不明其意。针对以上问题,我给大家以下建议:

1)不要编写大段的代码

如果你有阅读他人代码的经验,当你看到别人写的大段大段的代码,而且还不怎么带注释,你是怎样的感觉,是不是“嗡”地一声头大。各种各样的功能纠缠在一个方法中,各种变量来回调用,相信任何人多不会认为它是高质量的代码,但却频繁地出现在我们编写的程序了。如果现在你再回顾自己写过的代码,你会发现,稍微编写一个复杂的功能,几百行的代码就出去了。一些比较好的办法就是分段。将大段的代码经过整理,分为功能相对独立的一段又一段,并且在每段的前端编写一段注释。这样的编写,比前面那些杂乱无章的大段代码确实进步了不少,但它们在功能独立性、可复用性、可维护性方面依然不尽人意。从另一个比较专业的评价标准来说,它没有实现低耦合、高内聚。我给大家的建议是,将这些相对独立的段落另外封装成一个又一个的函数。

许多大师在自己的经典书籍中,都鼓励我们在编写代码的过程中应当养成不断重构的习惯。我们在编写代码的过程中常常要编写一些复杂的功能,起初是写在一个类的一个函数中。随着功能的逐渐展开,我们开始对复杂功能进行归纳整理,整理出了一个又一个的独立功能。这些独立功能有它与其它功能相互交流的输入输出数据。当我们分析到此处时,我们会非常自然地要将这些功能从原函数中分离出来,形成一个又一个独立的函数,供原函数调用。在编写这些函数时,我们应当仔细思考一下,为它们取一个释义名称,并为它们编写注释(后面还将详细讨论这个问题)。另一个需要思考的问题是,这些函数应当放到什么地方。这些函数可能放在原类中,也可能放到其它相应职责的类中,其遵循的原则应当是“职责驱动设计”(后面也将详细描述)。

下面是我编写的一个从XML文件中读取数据,将其生成工厂的一个类。这个类最主要的一段程序就是初始化工厂,该功能归纳起来就是三部分功能:用各种方式尝试读取文件、以DOM的方式解析XML数据流、生成工厂。而这些功能被我归纳整理后封装在一个不同的函数中,并且为其取了释义名称和编写了注释:

 

Java代码 复制代码 收藏代码
  1. /**
  2. * 初始化工厂。根据路径读取XML文件,将XML文件中的数据装载到工厂中
  3. * @param path XML的路径
  4. */
  5. publicvoid initFactory(String path){
  6. if(findOnlyOneFileByClassPath(path)){return;}
  7. if(findResourcesByUrl(path)){return;}
  8. if(findResourcesByFile(path)){return;}
  9. this.paths = new String[]{path};
  10. }
  11. /**
  12. * 初始化工厂。根据路径列表依次读取XML文件,将XML文件中的数据装载到工厂中
  13. * @param paths 路径列表
  14. */
  15. publicvoid initFactory(String[] paths){
  16. for(int i=0; i<paths.length; i++){
  17. initFactory(paths[i]);
  18. }
  19. this.paths = paths;
  20. }
  21. /**
  22. * 重新初始化工厂,初始化所需的参数,为上一次初始化工厂所用的参数。
  23. */
  24. publicvoid reloadFactory(){
  25. initFactory(this.paths);
  26. }
  27. /**
  28. * 采用ClassLoader的方式试图查找一个文件,并调用<code>readXmlStream()</code>进行解析
  29. * @param path XML文件的路径
  30. * @return 是否成功
  31. */
  32. protectedboolean findOnlyOneFileByClassPath(String path){
  33. boolean success = false;
  34. try {
  35. Resource resource = new ClassPathResource(path, this.getClass());
  36. resource.setFilter(this.getFilter());
  37. InputStream is = resource.getInputStream();
  38. if(is==null){returnfalse;}
  39. readXmlStream(is);
  40. success = true;
  41. } catch (SAXException e) {
  42. log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
  43. } catch (IOException e) {
  44. log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
  45. } catch (ParserConfigurationException e) {
  46. log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
  47. }
  48. return success;
  49. }
  50. /**
  51. * 采用URL的方式试图查找一个目录中的所有XML文件,并调用<code>readXmlStream()</code>进行解析
  52. * @param path XML文件的路径
  53. * @return 是否成功
  54. */
  55. protectedboolean findResourcesByUrl(String path){
  56. boolean success = false;
  57. try {
  58. ResourcePath resourcePath = new PathMatchResource(path, this.getClass());
  59. resourcePath.setFilter(this.getFilter());
  60. Resource[] loaders = resourcePath.getResources();
  61. for(int i=0; i<loaders.length; i++){
  62. InputStream is = loaders[i].getInputStream();
  63. if(is!=null){
  64. readXmlStream(is);
  65. success = true;
  66. }
  67. }
  68. } catch (SAXException e) {
  69. log.debug("Error when findResourcesByUrl:"+path,e);
  70. } catch (IOException e) {
  71. log.debug("Error when findResourcesByUrl:"+path,e);
  72. } catch (ParserConfigurationException e) {
  73. log.debug("Error when findResourcesByUrl:"+path,e);
  74. }
  75. return success;
  76. }
  77. /**
  78. * 用File的方式试图查找文件,并调用<code>readXmlStream()</code>解析
  79. * @param path XML文件的路径
  80. * @return 是否成功
  81. */
  82. protectedboolean findResourcesByFile(String path){
  83. boolean success = false;
  84. FileResource loader = new FileResource(new File(path));
  85. loader.setFilter(this.getFilter());
  86. try {
  87. Resource[] loaders = loader.getResources();
  88. if(loaders==null){returnfalse;}
  89. for(int i=0; i<loaders.length; i++){
  90. InputStream is = loaders[i].getInputStream();
  91. if(is!=null){
  92. readXmlStream(is);
  93. success = true;
  94. }
  95. }
  96. } catch (IOException e) {
  97. log.debug("Error when findResourcesByFile:"+path,e);
  98. } catch (SAXException e) {
  99. log.debug("Error when findResourcesByFile:"+path,e);
  100. } catch (ParserConfigurationException e) {
  101. log.debug("Error when findResourcesByFile:"+path,e);
  102. }
  103. return success;
  104. }
  105. /**
  106. * 读取并解析一个XML的文件输入流,以Element的形式获取XML的根,
  107. * 然后调用<code>buildFactory(Element)</code>构建工厂
  108. * @param inputStream 文件输入流
  109. * @throws SAXException
  110. * @throws IOException
  111. * @throws ParserConfigurationException
  112. */
  113. protectedvoid readXmlStream(InputStream inputStream) throws SAXException, IOException, ParserConfigurationException{
  114. if(inputStream==null){
  115. thrownew ParserConfigurationException("Cann't parse source because of InputStream is null!");
  116. }
  117. DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
  118. factory.setValidating(this.isValidating());
  119. factory.setNamespaceAware(this.isNamespaceAware());
  120. DocumentBuilder build = factory.newDocumentBuilder();
  121. Document doc = build.parse(new InputSource(inputStream));
  122. Element root = doc.getDocumentElement();
  123. buildFactory(root);
  124. }
  125. /**
  126. * 用从一个XML的文件中读取的数据构建工厂
  127. * @param root 从一个XML的文件中读取的数据的根
  128. */
  129. protectedabstractvoid buildFactory(Element root);
	/**
	 * 初始化工厂。根据路径读取XML文件,将XML文件中的数据装载到工厂中
	 * @param path XML的路径
	 */
	public void initFactory(String path){
		if(findOnlyOneFileByClassPath(path)){return;}
		if(findResourcesByUrl(path)){return;}
		if(findResourcesByFile(path)){return;}
		this.paths = new String[]{path};
	}
	
	/**
	 * 初始化工厂。根据路径列表依次读取XML文件,将XML文件中的数据装载到工厂中
	 * @param paths 路径列表
	 */
	public void initFactory(String[] paths){
		for(int i=0; i<paths.length; i++){
			initFactory(paths[i]);
		}
		this.paths = paths;
	}
	
	/**
	 * 重新初始化工厂,初始化所需的参数,为上一次初始化工厂所用的参数。
	 */
	public void reloadFactory(){
		initFactory(this.paths);
	}
	
	/**
	 * 采用ClassLoader的方式试图查找一个文件,并调用<code>readXmlStream()</code>进行解析
	 * @param path XML文件的路径
	 * @return 是否成功
	 */
	protected boolean findOnlyOneFileByClassPath(String path){
		boolean success = false;
		try {
			Resource resource = new ClassPathResource(path, this.getClass());
			resource.setFilter(this.getFilter());
			InputStream is = resource.getInputStream();
			if(is==null){return false;}
			readXmlStream(is);
			success = true;
		} catch (SAXException e) {
			log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
		} catch (IOException e) {
			log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
		} catch (ParserConfigurationException e) {
			log.debug("Error when findOnlyOneFileByClassPath:"+path,e);
		}
		return success;
	}
	
	/**
	 * 采用URL的方式试图查找一个目录中的所有XML文件,并调用<code>readXmlStream()</code>进行解析
	 * @param path XML文件的路径
	 * @return 是否成功
	 */
	protected boolean findResourcesByUrl(String path){
		boolean success = false;
		try {
			ResourcePath resourcePath = new PathMatchResource(path, this.getClass());
			resourcePath.setFilter(this.getFilter());
			Resource[] loaders = resourcePath.getResources();
			for(int i=0; i<loaders.length; i++){
				InputStream is = loaders[i].getInputStream();
				if(is!=null){
					readXmlStream(is);
					success = true;
				}
			}
		} catch (SAXException e) {
			log.debug("Error when findResourcesByUrl:"+path,e);
		} catch (IOException e) {
			log.debug("Error when findResourcesByUrl:"+path,e);
		} catch (ParserConfigurationException e) {
			log.debug("Error when findResourcesByUrl:"+path,e);
		}
		return success;
	}
	
	/**
	 * 用File的方式试图查找文件,并调用<code>readXmlStream()</code>解析
	 * @param path XML文件的路径
	 * @return 是否成功
	 */
	protected boolean findResourcesByFile(String path){
		boolean success = false;
		FileResource loader = new FileResource(new File(path));
		loader.setFilter(this.getFilter());
		try {
			Resource[] loaders = loader.getResources();
			if(loaders==null){return false;}
			for(int i=0; i<loaders.length; i++){
				InputStream is = loaders[i].getInputStream();
				if(is!=null){
					readXmlStream(is);
					success = true;
				}
			}
		} catch (IOException e) {
			log.debug("Error when findResourcesByFile:"+path,e);
		} catch (SAXException e) {
			log.debug("Error when findResourcesByFile:"+path,e);
		} catch (ParserConfigurationException e) {
			log.debug("Error when findResourcesByFile:"+path,e);
		}
		return success;
	}

	/**
	 * 读取并解析一个XML的文件输入流,以Element的形式获取XML的根,
	 * 然后调用<code>buildFactory(Element)</code>构建工厂
	 * @param inputStream 文件输入流
	 * @throws SAXException
	 * @throws IOException
	 * @throws ParserConfigurationException
	 */
	protected void readXmlStream(InputStream inputStream) throws SAXException, IOException, ParserConfigurationException{
		if(inputStream==null){
			throw new ParserConfigurationException("Cann't parse source because of InputStream is null!");
		}
		DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setValidating(this.isValidating());
        factory.setNamespaceAware(this.isNamespaceAware());
        DocumentBuilder build = factory.newDocumentBuilder();
        Document doc = build.parse(new InputSource(inputStream));
        Element root = doc.getDocumentElement();
        buildFactory(root);
	}
	
	/**
	 * 用从一个XML的文件中读取的数据构建工厂
	 * @param root 从一个XML的文件中读取的数据的根
	 */
	protected abstract void buildFactory(Element root);
	

 

完整代码在附件中。在编写代码的过程中,通常有两种不同的方式。一种是从下往上编写,也就是按照顺序,每分出去一个函数,都要将这个函数编写完,才回到主程序,继续往下编写。而一些更有经验的程序员会采用另外一种从上往下的编写方式。当他们在编写程序的时候,每个被分出去的程序,可以暂时只写一个空程序而不去具体实现功能。当主程序完成以后,再一个个实现它的所有子程序。采用这样的编写方式,可以使复杂程序有更好的规划,避免只见树木不见森林的弊病。

有多少代码就算大段代码,每个人有自己的理解。我编写代码,每当达到15~20行的时候,我就开始考虑是否需要重构代码。同理,一个类也不应当有太多的函数,当函数达到一定程度的时候就应该考虑分为多个类了;一个包也不应当有太多的类。。。。。。

2)释义名称与注释

我们在命名变量、函数、属性、类以及包的时候,应当仔细想想,使名称更加符合相应的功能。我们常常在说,设计一个系统时应当有一个或多个系统分析师对整个系统的包、类以及相关的函数和属性进行规划,但在通常的项目中这都非常难于做到。对它们的命名更多的还是程序员来完成。但是,在一个项目开始的时候,应当对项目的命名出台一个规范。譬如,在我的项目中规定,新增记录用newadd开头,更新记录用editmod开头,删除用del开头,查询用findquery开头。使用最乱的就是get,因此我规定,get开头的函数仅仅用于获取类属性。

注释是每个项目组都在不断强调的,可是依然有许多的代码没有任何的注释。为什么呢?因为每个项目在开发过程中往往时间都是非常紧的。在紧张的代码开发过程中,注释往往就渐渐地被忽略了。利用开发工具的代码编写模板也许可以解决这个问题。

用我们常用的MyEclipse为例,在菜单“window>>Preferences>>Java>>Code Style>>Code Templates>>Comments”中,可以简单的修改一下。

 



 

 

Files”代表的是我们每新建一个文件(可能是类也可能是接口)时编写的注释,我通常设定为:

Java代码 复制代码 收藏代码
  1. /*
  2. * created on ${date}
  3. */
/*
 * created on ${date}
 */

 

Types”代表的是我们新建的接口或类前的注释,我通常设定为:

 

Java代码 复制代码 收藏代码
  1. /**
  2. *
  3. * @author ${user}
  4. */
/**
 * 
 * @author ${user}
 */

 

 

第一行为一个空行,是用于你写该类的注释。如果你采用“职责驱动设计”,这里首先应当描述的是该类的职责。如果需要,你可以写该类一些重要的方法及其用法、该类的属性及其中文含义等。

${user}代表的是你在windows中登陆的用户名。如果这个用户名不是你的名称,你可以直接写死为你自己的名称。

其它我通常都保持为默认值。通过以上设定,你在创建类或接口的时候,系统将自动为你编写好注释,然后你可以在这个基础上进行修改,大大提高注释编写的效率。

同时,如果你在代码中新增了一个函数时,通过Alt+Shift+J快捷键,可以按照模板快速添加注释。

在编写代码时如果你编写的是一个接口或抽象类,我还建议你在@author后面增加@see注释,将该接口或抽象类的所有实现类列出来,因为阅读者在阅读的时候,寻找接口或抽象类的实现类比较困难。

 

Java代码 复制代码 收藏代码
  1. /**
  2. * 抽象的单表数组查询实现类,仅用于单表查询
  3. * @author 范钢
  4. * @see com.htxx.support.query.DefaultArrayQuery
  5. * @see com.htxx.support.query.DwrQuery
  6. */
  7. publicabstractclass ArrayQuery implements ISingleQuery {
  8. ...
/**
 * 抽象的单表数组查询实现类,仅用于单表查询
 * @author 范钢
 * @see com.htxx.support.query.DefaultArrayQuery
 * @see com.htxx.support.query.DwrQuery
 */
public abstract class ArrayQuery implements ISingleQuery {
...

 

2.可维护性

软件的可维护性有几层意思,首先的意思就是能够适应软件在部署和使用中的各种情况。从这个角度上来说,它对我们的软件提出的要求就是不能将代码写死。

1)代码不能写死

我曾经见我的同事将系统要读取的一个日志文件指定在C盘的一个固定目录下,如果系统部署时没有这个目录以及这个文件就会出错。如果他将这个决定路径下的目录改为相对路径,或者通过一个属性文件可以修改,代码岂不就写活了。一般来说,我在设计中需要使用日志文件、属性文件、配置文件,通常都是以下几个方式:将文件放到与类相同的目录,使用ClassLoader.getResource()来读取;将文件放到classpath目录下,用File的相对路径来读取;使用web.xml或另一个属性文件来制定读取路径。

我也曾见另一家公司的软件要求,在部署的时候必须在C:/bea目录下,如果换成其它目录则不能正常运行。这样的设定常常为软件部署时带来许多的麻烦。如果服务器在该目录下已经没有多余空间,或者已经有其它软件,将是很挠头的事情。

2)预测可能发生的变化

除此之外,在设计的时候,如果将一些关键参数放到配置文件中,可以为软件部署和使用带来更多的灵活性。要做到这一点,要求我们在软件设计时,应当更多地有更多的意识,考虑到软件应用中可能发生的变化。比如,有一次我在设计财务软件的时候,考虑到一些单据在制作时的前置条件,在不同企业使用的时候,可能要求不一样,有些企业可能要求严格些而有些要求松散些。考虑到这种可能的变化,我将前置条件设计为可配置的,就可能方便部署人员在实际部署中进行灵活变化。然而这样的配置,必要的注释说明是非常必要的。

软件的可维护性的另一层意思就是软件的设计便于日后的变更。这一层意思与软件的可变更性是重合的。所有的软件设计理论的发展,都是从软件的可变更性这一要求逐渐展开的,它成为了软件设计理论的核心。

分享到:
评论

相关推荐

    java代码统计工具

    总的来说,"java代码统计工具"是一个实用的开发辅助工具,通过解析.java文件,它提供了量化代码结构和质量的重要信息,对于项目管理和代码质量管理具有重要意义。它的实现涉及到文件操作、文本解析和可能的过滤策略...

    Java代码统计工具

    总的来说,Java代码统计工具通过提供丰富的统计信息,可以帮助开发人员和团队更好地管理代码库,提升代码质量,优化开发流程。它不仅能够作为个人开发者自我评估的工具,也能在团队协作中发挥重要作用,确保代码规范...

    jPeek是一个Java代码指标的静态收集器

    jPeek就是这样一款工具,专门针对Java代码进行静态分析,以收集关于代码质量的各种指标,如内聚力和耦合等。这些指标对于评估代码的可维护性、可读性和整体健康状况至关重要。 **内聚力(Cohesion)** 内聚力是...

    代码统计工具【java】

    例如,SonarQube是一个全面的代码质量管理平台,它提供了丰富的统计指标和规则检查。 6. **代码统计的意义**: 代码统计不仅仅是数量的体现,它可以帮助我们发现可能的代码冗余,评估重构需求,以及衡量团队的工作...

    Java代码质量守护:深入探索FindBugs在Spring Boot与Vue.js集成中的应用

    在Java开发领域,代码质量是衡量软件成功的关键指标之一。FindBugs作为一个强大的静态代码分析工具,它通过分析字节码来发现潜在的问题,帮助开发者提升代码质量。本文将详细介绍FindBugs的主要功能、如何在Spring ...

    java代码比对覆盖工具

    6. **代码审查**:在代码审查过程中,代码比对工具可以让审阅者清晰地看到改动细节,有助于发现潜在的问题和改进点,提升代码质量。 7. **格式化和排序**:一些高级的比对工具还提供了代码格式化和排序的功能,这...

    JAVA代码行数计算器

    总的来说,"JAVA代码行数计算器"是软件开发过程中的一种实用工具,能够辅助开发团队进行项目管理,优化代码质量,并且在评估开发进度和复杂性时起到重要作用。正确理解和使用这样的工具,对于提升开发效率和维护良好...

    java代码大全 工具书

    对于程序员来说,理解和掌握如何有效地进行构建是衡量其专业水平的重要指标。 在软件开发的生命周期中,有许多不同的活动,例如问题定义、需求分析、规划构建、架构设计、详细设计、编码与调试、单元测试、集成测试...

    java编写软件度量代码

    Java编程语言在软件开发中扮演着重要角色,而软件度量是评估代码质量和复杂性的重要工具。本资源提供了一个简单的Java实现,用于计算代码行数,这是度量软件规模的一个基本指标,也是估算软件开发成本的关键因素。这...

    Java单元测试之代码覆盖率-JaCoCo

    而代码覆盖率则是衡量单元测试有效性的一个关键指标,它表示了被测试代码被执行的程度。JaCoCo是一款广泛使用的Java代码覆盖率工具,它可以方便地集成到各种构建工具和持续集成系统中,提供详细的覆盖率报告。 ...

    统计源代码量源码(java编程)

    在Java编程领域,统计源代码量是一项常见的任务,它有助于开发者了解项目的规模,评估开发进度,以及进行代码质量分析。本程序就是一个专为此目的而设计的工具,它可以帮助我们快速统计出一个项目中的源代码行数。...

    JAVA代码统计工具,可以按人统计,很不错

    3. **质量控制**:统计工具可以发现代码中的冗余和重复部分,提示代码审查,以提升代码质量和可维护性。 4. **代码审查**:在代码审查过程中,统计工具可以作为参考,帮助识别可能过于复杂的代码段,需要进一步优化...

    java代码统计器 要安装java虚拟机才能运行

    代码统计器的输出通常包括总代码行数、注释行数、空行数以及可能的复杂度指标,如Cyclomatic Complexity(圈复杂度),这是一种衡量代码复杂性的度量,由Mccabe提出,通过计算控制流图的边数来确定。在Java中,可以...

    java 测试代码

    Java测试代码是软件开发过程中不可或缺的一部分,主要用于验证代码的功能正确性、性能以及代码质量。在Java编程中,测试代码通常采用单元测试、集成测试和系统测试等不同层次的方法。下面我们将详细探讨Java测试的...

    C++、Java代码统计工具

    总的来说,C++和Java代码统计工具是开发流程中不可或缺的一部分,它们通过提供量化数据,帮助开发者更好地理解和管理项目,从而提高开发效率和代码质量。在选择或使用这类工具时,应根据具体需求,考虑其是否支持所...

    Java版代码统计.zip

    总的来说,“Java版代码统计.exe”是一个为Java开发者设计的实用工具,它可以帮助我们更好地理解和管理Java项目,提升开发效率和代码质量。在实际使用时,结合代码审查和其他质量保证措施,将使软件开发过程更为科学...

    一个基于java的c的代码计数程序

    此外,“其他扩展功能”可能包括统计注释数量、函数数量、变量数量等,这些都能为代码质量分析提供更深入的见解。 对于Java开发者来说,编写这样的工具可能涉及到以下几个关键知识点: 1. 文件I/O操作:Java的`...

    追求代码质量(9)用JUnitPerf进行性能测试Java

    总之,JUnitPerf为Java开发者提供了一个强大的性能测试工具,通过集成到JUnit测试流程中,能够帮助我们有效地识别和解决性能问题,从而提升代码质量和应用的总体性能。通过熟练掌握JUnitPerf的使用,开发者能够在...

    基于java代码的测试覆盖率.pdf

    总的来说,代码覆盖率是提高Java代码质量的重要工具。从初级的行覆盖和方法覆盖,到中级的圈复杂度管理和重构,再到高级的持续集成与突变测试,每个阶段都旨在提升代码的健壮性和可维护性。通过这些实践,开发者可以...

Global site tag (gtag.js) - Google Analytics