返回介绍

3.4 可扩展性问题

发布于 2025-02-20 00:17:07 字数 6247 浏览 0 评论 0 收藏 0

面向对象程序设计通常被认为是软件可扩展性方面的灵丹妙药。但是,“可扩展”究竟意味着什么呢?

可扩展性问题说的是如何定义数据类型(结构+操作),使之能够支持两种形式的扩展:添加新的表示变体,或添加新的操作。

这里,ADT 的意思遵从 Cook 的用法。然而我们需要澄清,这里对扩展性问题的讨论实际上将对象与变体类型(variant type)(即代数数据类型(algebraic data types))进行对比。我们关心的是可扩展的 实现 。这里不关心界面的抽象。

事实表明,ADT 和对象分别都能很好地支持可扩展性的一个维度,但是在另一维度就不行了。让我们用一个众所周知的例子来研究此问题:简单表达式的解释器。

3.4.1 ADT

先来考虑 ADT 的做法。表达式的数据类型有三种变体:

(define-type Expr
   [num  (n number?)]
   [bool (b boolean?)]
   [add (l Expr?) (r Expr?)])

接下来定义解释器,这是一个函数,用 type-case 处理抽象语法树:

(define (interp expr)
   (type-case Expr expr
      [num (n) n]
      [bool (b) b]
      [add (l r) (+ (interp l) (interp r))]))

这是一道很好的 PLAI 练习题。举个例子:

> (define prog (add (num 1)
                    (add (num 2) (num 3))))
> (interp prog)
6

扩展:新的操作

先来考虑给表达式添加一个新操作。除了对表达式进行解释,我们还想做类型检查,也就是确定它将算得的值的类型(在这里,是 numberboolean )。这很简单,但是能检测到解释过程中出现的失败的情况,比如对两个不是数字的东西进行相加操作:

(define (typeof expr)
  (type-case Expr expr
    [num (n) 'number]
    [bool (b) 'boolean]
    [add (l r) (if (and (equal? 'number (typeof l))
                        (equal? 'number (typeof r)))
                   'number
                   (error "类型错误:并非数"))]))

求一下之前那个程序的类型:

> (typeof prog)
'number

我们的类型检查器会拒绝不合理的程序:

> (typeof (add (num 1) (bool #f)))
类型错误:并非数

反思一下这个扩展案例,我们看到一切都很顺利。想要新的操作,我们只需要定义新的函数。这种扩展是模块化的,因为只需要在一个地方新加定义。

扩展:新的数据

接下来考虑另一个维度的可扩展性:添加新的数据变体。假设我们扩展这里的简单语言,增加新的表达式: ifc 。扩展后数据类型的定义是:

(define-type Expr
  [num  (n number?)]
  [bool (b boolean?)]
  [add (l Expr?) (r Expr?)]
  [ifc (c Expr?) (t Expr?) (f Expr?)])

修改 Expr 的定义加上这个新变体破坏了所有现有的函数定义! interptypeof 都不再成立,因为它们用 type-case 对表达式“按类型处理”,但是并没有处理 ifc 的情况。我们需要修改它们,加上对 ifc 的处理:

(define (interp expr)
  (type-case Expr expr
    [num (n) n]
    [bool (b) b]
    [add (l r) (+ (interp l) (interp r))]
    [ifc (c t f)
         (if (interp c)
             (interp t)
             (interp f))]))

(define (typeof expr)
  (type-case Expr expr
    [num (n) 'number]
    [bool (b) 'boolean]
    [add (l r) (if (and (equal? 'number (typeof l))
                        (equal? 'number (typeof r)))
                   'number
                   (error "类型错误:并非数"))]
    [ifc (c t f)
         (if (equal? 'boolean (typeof c))
             (let ((type-t (typeof t))
                   (type-f (typeof f)))
               (if (equal? type-t type-f)
                   type-t
                   (error "类型错误:两个分支的类型不同")))
             (error "类型错误:并非布尔值"))]))

程序是正确的:

> (define prog (ifc (bool false)
                    (add (num 1)
                         (add (num 2) (num 3)))
                    (num 5)))
> (interp prog)
5

这种情况下的可扩展性就不怎么样了。我们必须修改数据类型的定义,然后修改所有的函数。

总而言之,使用 ADT,添加新的操作(如 typeof )是模块化的所以很容易,但添加新的数据类型(例如 ifc )则不是模块化的所以非常麻烦。

3.4.2 OOP

对象在这些场景下表现如何?

我们从面向对象版本的解释器开始:

(define (bool b)
  (OBJECT () ([method interp () b])))

(define (num n)
  (OBJECT () ([method interp () n])))

(define (add l r)
  (OBJECT () ([method interp () (+ (-> l interp)
                                   (-> r interp))])))

请注意,遵循面向对象的设计原则,每个表达式对象都知道如何解释自己。程序中不存在某个中央解释器能处理所有的表达式。解释程序是通过给该程序发送 interp 消息来完成:

> (define prog (add (num 1)
                    (add (num 2) (num 3))))
> (-> prog interp)
6

扩展:新的数据

要添加新的数据,比如条件对象 ifc,可以简单地定义新的对象工厂,其中包含该新对象处理 interp 消息的定义:

(define (ifc c t f)
  (OBJECT () ([method interp ()
                      (if (-> c interp)
                          (-> t interp)
                          (-> f interp))])))

现在可以解释包含条件的程序了:

> (-> (ifc (bool #f)
           (num 1)
           (add (num 1) (num 3))) interp)
4

这表明,与 ADT 相反,使用 OOP 添加新类型的数据是直接的、模块化的:只需创建新对象即可。对比 ADT,这是明显的优势。

扩展:新的操作

但在得出结论,认为 OOP 是软件可扩展性的灵丹妙药之前,我们必须考虑另一种扩展场景:添加操作。假设我们和以前一样,需要检查程序的类型。这意味着表达式对象现在还需要理解“typeof”消息。要做到这一点,我们就必须修改所有的对象定义:

(define (bool b)
  (OBJECT () ([method interp () b]
              [method typeof () 'boolean])))

(define (num n)
  (OBJECT () ([method interp () n]
              [method typeof () 'number])))

(define (add l r)
  (OBJECT () ([method interp () (+ (-> l interp)
                                   (-> r interp))]
              [method typeof ()
                      (if (and (equal? 'number (-> l typeof))
                               (equal? 'number (-> r typeof)))
                          'number
                          (error "类型错误:并非数"))])))

(define (ifc c t f)
  (OBJECT () ([method interp ()
                      (if (-> c interp)
                          (-> t interp)
                          (-> f interp))]
              [method typeof ()
                      (if (equal? 'boolean (-> c typeof))
                          (let ((type-t (-> t typeof))
                                (type-f (-> f typeof)))
                            (if (equal? type-t type-f)
                                type-t
                                (error "类型错误:两个分支的类型不同")))
                          (error "类型错误:并非布尔值"))])))

程序是正确的:

> (-> (ifc (bool #f) (num 1) (num 3)) typeof)
'number
> (-> (ifc (num 1) (bool #f) (num 3)) typeof)
类型错误:并非布尔值

这个可扩展性场景下,我们被迫修改所有的代码才能添加新方法。

总而言之,对对象来说,添加新的数据类型(例如 ifc)模块化所以容易,但添加新的操作(例如 typeof)不模块化所以麻烦。

请注意,这就是 ADT 的对偶情况!

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文