为什么构造函数需要尽可能的简单

news/2025/4/2 3:15:29/文章来源:https://www.cnblogs.com/CareySon/p/18801509

最近在做一些代码重构,涉及到Python中部分代码重构后,单元测试实现较为麻烦甚至难以实现的场景,其中一个主要的原因是构造函数过于复杂。

因此,本篇文章借此总结一下我们应该需要什么样的构造函数。本篇文章涉及的概念不仅限于Python。

 

构造函数是什么

构造函数用于创建对象时触发,如果不自定义构造函数,通常现代的编程语言在编译时会自动加一个无参的构造函数,同时将类成员设置成默认值,Python中需要定义对象成员才能访问,而类C语言比如C#中int、bool、float等都会设置为0等值的值,比如整数为0、浮点数为0.0或布尔值为false,对于非原始值的引用类型,比如String或Class,都会设置为Null。

构造函数是对类初始化非常合理的位置。因为构造函数是新建对象时触发,相比对象构造之后再去修改对象属性,带来的麻烦远比收益多,比如说空指针、时序耦合、多线程问题等,这些有兴趣后续有机会再聊,但总之将类的初始化放到构造函数中就像是先打地基再盖房子,而不是房子盖一半再回头修补地基,也避免类处于“半成品”状态。

虽然构造函数应该做完整的工作避免半成品,但如果给构造函数赋予太多的责任,会对系统带来很多麻烦,就好比房子主体结构(构造函数)还没完工,就要搬家具进房屋,通常会带来不必要的负担。

 

我们需要什么样的构造函数

一句话总结:在我看来,构造函数只应该做赋值,以及最基本的参数校验。而不应该做外部调用和复杂的初始化,使用简单构造函数能够带来如下好处:

 

可维护性

单一职责,避免惊喜

构造函数也应当遵循单一职责原则,仅负责对象的初始化和基本验证,而不应包含其他复杂操作。当构造函数承担过多责任时,会产生意外的"惊喜",使代码难以理解和维护。

例如下面代码,在构造函数中执行了数据库查询操作(外部依赖),以及统计计算(无外部依赖,复杂的内部计算),我们很难一眼看出该函数初始化要做什么,增加阅读和理解代码的认知负担。

class UserReport:def __init__(self, user_id):self.user_id = user_id# 构造函数中进行数据库操作(有外部依赖)self.user = database.fetch_user(user_id)# 构造函数中执行复杂计算(内部复杂计算,无外部依赖)self.statistics = self._calculate_statistics()def _calculate_statistics(self):# 假设是一个复杂的统计计算return {"login_count": 42, "active_days": 15}

而理想的构造函数,应该只是简单做“初始化赋值”这一个操作,如下所示:

class UserReport:def __init__(self, user, statistics):"""构造函数只负责初始化,不执行其他操作"""self.user = userself.statistics = statistics

该构造函数只做初始化赋值,没有预期之外的情况,比如例子中_calculate_statistics函数,如果在方法内继续引用其他类,其他类再次有外部依赖的访问(比如IO、API调用、数据库操作等),会产生惊喜。

 

减少意外的副作用

构造函数中包含复杂操作不仅违反单一职责原则,还可能带来意外的副作用。这些副作用可能导致系统行为不可预测,增加调试难度,甚至引发难以察觉的bug。

我们继续看之前的代码示例:

class UserReport:def __init__(self, user_id):self.user_id = user_id# 构造函数中进行数据库操作self.user = database.fetch_user(user_id)# 构造函数中执行复杂计算self.statistics = self._calculate_statistics()def _calculate_statistics(self):# 复杂的统计计算data = database.fetch_user_activities(self.user_id)if not data:# 可能抛出异常raise ValueError(f"No activity data for user {self.user_id}")return {"login_count": len(data), "active_days": len(set(d.date() for d in data))}

