在传统的面向对象 UI 框架中,控件往往通过继承基类来扩展功能。例如经典的 WinForms 中,新控件通常继承自 Control 类。但这种继承策略容易导致深层次的类层次,难以灵活扩展。为了解决这一问题,各平台的后续框架逐渐引入了组合优于继承的思想:通过组合小型组件或模板来构建复杂界面,提升灵活性和可复用性。下面按平台分析主流框架的设计思路及演进规律。

经典桌面框架:WinForms 与 WPF

  • WinForms(.NET):WinForms 基于 Windows 传统消息循环,将 UI 控件设计为 Control 基类及其子类。新控件通常继承已有控件或重写 Control,以复用已有功能。但当需要复合功能时,常用 组合 方式:将多个基础控件(如 TextBoxButtonListBox 等)放入一个容器(Form 或 UserControl),形成一个新的复合控件。例如下拉框就是由文本框、按钮和列表等控件组成的。

  • WPF (.NET):WPF 提供了两种创建控件的模型。其一,UserControl(组合已有控件):开发者在 XAML 中把多个标准控件组合起来,实现简单复用和封装;但此种方式不能使用数据模板(DataTemplate)或控件模板(ControlTemplate)进行外观定制。其二,Custom Control(继承):通过继承 Control 类创建控件,控件的外观由 ControlTemplate 定义,从而彻底分离逻辑与表现,支持主题和样式定制。也就是说,WPF 既保留了继承扩展的能力,又通过模板和依赖属性等机制引入了更灵活的组合方式(比如在 XAML 中组合控件或使用附加属性),以克服纯继承的笨重和不灵活。

例如,微软文档指出:派生自 UserControl 的控件 是将现有组件添加到该用户控件中(通过 XAML 组合多个控件),但无法使用 DataTemplate/ControlTemplate 定制外观;而派生自 Control 的自定义控件则使用模板定义外观,实现逻辑与视觉的彻底解耦。 这意味着 WPF 鼓励使用控件组合(UserControl)进行快速开发,同时为需要高度可定制外观的场景提供继承+模板机制。

iOS/macOS 平台:UIKit 与 SwiftUI

  • UIKit/AppKit(传统 Cocoa 框架):UIKit (iOS) 和 AppKit (macOS) 中,视图对象都是从 UIView/NSView 继承而来。但在应用层面,这些框架更倾向于组合而非盲目继承。以 iOS 为例,苹果框架常通过委托(Delegate)和数据源(DataSource)模式来扩展功能,而不是子类化视图。例如,如果只想让 UITextField 限制某些输入字符,正确做法不是继承 UITextField,而是设置一个符合 UITextFieldDelegate 协议的委托对象,由文本框在输入时回调检查;同样,UITableView 不鼓励继承出 SongsTableView 这样的子类,而是通过组合一个数据源对象(通常是控制器)来向表格提供数据。正如开发者 Rob Napier 所言:“UIKit 中继承很少用到组合更常见”。UIKit 的大多数功能都通过标准视图和组件组合实现,只有在需要新增底层功能时才会考虑继承。例如开发者常用的 UIViewController 也是一个组合型的构造器,它把多个视图对象组合起来管理界面和交互。

  • SwiftUI(声明式框架):苹果于 2019 年推出 SwiftUI,一个面向所有 Apple 平台的声明式 UI 框架。在 SwiftUI 中,一切都是 View 协议的实例,界面通过函数组合来构建,而不是通过类继承。开发者可以通过 VStackListNavigationView 等容器,将简单视图(如 TextImage组合成复杂界面。直接说明:“SwiftUI 提供用于声明 App 用户界面的视图…通过 stacks、lists 等将…SwiftUI 视图组合起来”。这种组合方式意味着 UI 的结构更灵活,可读性更高,也更易测试。相比之下,UIKit 依赖的继承和 Interface Builder 链接(IBOutlet/IBAction)繁琐、易出错,而 SwiftUI 通过功能强大的“修饰符”和数据绑定等机制,以组合思想解决了可复用性和状态管理问题。简单来说,UIKit 的核心设计思想是继承,而 SwiftUI 的核心设计思想是组合(声明式)

Android 平台:传统视图体系 vs Jetpack Compose

  • 经典 Android View:Android 最初将所有 UI 元素都设计为继承自同一个 View 基类。Button、TextView、ImageView、ViewGroup 等都是 View 的子类,布局通过嵌套 ViewGroup 实现。这种设计最初方便代码重用——比如按钮不用各自实现文本渲染逻辑。然而,这种 深度继承 的策略很快显露出缺陷:当需求变化时,很难预测最佳的类层次结构。例如,如果要让按钮显示图片而非文字,Android 提供了 ImageButton,但 ImageButtonButton 虽名称相近却没有公用的扩展接口,经常出现调用无效的问题。更广泛地说,当框架要修改已有视图功能时,没有机制通知所有继承自它的子类去检查变化是否兼容,导致难以维护。正如 Louis Tsai 所述:“构建基于继承的 UI 随空间和时间的增长而失效”。

  • Jetpack Compose(声明式 UI):为了解决传统 View 系统的局限,Android 推出了 Jetpack Compose,一个基于 Kotlin 的声明式 UI 框架。在 Compose 中,界面通过组合函数构建:开发者调用各种内置或自定义的 Composable 函数(如 Text(), Button(), Column(), Row() 等)来描述界面,系统负责高效渲染。这种方法避免了笨重的类层次结构,让 UI 更可组合、更动态。正如 Louis Tsai 总结的那样,通过组合的方式构建按钮等控件:“假设 Button = 随意提示 + 背景 + 点击回调”(而不是在视图上做文本渲染的继承逻辑)。Jetpack Compose 借鉴了 Flutter、React 等框架的思路,强调组合优于继承:通过把功能细分为可复用的小组件(函数)并嵌套调用,来构建复杂的界面,从而解决了 Android 视图体系“继承难扩展”的问题。

Web 前端:组件化框架

现代 Web 前端框架(如 React、Vue、Angular 等)本质上也是组件化的设计,强调组合而非继承。以 React 为例,其官方文档直接指出:“React 有一个强大的组合模型,我们推荐使用组合而不是继承来在组件间重用代码”。开发者可以在 JSX 中将组件嵌套或者通过 props.children 传递子组件,实现复用和定制界面。Facebook 进一步说明,在他们的经验中,没有任何情况需要构建组件继承层次:组件间的灵活性完全可以通过组合和属性传递来实现。类似地,Angular 也以模块和组件为基础,通常通过依赖注入(services)将功能组装到组件中,而不是通过继承共享逻辑(即使存在基类组件,也经常被建议尽量简化,更多使用组合的方式)。总之,Web UI 领域的主流框架普遍摒弃深继承体系,转而提倡组件组合的开发模式。

游戏引擎:实体-组件架构

在游戏开发领域,实体-组件系统(Entity-Component-System, ECS)是经典的“组合优于继承”设计模式。在 ECS 中,一个游戏对象(Entity)本身不包含任何行为,所有功能都通过附加多个组件(Component)来实现;系统(System)对拥有特定组件的数据集进行统一处理。例如,在 Unity 引擎中,任何 GameObject 默认只有一个 Transform 组件,它表示位置/旋转/缩放。要让这个对象具备渲染、物理碰撞、音频等功能,需要往同一个 GameObject 上添加 Camera、Collider、AudioSource 等组件。Unity 官方手册指出:“没有组件的话,GameObject 只是内存中的一些信息——它在世界中就像不存在一样”;不同的组件“为 GameObject 提供附加功能”。也就是说,Unity 通过组合组件赋予对象能力,而不是通过在类层次中预定义所有可能的类型。更通用地,ECS 理念强调实体的行为由所拥有的组件而非类继承决定。这种模式彻底避免了传统游戏对象(如怪物、道具)间复杂的多层继承,极大增强了灵活性和可扩展性。

总结

综上所述,各主流 UI 框架都在继承与组合的博弈中逐步摸索出“组合优于继承”的架构范式。早期框架(WinForms、早期 AppKit/UIKit、Android 视图系统等)依赖继承来复用代码,但在实际应用中经常遇到可扩展性差、类层次脆弱的问题。后来随着经验积累,框架设计者引入了更多的组合机制:WPF 用模板(ControlTemplate/DataTemplate)将外观从逻辑中解耦;iOS 通过委托和协议替代子类化;Android 和 Web 则借鉴函数式、声明式思想,用组件或函数来动态构建界面;游戏引擎全面采用组件化实体模型。这些设计进化共同发现并解决了早期“继承体系过于僵化”的问题,形成了如今被广泛认可的更灵活、模块化的 UI 架构

参考文献: 已在正文中用 【…†L…】 标注了引用来源。每个引用均对应研究中实际查阅的文献。


<
Previous Post
使用 AOT 编译保护 .NET 核心逻辑,同时支持第三方扩展
>
Blog Archive
Archive of all previous blog posts