第一部分 :高质量代码的意义
在软件开发中,代码的质量几乎与它的功能同等重要。质量高的代码能够确保软件的可靠性、可扩展性,并降低维护成本。
在思考高质量代码的意义时,不妨反过来思考,低质量的代码会造成什么后果
低质量的代码会造成什么影响呢
取一个碗,我们现在称之为A。取一个平底锅,我们现在称之为B。在B中装满水,置于炉盘上。在A中放入黄油和巧克力,前者100g,后者185g。这应该是70%的黑巧克力。将A放在B之上;等待A的内容物融化,然后将A移到B之外。再取一个碗,我们现在称之为C。在C中放入鸡蛋、糖和香草香精,第一种原料放两个,第二种185g,第三种半茶匙。混合C的内容物。A的内容物冷却后,将其加入C中并混合。取一个碗,我们称之为D。在D中放入面粉、可可粉和盐,第一种原料50g,第二种原料35g,第三种半茶匙。完全混合D的内容物,然后过滤到C中。充分搅拌D的内容物使其完全混合。我们要用这种方法制作巧克力糕饼,我是不是忘记说这个了?在D中加入70g巧克力屑,充分搅拌D的内容物。取一个烘焙模具,我们称之为E。在E中涂上油脂并铺上烘焙纸。将D的内容物放入E中。我们将把你的烤炉称为F。顺便说一句,你应该将F预热到160℃。将E放入F中20min,然后取出。让E冷却几小时。
阅读以上文本就好比阅读低质量代码一样,难以理解具体要做的是什么,怎么做
技术债务累积: 低质量代码很容易产生技术债务,随着时间的推移,应对这些债务所需的成本和精力可能会成倍增长。
阻碍新功能的实现: 低质量的代码很难扩展和修改,这可能妨碍新功能的快速迭代和部署,影响市场响应速度和竞争力。
破坏用户信任: 由于错误和问题的数量增加,软件的整体稳定性会降低,这可能导致更频繁的系统崩溃或故障。不稳定的软件会影响用户体验,从而损害用户对产品的信任
增加调试和维护成本: 低质量代码通常包含更多的bug和错误,这意味着需要更多的时间和资源去诊断和修复问题,长期看会增加软件的维护成本。
知识传递障碍:低质量的代码往往难以阅读、理解和修改,使得新成员或其他项目人员熟悉的过程变得困难和耗时。对个别人员的依赖过度增加了项目的风险 , 如果这些熟悉代码的开发者离开或不可用,项目可能遭受严重破坏。
第二部分 好代码和坏代码
高质量代码不仅体现在它能正常运行,还包括它的设计思想、结构整洁、易于其他开发者理解和维护、并且能经受时间的考验。
- 易于理解的代码: 高质量代码应该是直观的,这样开发者可以快速把握它的功能和目的,无需深入研究每一行代码。良好的命名、清晰的逻辑和简洁的结构有助于提升代码的可读性。
- 易于扩展: 高质量的代码应容易进行修改和扩展。这意味着采用模块化设计,各个部分之间耦合度低,从而更易于处理变更和扩展。如果子问题的解决方案完全包含在单独的类中,其他类只要通过几个深思熟虑的公共函数与之交互,那么在必要时用另一个实现替换它就很容易了。
- 可复用性: 代码不应该被编写为一次性使用。如果解决某个问题需要解决两个子问题,那么其他人也很可能在未来有解决其中一个子问题的需求。如果我们将两个子问题的解决方案捆绑在一个类中,就降低了其他人重用其中一个解决方案的概率。
- 可维护性: 优秀的代码应该易于维护。这包括清晰的架构、遵循编码标准、良好的文档记录和注释,以及对代码质量的持续重视,通过定期的重构来应对技术债务。
- 可测试性: 高质量代码应当易于测试。它遵循某种形式的测试驱动开发(TDD),函数和组件可独立测试,这有助于在开发初期发现并解决问题。
- 鲁棒性和错误处理: 高质量代码能够妥善处理错误和异常情况,不会因为意外的用户输入或环境变化导致程序崩溃。
- 文档和注释: 代码应有充分的文档来指导新开发者了解系统,注释应精确而有用,标注复杂逻辑和重要决策点。
代码常见的坏味道
代码的“坏味道”通常指的是代码中的那些问题,这些问题虽然不一定是错误,但它们可能表明设计上的缺陷,通常会降低代码的可读性、可维护性或可扩展性。
-
重复代码(Duplicated Code):
代码中存在两处或更多几乎相同的代码片段。
-
过长函数(Long Method):
函数体过长,包含了过多的行数,难以理解和维护。当我们无法用一两句话解释这个函数所作的事情时,往往可能这个函数就需要进行拆分
-
过多的参数(Long Parameter List):
函数或方法的参数列表过长,难以理解和使用。
-
条件复杂性(Complex Conditional):语句和逻辑判断。
包含复杂且难以理解的 if - else , switch
-
依赖怪物(Dependency Monster):
类或模块具有过多的依赖关系,任何小的变动都可能引起连锁效应。
Example:
def process_data(*args):
# 坏味道:参数过多,使用不定长参数列表不清晰
if len(args) < 5:
print("Error: Not enough arguments provided.")
return
user_id, data, update, delete, user_name, user_age, user_email, user_status = args[0], args[1], args[2], args[3], args[4], args[5], args[6], args[7]
# 坏味道:过度复杂的逻辑判断
if user_id is not None and data is not None and update and not delete:
if user_name is not None and user_age > 0:
# 假设有一些数据更新逻辑
update_user_name_and_age(user_id, user_name, user_age)
elif user_email is not None:
if user_status in ["active", "inactive"]:
# 假设有一些其他的数据更新逻辑
update_user_email_and_status(user_id, user_email, user_status)
else:
print("Error: Invalid user status provided.")
else:
print("Error: Invalid data provided for update.")
elif delete and user_id is not None:
# 假设有一些用户删除逻辑
delete_user(user_id)
else:
print("Error: Invalid operation requested.")
# 坏味道:函数太长,包含多个不同的操作
return data
# 假设的更新函数
def update_user_name_and_age(user_id, user_name, user_age):
pass
# 假设的更新函数
def update_user_email_and_status(user_id, user_email, user_status):
pass
# 假设的删除函数
def delete_user(user_id):
pass
# 用法示例
process_data(1, {}, True, False, "John Doe", 30, "johndoe@example.com", "active")
高质量代码的技巧 - 良好的编程习惯
利用Pythonic的特性
理解Python的特性:Python提供了许多语言特性和函数,如列表推导、生成器表达式、内置函数等,合理利用这些特性可以让你的代码更简洁、更高效。
- 列表推导是 Python 中非常有用的一个特性,它可以用来创建新的列表,并且写法简洁,执行效率高。
numbers = []
for i in range(10):
numbers.append(run_calculation(i))
numbers = [run_calculation(i) for i in range(10)] # 使用推导式直接创建
Python 的内置函数可以帮助我们更高效地完成常见任务,如 map()
, filter()
, sum()
等。
# 使用 map 应用函数到每个元素
numbers = [1, 2, 3, 4, 5]
squared_numbers = list(map(lambda x: x**2, numbers))
# 使用 filter 筛选出符合条件的元素
even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
# 当你需要在循环中获取元素的索引时,enumerate 提供了一种优雅的方式。
names = ['Alice', 'Bob', 'Charlie']
for index, name in enumerate(names):
print(f"{index}: {name}")
#当你需要同时迭代多个列表时,zip 可以同步遍历它们。
names = ['Alice', 'Bob', 'Charlie']
ages = [24, 30, 28]
for name, age in zip(names, ages):
print(f"{name} is {age} years old")
编写文档和注释
- 文档字符串:为模块、函数、类和方法编写文档字符串(docstrings),说明其功能和用法。这对于代码的后期维护及他人阅读你的代码非常重要。
- 适当注释:对复杂的算法或决策逻辑进行注释,但避免对显而易见的代码添加不必要的注释。
提前返回(Return Early)
- 提前返回是一种编程技巧,其中函数或方法在满足某些条件时尽早退出,而不是继续执行更多的逻辑。这样做的目的是减少嵌套的深度,使代码更加清晰和简洁。
class LoginPage:
def is_loaded(self):
return True # 与页面加载状态相关的逻辑
def is_logged_in(self):
# 与用户登录状态相关的逻辑,需要提供实现
pass
def login(self, username, password):
# 前置条件: 确保在登录页面,如果不满足直接返回
if not self.is_loaded():
print("Login page not loaded.")
return
# 检查是否已经登录
if self.is_logged_in():
return
# 登录操作 具体执行登录操作的逻辑
return "Login successful"
代码质量的基础 - 抽象层次
在编写测试框架时,往往我们会有一种说法,该测试框架是为某某某项目定制的,那这个测试框架就真的无法给其他项目重用了吗,或者说真的完全存在100%定制的代码吗
为什么要创建抽象层次
编写代码是将一个大问题持续分解为小问题的过程。如果我们能将一个问题很好的递归分解为几个子问题,并创建合理的抽象层次和模块,那么任何一段单独的代码看起来都不会特别复杂
- 比如 : 验证数据库中的最终数值与特定数据处理逻辑的结果是否一致,先看一下如果不进行抽象和分层,用常见的也就是轻设计重实现的方式来完成大概会是什么样的方式
import pytest
import pandas as pd
import requests
import pymysql
def test_data_processing():
# 从Excel文件读取测试数据
file_path = 'data.xlsx'
df = pd.read_excel(file_path)
test_data = df.iloc[1].to_dict()
# 假设调用某个数据处理的API,并向其发送测试数据
response = requests.post("<http://example.com/api/process>", json={"data": test_data})
assert response.status_code == 200
data_processing_result = response.json().get('result') # 假定API返回了处理结果
# MySQL数据库连接信息
db_host = 'localhost'
db_user = 'your_user'
db_password = 'your_password'
db_database = 'your_database'
# 连接到MySQL数据库
connection = pymysql.connect(host=db_host, user=db_user, password=db_password, database=db_database)
try:
with connection.cursor() as cursor:
# 查询语句,假设我们需要比较的是value列的总和
sql = "SELECT SUM(value) FROM data_table"
cursor.execute(sql)
db_sum_result = cursor.fetchone()[0]
# 比较测试数据处理结果与数据库中的数值
assert db_sum_result == data_processing_result, "数据库中的数值与特定数据处理逻辑的结果不一致"
finally:
connection.close()
# 如果你希望直接运行这个脚本进行测试,而不是通过pytest命令
if __name__ == "__main__":
test_data_processing()
这个代码片段虽然可以直接运行并达到了预期的目标,但它将所有逻辑都紧密集成在了一个函数中。这种做法的主要缺点包括:
- 可维护性差:随着这段代码逻辑的扩展,其将变得越来越难以维护和更新。
- 可复用性低:代码中关于数据库操作和数据处理的逻辑难以被其他部分或项目复用。如果有其他测试用例,往往要重新编写之前实现的内容,比如数据库查询 连接等
- 测试困难:因为所有逻辑都紧密耦合,进行单元测试或者功能测试将变得非常困难。
面对软件开发任务时,一种高效的策略是将其拆解为一系列更小、更具体的操作步骤。这种策略不仅有助于梳理思路,还能使得后续的代码实现更加条理化和清晰。接下来,让我们拆分及解析整个任务流程:
- 步骤一:获取测试数据:最初的阶段是确认我们需要验证的精确数据。这通常意味着从某个确定的数据源(如Excel文件)提取或创建出适用于测试的数据集。
- 步骤二:触发数据处理逻辑:执行特定的数据处理逻辑。
- 步骤三:建立数据库联系
- 步骤四:执行查询
- 步骤五:断开连接
- 步骤六:对比结果:最终,将数据处理逻辑的输出与从数据库中查询到的结果进行比较,完成验证流程。
开始进行改造
-
先将读取Excel文件获取数据进行一层抽象
import pandas as pd class ExcelReader: def __init__(self, file_path): self.file_path = file_path self.data_frame = pd.read_excel(file_path) def read_column_to_dict(self, column_name): """按列名读取数据到字典。""" if column_name in self.data_frame: return self.data_frame[column_name].to_dict() else: raise ValueError(f"Column '{column_name}' not found in the Excel file.") def read_row_to_dict(self, row_index): """按行号读取数据到字典。""" if row_index < len(self.data_frame): return self.data_frame.iloc[row_index].to_dict() else: raise IndexError(f"Row index '{row_index}' is out of bounds.")
-
数据库的连接查询关闭操作可以单独封装进行抽象
import pymysql class DatabaseManager: def __init__(self, host, user, password, database): self.host = host self.user = user self.password = password self.database = database def connect(self): self.connection = pymysql.connect( host=self.host, user=self.user, password=self.password, database=self.database ) def close(self): if self.connection: self.connection.close() def get_sum_of_values(self): query = "SELECT SUM(value) FROM data_table" with self.connection.cursor() as cursor: cursor.execute(query) result = cursor.fetchone()[0] return result def query(self, sql, params=None): with self.connection.cursor() as cursor: cursor.execute(sql, params) result = cursor.fetchall() return result def execute(self, sql, params=None): with self.connection.cursor() as cursor: cursor.execute(sql, params) self.connection.commit() def __enter__(self): self.connect() return self def __exit__(self, exc_type, exc_val, exc_tb): self.close()
-
最终修改后的代码如下
import pandas as pd import requests from database_manager import DatabaseManager from excel_reader import ExcelReader # 导入ExcelReader类 from config import Config # 导入配置 def test_data_processing(): # 从Excel文件读取指定列的数据到字典中 excel_reader = ExcelReader(Config.EXCEL_PATH) test_data = excel_reader.read_row_to_dict(1) # 调用数据处理API response = requests.post(Config.DATA_PROCESSING_API_URL, json={"data": test_data}) assert response.status_code == 200 data_processing_result = response.json().get('result') # 使用数据库管理类进行操作 with DatabaseManager(Config.DB_HOST, Config.DB_USER, Config.DB_PASSWORD, Config.DB_DATABASE) as db_manager: query_result = db_manager.query("SELECT SUM(value) FROM data_table") db_sum_result = query_result[0][0] if query_result else None # 比较测试数据处理结果与数据库中的数值 assert db_sum_result == data_processing_result, "数据库中的数值与特定数据处理逻辑的结果不一致" if __name__ == "__main__": test_data_processing()
请看以下代码
#DatabaseManager中的函数 def get_sum_of_values(self): query = "SELECT SUM(value) FROM data_table" with self.connection.cursor() as cursor: cursor.execute(query) result = cursor.fetchone()[0] return result #DatabaseManager中的函数 def query(self, sql, params=None): with self.connection.cursor() as cursor: cursor.execute(sql, params) result = cursor.fetchall() return result #DatabaseManager中的函数 def execute(self, sql, params=None): with self.connection.cursor() as cursor: cursor.execute(sql, params) self.connection.commit()
Q : get_sum_of_values函数应该包括在DatabaseManager中吗
将
get_sum_of_values
函数包括在DatabaseManager
类中可能不是最佳实践,因为这样做使得DatabaseManager
承担了超出其基本职责的工作。DatabaseManager
的主要职责应当是提供一种与数据库进行通用交互的方式,而不是处理特定于应用逻辑的查询。包括特定业务逻辑(如获取数据表中某一列的总和)在
DatabaseManager
类中的主要问题如下:- 违反单一职责原则:
DatabaseManager
不仅仅是作为一个与数据库连接和操作的通用工具,同时还特定处理了某些业务逻辑,这增加了它的复杂度和变更时的风险。 - 降低灵活性和可维护性:如果未来有对数据表结构或业务逻辑的修改,直接在
DatabaseManager
中进行这些调整可能会影响到所有使用它的代码,而不是只影响到处理特定业务逻辑的部分。 - 减少代码的可重用性:其他可能需要不同类型数据处理的业务场景无法通用
DatabaseManager
,限制了其作为一个通用数据库管理工具的潜在价值。
- 违反单一职责原则: