可扩展性是系统设计中一个关键概念,它指的是系统为了适应未来需求的变化,具备的一种扩展能力。这意味着当新需求出现时,系统可以通过最小的或没有修改来支持这些需求,而无需进行全面的重构或重建。随着软件系统的固有多变性,不断有新需求提出,这使得可扩展性成为软件开发中的一个重要考虑点。面向对象的编程思想和设计模式的发展,都是为了更好地应对和解决可扩展性的挑战。设计模式的广泛应用显示了对可扩展性的高度重视,几乎成为了每一位技术专家的共识。
为了构建一个具有良好可扩展性的系统,主要需要满足两个前提条件:准确预测未来的变化,以及有效封装这些变化。然而,实现这两个条件并非易事,下面我将详细探讨这个主题。
与硬件或建筑项目不同,软件系统的一个显著特点是其发布后仍然能够持续进行修改和更新。这一特性意味着软件系统需要不断地适应和实现新的需求。理想情况下,如果能够在不修改现有代码或仅通过少量修改来满足这些新需求,对所有相关方来说无疑是最佳场景。反之,如果每出现一个新需求就需要对系统进行大规模改动,不仅成本高昂,而且开发人员、产品经理、甚至老板都会感到不满——这种频繁的大幅度修改既耗时又耗力。因此,架构设计的一个关键目标是尽可能预见未来的变化,并设计出能够灵活适应这些变化的架构,使得当新需求出现时,可以轻松地说:“我们已经考虑到了这一点,现有架构可以轻松支持这个新功能,仅需几天的工作量。”
然而,现实往往远比理想复杂。正如一句古老的谚语所言:“唯一不变的是变化本身”。这意味着在架构设计时,考虑到可扩展性变得尤为重要。比如,在设计一个后台管理系统时,如果选择使用MySQL作为数据库,是否需要预留空间以便将来可能切换到Oracle?在决定使用HTTP作为接口协议时,是否需要考虑未来可能支持ProtocolBuffer?甚至更进一步,是否需要考虑VR技术可能带来的影响,以确保架构的长期可扩展性?如果尝试预测和准备每一个可能的变化,架构师可能会感到不堪重负,导致设计过于庞大而难以实施。但另一方面,如果完全不进行未来规划,新需求的到来可能会迫使系统进行重构,这同样意味着前期的投入和努力可能会付之东流。
应对变化
第一种应对变化的常见方案是将“变化”封装在一个“变化层”,将不变的部分封装在一个独立的“稳定层”
图片
无论采取哪种形式,通过剥离变化层和稳定层的方式应对变化,都会带来两个主要的复杂性相关的问题。
区分变化与稳定层
在系统架构设计中,识别哪些部分容易发生变化(变化层)与哪些部分相对稳定(稳定层)是关键的第一步。然而,区分这两层并非总是直接明了的,比如不同的数据库选择或接口协议可能容易识别,但在实际情况中,不同设计者可能对哪些层次属于变化层,哪些属于稳定层有不同的见解。这种差异可能会在架构审查过程中引发激烈的讨论。
设计变化层与稳定层间的接口
接口的设计是连接变化层与稳定层的桥梁,对于确保系统的整体稳定性和可扩展性至关重要。稳定层的接口应当尽可能的稳定,而对于变化层,设计一个能够适应不同实现方式并在引入新功能时仍保持兼容性的接口则更加复杂。以数据库为例,不同数据库(如MySQL、Oracle、DB2)之间在某些操作(如数据插入或更新)的实现上可能存在差异,这就需要在设计存储层访问接口时做出选择:是采用特定数据库的实现方式,还是设计一个能够自适应不同数据库特性的通用接口?这个例子揭示了设计接口时需要面对的挑战。
另一种常用的方法来应对系统变化是区分“抽象层”与“实现层”。在这种架构策略中,抽象层保持稳定,为系统的核心和通用功能提供定义,而实现层则具有可变性,可以根据不同的业务需求进行定制化开发。当需要引入新功能时,仅需添加新的实现即可,而不需要对抽象层进行修改。设计模式和规则引擎就是这种策略的经典实践案例。鉴于大多数技术专业人士对设计模式已相当熟悉,我将以设计模式为例进一步阐述这种方法的复杂之处。
装饰者模式提供了一种相较于传统继承更为灵活的方式来扩展功能。以《设计模式》一书中的“TextView”类示例为例,通过使用装饰者模式,可以非常灵活地为TextView添加各种额外功能,如边框、滚动条、背景图片等,而这些功能的组合并不会影响到基本的实现规则,只需遵循装饰者模式的设计即可实现。然而,与传统的类实现相比,装饰者模式的确引入了更多的复杂性。原本可能通过单个函数或类就能完成的任务,现在需要分解成多个类,并且这些类之间的关系和调用方式都必须遵循装饰者模式的设计原则。
同样,规则引擎的设计理念与设计模式持有相同的目标——通过灵活的设计达到系统的可扩展性。然而,这种“灵活性”本身就带来了设计上的复杂性。不仅如此,仅仅是要彻底理解并掌握23种设计模式本身就是一个挑战。