在软件工程的实际操作中,我常常遇到这样一种现象:本可以用简单代码解决的问题,却因为设计者过分关注“通用性”、“可维护性”和“可扩展性”而变得不必要地复杂,难以理解。
他们的思维方式是这样的:“这段代码未来可能会在更多场景中使用,所以我现在应该考虑它的可扩展性。”结果,他们在代码中加入了各种“框架结构”,以便在未来有新需求时,无需修改就能在新的地方使用。
“通用性”的价值与误区
作者并不否认“通用性”的价值,我的一些程序也具有很高的通用性。然而,很多人所谓的“通用性”实际上适得其反,这种现象通常被称为 “过度设计” 。关于过度设计,有一个有趣的故事:
传说在上世纪60年代美国和俄罗斯的“太空竞赛”期间,NASA 遇到了一个严重的技术问题:宇航员需要一支能在外太空真空环境中书写的笔。最终,NASA 花费了150万美元开发了这样一支笔。不幸的是,这种笔在市场上并不畅销。
俄罗斯人也面临同样的问题,他们则用了铅笔。
虽然这个故事是虚构的,但它具备了伊索寓言的力量。现在让我们看看软件行业,可能会发现:
代码需要“重用”的场合比您想象的要少得多。
许多人在写程序时,连“当前异常”都处理不好,却关注“未来的需求”。他们总是想象别人会重用这段代码。然而,实际上,由于设计过于复杂,理解这些设计所需的心智努力已经超过了从头开始的成本。因此,大多数人根本不会使用他们的代码,而是重新写一个。有些人最终会发现自己甚至看不下去之前写的代码,更愿意删除它并重新开始,而不是谈论重用。
我们用一个简单的例子来说明这一点。假设我们需要写一个简单的程序来计算两个整数的和:
def add(a, b):
return a + b
result = add(3, 5)
print(result) # 输出 8
这个函数非常直观,可以很好地满足当前需求。然而,一些程序员可能会考虑未来可能需要更多的计算能力,因此设计了一个复杂的框架:
class Operation:
def execute(self, a, b):
raise NotImplementedError
class AddOperation(Operation):
def execute(self, a, b):
return a + b
class OperationFactory:
@staticmethod
def get_operation(op_type):
if op_type == 'add':
return AddOperation()
# 可以添加更多操作类型。
raise ValueError("不支持的操作类型")
operation = OperationFactory.get_operation('add')
result = operation.execute(3, 5)
print(result) # 输出 8
虽然这个设计提供了可扩展性,但在当前只需计算两个整数和的场景中,这个设计无疑增加了代码的复杂性和理解成本。
实际修改代码所需的工作量比你想象的少
还有一种情况是,这些设计为“共享”而写的代码在很多地方并没有被使用,因此即使你完全手动修改它们,也不会花费很多时间。现在,随着 IDE 技术的发展和各种高级重构工具的出现,批量代码修改不再特别麻烦。过去需要在逻辑层面设计可维护性,现在只需在 IDE 中点击几下就能轻松完成。因此,在考虑设计框架之前,你还应该考虑这些因素。
例如,在上面提到的复杂设计中,如果我们需要修改加法操作,我们需要修改多个类和文件。在这种情况下,我们可能会发现简单的函数实现更容易维护和修改。
“考虑”通用性并不意味着你已经准确“掌握”了通用性
许多人考虑通用性,但他们并不准确地看到未来可能需要修改的部分,因此他们的设计往往错失重点。当新需求出现时,发现最初认为可能变化的部分并没有变化,而那些认为不变的部分却发生了变化。
能够准确预测未来需求并从代码中抽象出真正通用的框架是非常困难的任务。它不仅需要编程技能,还需要强大的观察现实世界事物的能力。许多人设计的框架只是复制他人的经验,无法适应实际需求。Java 世界中的许多设计模式就是由这些半吊子的人创造的。
例如,假设我们需要添加一个新的操作,如减法。如果我们没有准确掌握哪些部分需要通用设计,我们可能会发现现有框架不适用于新需求:
class SubtractOperation(Operation):
def execute(self, a, b):
return a - b
operation = OperationFactory.get_operation('subtract')
result = operation.execute(10, 3)
print(result) # 输出 7
在这里,我们需要修改 OperationFactory 类以支持减法操作。这表明,虽然我们考虑了通用性,但我们没有准确掌握未来的需求,导致框架的灵活性有限。
初始设计的复杂性
如果在初始设计中过早地考虑未来需求,可能会导致不必要的复杂性和问题。因此,这种对未来变化的考虑阻碍了进展。原本如果我们专注于解决当前问题,可以取得很好的结果。然而,由于“通用性”带来的复杂性,设计者每次都要多费一些心思,无法创建优雅的程序。
例如,在上面提到的复杂设计中,如果我们只需要一个简单的加法操作,复杂的框架反而使初始设计变得臃肿且难以理解:
class Operation:
def execute(self, a, b):
raise NotImplementedError
class AddOperation(Operation):
def execute(self, a, b):
return a + b
class OperationFactory:
@staticmethod
def get_operation(op_type):
if op_type == 'add':
return AddOperation()
raise ValueError("不支持的操作类型")
# 初始设计的复杂性。
operation = OperationFactory.get_operation('add')
result = operation.execute(3, 5)
print(result) # 输出 8
相比之下,只需执行一个简单的函数即可。
def add(a, b):
return a + b
result = add(3, 5)
print(result) # 输出 8
理解和维护框架代码的开销
如果你设计了一个框架式的代码,每个程序员都需要理解这个框架的构建,才能在这个框架下编写代码,这带来了学习成本。一旦发现这个框架有设计问题,依赖它的代码可能需要修改,这带来了修改成本。因此,在设计中加入“通用性”的初衷是为了节省未来的修改成本,但可能会增加当前的开发和维护成本。
例如,在复杂的框架设计中,添加新操作类型需要理解多个类及其关系:
class MultiplyOperation(Operation):
def execute(self, a, b):
return a * b
# 理解和维护的成本。
operation = OperationFactory.get_operation('multiply')
result = operation.execute(3, 5)
print(result) # 输出 15
而在简单的函数实现中,添加新功能相对简单:
def multiply(a, b):
return a * b
result = multiply(3, 5)
print(result) # 输出 15
结论
在软件工程中,设计的“通用性”确实是一个重要的考虑因素,但我们必须谨慎。过度设计和过度工程不仅不会提高代码的可维护性,反而会增加开发和维护成本。在实际项目中,我们应该根据当前需求采用最简单直接的解决方案,而不是为了未来的可能性增加当前的复杂性。
总的来说,简洁直观的代码往往比复杂的框架更能满足实际需求。我们应在设计中保持平衡,避免过度工程,专注于解决当前问题,同时为未来扩展留有余地。