这段代码可以看到,_calculate_statistics() 函数有数据库访问,这是隐藏的依赖,同时如果数据库访问存在异常可能导致整个对象创建失败,调用者只想创建对象,却可能引发了数据库无法连接的异常。这在运行时都属于意外。

Traceback (most recent call last):File "main.py", line 42, in <module>report = UserReport(user_id=1001)  # 调用者只是想创建一个报告对象File "user_report.py", line 5, in __init__self.user = database.fetch_user(user_id)  # 数据库查询可能失败File "database.py", line 78, in fetch_useruser_data = self._execute_query(f"SELECT * FROM users WHERE id = {user_id}")File "database.py", line 31, in _execute_queryconnection = self._get_connection()File "database.py", line 15, in _get_connectionreturn pymysql.connect(host=self.host, user=self.user, password=self.password, db=self.db_name)File "/usr/local/lib/python3.8/site-packages/pymysql/__init__.py", line 94, in Connectreturn Connection(*args, **kwargs)File "/usr/local/lib/python3.8/site-packages/pymysql/connections.py", line 327, in __init__self.connect()File "/usr/local/lib/python3.8/site-packages/pymysql/connections.py", line 629, in connectraise exc
pymysql.err.OperationalError: (2003, "Can't connect to MySQL server on 'db.example.com' (timed out)")

而将计算逻辑提取到专门函数,访问外部依赖的逻辑通过注入进行,就不会存在该问题:

class UserReport:def __init__(self, user, statistics=None):"""构造函数只负责初始化,无副作用"""self.user = userself.statistics = statistics if statistics is not None else {}def calculate_statistics(self, activity_source):"""将计算逻辑分离到专门的方法,并接受依赖注入"""activities = activity_source.get_activities(self.user.id)self.statistics = {"login_count": len(activities),"active_days": len(set(a.date for a in activities))}return self.statisticsclass UserActivity:def __init__(self, user_id, date, action):self.user_id = user_idself.date = dateself.action = actionclass DatabaseActivity:def get_activities(self, user_id):# 实际应用中会查询数据库return database.fetch_user_activities(user_id)

 

方便调试和演进

构造函数仅负责简单的初始化时,代码变得更加易于调试和演进。相比之下,包含复杂逻辑的构造函数会使问题定位和系统扩展变得困难。比如下面例子

class UserReport:def __init__(self, user_id):self.user_id = user_idself.user = database.fetch_user(user_id)self.activities = database.fetch_user_activities(user_id)self.statistics = self._calculate_statistics()self.recommendations = self._generate_recommendations()# 更多复杂逻辑...

可以看到构造函数包括了太多可能失败的点,调试时也不容易找到具体哪一行除了问题。而下面方式调试容易很多:

class UserReport:def __init__(self, user, activities=None, statistics=None, recommendations=None):self.user = userself.activities = activities or []self.statistics = statistics or {}self.recommendations = recommendations or []

而演进时,复杂的构造函数有很大风险,例如:

# 需要修改原有构造函数,风险很高
class UserReport:def __init__(self, user_id, month=None):  # 添加新参数self.user_id = user_idself.user = database.fetch_user(user_id)# 修改现有逻辑if month:self.activities = database.fetch_user_activities_by_month(user_id, month)else:self.activities = database.fetch_user_activities(user_id)# 以下计算可能需要调整self.statistics = self._calculate_statistics()self.recommendations = self._generate_recommendations()

我们需要添加按月筛选活动数据,增加一个参数,这种情况也是实际代码维护中经常出现的,想到哪写到哪,导致构造函数变的非常复杂难以理解,同时增加出错可能性,而更好的方式如下:

class UserReport:def __init__(self, user, activities=None, statistics=None, recommendations=None):self.user = userself.activities = activities or []self.statistics = statistics or {}self.recommendations = recommendations or []def filter_by_month(self, month):"""添加新功能作为单独的方法"""filtered_activities = [a for a in self.activities if a.date.month == month]return UserReport(self.user,activities=filtered_activities,# 可根据需要重新计算或保留原有数据)

新功能可以独立添加,不影响现有功能,同时也避免修改这种核心逻辑时测试不全面带来的上线提心吊胆。

 

可测试性

良好的构造函数设计对代码的可测试性有着决定性的影响。当构造函数简单且只负责基本初始化时,测试变得更加容易、更加可靠,且不依赖于特定环境。这也是为什么我写本篇文章的原因,就是在写单元测试时发现很多类几乎不可测试(部分引用的第三方类库中的类,类本身属于其他组件,我无权修改,-.-)。

依赖注入与可测试性

如果构造函数有较多逻辑,例如:

class UserReport:def __init__(self, user_id):self.user_id = user_idself.user = database.fetch_user(user_id)self.activities = database.fetch_user_activities(user_id)self.statistics = self._calculate_statistics()

那么我们的单元测试会变的成本非常高昂,每一个外部依赖都需要mock,就算只需要测试一个非常简单的Case,也需要模拟所有外部依赖,比如

def test_user_report():# 需要大量的模拟设置with patch('module.database.fetch_user') as mock_fetch_user:with patch('module.database.fetch_user_activities') as mock_fetch_activities:# 配置模拟返回值mock_fetch_user.return_value = User(1, "Test User", "test@example.com")mock_fetch_activities.return_value = [Activity(1, datetime(2023, 1, 1), "login"),Activity(1, datetime(2023, 1, 2), "login")]# 创建对象 - 即使只是测试一小部分功能也需要模拟所有依赖report = UserReport(1)# 验证结果assert report.statistics["login_count"] == 2assert report.statistics["active_days"] == 2# 验证调用mock_fetch_user.assert_called_once_with(1)mock_fetch_activities.assert_called_once_with(1)

 

而构造函数简单,我们的单元测试也会变得非常简单,比如针对下面代码进行测试:

class UserReport:def __init__(self, user, activities=None):self.user = userself.activities = activities or []self.statistics = {}def calculate_statistics(self):"""计算统计数据"""login_count = len(self.activities)active_days = len(set(a.date for a in self.activities))self.statistics = {"login_count": login_count,"active_days": active_days}return self.statistics

可以看到单元测试不再需要复杂的Mock

def test_report_should_calculate_correct_statistics_when_activities_provided():# 直接创建测试对象,无需模拟外部依赖user = User(1, "Test User", "test@example.com")activities = [UserActivity(1, datetime(2023, 1, 1), "login"),UserActivity(1, datetime(2023, 1, 2), "login"),UserActivity(1, datetime(2023, 1, 2), "logout")  # 同一天的另一个活动]# 创建对象非常简单report = UserReport(user, activities)# 测试特定方法stats = report.calculate_statistics()# 验证结果assert stats["login_count"] == 3assert stats["active_days"] == 2

同时测试时,Mock对象注入也变得非常简单,如下:

def test_report_should_use_activity_source_when_calculating_statistics():# 准备测试数据user = User(42, "Test User", "test@example.com")mock_activities = [UserActivity(42, datetime(2023, 1, 1), "login"),UserActivity(42, datetime(2023, 1, 2), "login")]# 创建模拟数据源activity_source = MockActivity(mock_activities)# 使用依赖注入report = UserReport(user)report.calculate_statistics(activity_source)# 验证结果assert report.statistics["login_count"] == 2assert report.statistics["active_days"] == 2

而做边界值测试时更为简单:

def test_statistics_should_be_empty_when_activities_list_is_empty():user = User(1, "Test User", "test@example.com")report = UserReport(user, [])  # 空活动列表stats = report.calculate_statistics()assert stats["login_count"] == 0assert stats["active_days"] == 0def test_constructor_should_throw_exception_when_user_is_null():# 测试无效用户情况with pytest.raises(ValueError):report = UserReport(None)  # 假设我们在构造函数中验证用户不为空

因此整个代码逻辑通过单元测试将变得更为健壮,而不是需要大量复杂的Mock,复杂的Mock会导致单元测试非常脆弱(也就是修改一点逻辑,导致现有的单元测试无效)

 

架构相关影响

更容易依赖注入

依赖注入的核心理念是高层模块不应该依赖于低层模块的实现细节,而应该依赖于抽象。好比我们需要打车去公司上班,我们只要打开滴滴输入目的地,我们更高层次的需求是从A到B,而具体的实现细节是打车过程是哪款车,或者司机是谁,这也不是我们关心的。具体由哪辆车,哪位司机提供服务可以随时切换。

依赖注入是现代软件架构的核心实践之一,而简单的构造函数设计是实现有效依赖注入的基础。通过构造函数注入依赖,我们可以构建松耦合、高内聚的系统,显著提高代码的可维护性和可扩展性。

# 直接在类内部创建依赖
class UserReport:def __init__(self, user_id):self.user_id = user_id# 直接依赖具体实现self.database = MySQLDatabase()self.user = self.database.fetch_user(user_id)
# 通过构造函数注入依赖
class UserReport:def __init__(self, user, activity_source):self.user = userself.activity_source = activity_sourceself.statistics = {}def calculate_statistics(self):activities = self.activity_source.get_activities(self.user.id)# 计算逻辑...

通过第二段代码可以看到更容易实现依赖注入,通常实际使用中还结合依赖注入容器(IoC)自动化依赖的创建和注入,但这超出本篇的篇幅了。

 

 

更容易暴露设计问题

构造函数仅做赋值操作,还能更容易得暴露类的设计问题。当构造函数变得臃肿或复杂时,这通常表明存在更深层次的设计缺陷。

比如一个类的构造函数有大量参数时,通常意味着类承担过多的职责,比如:

# 需要引起警觉:参数过多的构造函数
class UserReport:def __init__(self, user, activity_list, login_calculator, active_days_calculator, visualization_tool, report_exporter, notification_system):self.user = userself.activity_list = activity_listself.login_calculator = login_calculator  self.active_days_calculator = active_days_calculatorself.visualization_tool = visualization_toolself.report_exporter = report_exporterself.notification_system = notification_systemself.statistics = {}

 

一个常见的解决思路是使用Builder模式,让初始化过程更加优雅,但这通常只能掩盖问题,而不是解决问题

因此可以将过多参数的构造函数当做red flag,正确的解决办法是重新查看类的设计,进行职责分离:

# 核心报告类,只关注数据和基本统计
class UserReport:def __init__(self, user, activities):self.user = userself.activities = activitiesself.statistics = {}def calculate(self, calculator):self.statistics = calculator.compute(self.activities)return self# 分离的统计计算
class ActivityStatistics:def compute(self, activities):login_count = len([a for a in activities if a.action == 'login'])unique_days = len(set(a.date for a in activities))return {"logins": login_count, "active_days": unique_days}# 分离的报告导出功能
class ReportExport:def to_pdf(self, report):# PDF导出逻辑passdef to_excel(self, report):# Excel导出逻辑pass# 分离的通知功能
class ReportNotification:def send(self, report, recipients):# 发送通知逻辑pass

那么类的调用就会变得非常清晰:

# 清晰的职责分离
user = User(42, "John Doe", "john@example.com")
activities = activity_database.get_user_activities(user.id)# 创建和计算报告
calculator = ActivityStatistics()
report = UserReport(user, activities).calculate(calculator)# 导出报告(如果需要)
if export_needed:exporter = ReportExport()pdf_file = exporter.to_pdf(report)# 发送通知(如果需要)
if notify_admin:notifier = ReportNotification()notifier.send(report, ["admin@example.com"])

这种方式每个类都有明确的单一职责,构造函数简单明了,同时功能可以按需组合使用以及测试变得简单(可以单独测试每个组件)。

 

特例

某些情况下,构造函数除了赋值,还可以做一些其他工作也是合理的,如下:

参数合法性检查

在构造函数中进行基本的参数验证是合理的,这确保对象从创建之初就处于有效状态,例如下面例子,只要构造函数不进行外部依赖操作或复杂的逻辑运算都是合理的

class User:def __init__(self, id, name, email):# 基本参数验证if id <= 0:raise ValueError("User ID must be positive")if not name or not name.strip():raise ValueError("User name cannot be empty")if not email or "@" not in email:raise ValueError("Invalid email format")self.id = idself.name = nameself.email = email

 

简单的派生值计算

有时,在构造函数中计算一些简单的派生值是合理的,只要在整个类声明周期,计算后的值都不变:

class Rectangle:def __init__(self, width, height):if width <= 0 or height <= 0:raise ValueError("Dimensions must be positive")self.width = widthself.height = height# 简单的派生值计算self.area = width * heightself.perimeter = 2 * (width + height)

 

不可变对象的初始化

对于不可变对象(创建后状态不能改变的对象),构造函数需要完成所有必要的初始化工作:

class ImmutablePoint:def __init__(self, x, y):self._x = xself._y = y# 预计算常用值self._distance_from_origin = (x**2 + y**2)**0.5@propertydef x(self):return self._x@propertydef y(self):return self._y@propertydef distance_from_origin(self):return self._distance_from_origin

 

 

小结

一个设计合理的构造函数,是打造易维护、易测试、易扩展系统的基础。我们应始终坚持构造函数「仅做赋值和必要的基础验证」这一原则,使代码更为清晰和灵活。

简单的构造函数能带来以下优势:

  • 易于维护:职责单一、副作用少,便于后续的调试与迭代。
  • 易于测试:不依赖外部环境,能轻松实现模拟和单元测试。
  • 架构更清晰:便于实现依赖注入,更符合SOLID原则,也能更快地识别设计上的问题。

当我们发现构造函数开始复杂化,参数越来越多时,这通常是代码设计本身出现了问题,而不是一个能用Builder模式等技巧快速掩盖的问题。正确的做法是退一步重新审视类的职责,及时进行重构。

当然,在实际编码过程中,有时候我们可能会做出一定程度的妥协,例如对参数进行基本合法性检查、简单的数据派生计算,或者初始化不可变对象。这些情况应该是少数的例外,而不是普遍的规则。

总之,通过保持构造函数的简洁和直观,我们不仅能够写出高质量的代码,更能及早发现和解决潜在的设计问题,使整个系统更加稳固和易于维护。

 

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【分享】Ftrans内外网文件摆渡系统:让数据传输更安全更可靠!

随着大数据时代的到来,数据的重要性日渐得到重视,数据作为数字经济时代下的基础性资源和战略性资源,是决定国家经济发展水平和竞争力的核心驱动力。以行业为维度来看,数据泄露已发生在并影响了各个行业,全球范围内,各行业发生数据泄露的数量和损失都在增加。很多企业为了…

地球无法承受 AI,是时候踩刹车了

作者:Kollibri terre Sonnenblume公有领域艺术作品,作者提供,来自公共领域元素。**前言: **如果你不想阅读完整篇,这里是本篇的作者的核心观点:人工智能(AI)虽然在技术上有巨大的潜力,但它对环境的负面影响极其严重,可能加剧当前面临的多重危机,如气候变化、资源枯竭…

VMware ESXi 8.0U3d macOS Unlocker OEM BIOS 集成驱动版,新增 12 款 I219 网卡驱动

VMware ESXi 8.0U3d macOS Unlocker & OEM BIOS 集成驱动版,新增 12 款 I219 网卡驱动VMware ESXi 8.0U3d macOS Unlocker & OEM BIOS 集成驱动版,新增 12 款 I219 网卡驱动 VMware ESXi 8.0U3d macOS Unlocker & OEM BIOS 集成网卡驱动和 NVMe 驱动 (集成驱动版…

Gitea Enterprise 23.6.0 (Linux, macOS, Windows) - 本地部署的企业级 Gti 服务

Gitea Enterprise 23.6.0 (Linux, macOS, Windows) - 本地部署的企业级 Gti 服务Gitea Enterprise 23.6.0 (Linux, macOS, Windows) - 本地部署的企业级 Gti 服务 The Premier Enterprise Solution for Self-Hosted Git Service 请访问原文链接:https://sysin.org/blog/gitea/…

Autodesk Maya 2026 Multilanguage (macOS, Windows) - 三维动画和视觉特效软件

Autodesk Maya 2026 Multilanguage (macOS, Windows) - 三维动画和视觉特效软件Autodesk Maya 2026 Multilanguage (macOS, Windows) - 三维动画和视觉特效软件 三维计算机动画、建模、仿真和渲染软件 请访问原文链接:https://sysin.org/blog/autodesk-maya/ 查看最新版。原创…

Autodesk AutoCAD 2026 (macOS, Windows) - 自动计算机辅助设计软件

Autodesk AutoCAD 2026 (macOS, Windows) - 自动计算机辅助设计软件Autodesk AutoCAD 2026 (macOS, Windows) - 自动计算机辅助设计软件 计算机辅助设计 (CAD) 软件 请访问原文链接:https://sysin.org/blog/autodesk-autocad/ 查看最新版。原创作品,转载请保留出处。 作者主页…

VMware Aria Operations for Logs 8.18.3 新增功能简介

VMware Aria Operations for Logs 8.18.3 新增功能简介VMware Aria Operations for Logs 8.18.3 - 集中式日志管理 请访问原文链接:https://sysin.org/blog/vmware-aria-operations-for-logs/ 查看最新版。原创作品,转载请保留出处。 作者主页:sysin.org集中式日志管理 VMwa…

在 VS Code 中,一键安装 MCP Server!

大家好!我是韩老师。 本文是 MCP 系列文章的第三篇。之前的两篇文章是: Code Runner MCP Server,来了! 从零开始开发一个 MCP Server!经过之前两篇文章的介绍,相信不少童鞋已经用上甚至开发起了第一个 MCP Server。 不过呢,还是遇到一些童鞋在安装/配置 MCP Server 的时…

读DAMA数据管理知识体系指南36元数据管理概念(上)

读DAMA数据管理知识体系指南36元数据管理概念(上)1. 业务驱动因素 1.1. 可靠且良好管理元数据有助于1.1.1. 通过提供上下文语境和执行数据质量检查提高数据的可信度1.1.2. 通过扩展用途增加战略信息(如主数据)的价值1.1.3. 通过识别冗余数据和流程提高运营效率1.1.4. 防止使…

AMD CDNA介绍(上)

AMD CDNA介绍 AMD CDNA处理器采用并行微架构,旨在为通用数据并行应用提供一个出色的平台。需要高带宽或计算密集型的数据密集型应用程序,这是在AMD CDNA处理器上运行的候选者。 AMD CDNA生成系列处理器的框图,如图5-10所示。图5-10 AMD CDNA生成系列处理器的框图 CDNA设备包…

AMD Instinct™MI300系列微架构杂谈

AMD Instinct™MI300系列微架构 AMD Instinct MI300系列加速器基于AMD CDNA 3架构,旨在为HPC、人工智能(AI)和机器学习(ML)工作负载提供领先性能。AMD Instinct MI300系列加速器非常适合极端的可扩展性和计算性能,可以在单个服务器到世界上最大的EB级超级计算机的所有设备…

在Eager模式下对Llama 2 7B模型进行性能评估技术

在Eager模式下对Llama 2 7B模型进行性能评估 指定--compile none以使用Eager模式。 1)--compile:设置为none以使用Eager模式 2)--profile:启用torch.profiler的跟踪功能 3)--checkpoint_path:检查点路径 4)--prompt:输入提示 5)--max_new_tokens:最大新的token数 6)…