`

组合与继承(翻译)

阅读更多

这时一篇关于面向对象程序(软件对我来说还太宽泛)中组合和继承的概念比较文章翻译,原文:http://www.artima.com/designtechniques/compoinh.html,翻译的不好请见谅.

 

组合与继承-组织类关系的两种基本方法,Bill Venners,于1998年10月发布于javaworld

 

        摘要

 

        在我的Desion Technique系列的这个部分中,我对组合和继承的可扩展性及性能进行分析,并且提出几点使用这两种方法的准则.

 

软件系统设计的一个基本步骤是明确不同类间的关系.这其中有两种基本的方法即继承和组合,尽管在你使用继承的时候编译器和虚拟机帮你完成了绝大部分工作,但你依然可以使用组合来实现继承的效果.这篇文章就对两种方式进行了比较以及展示了一些使用它们的准则或者前提.

 

关于继承

在通篇文章里,我准备采用下面这个例子进行示例说明:

 

class Fruit {

    //...
}

class Apple extends Fruit {

    //...
}
 

 

这个例子里,类Apple继承于Fruit,Fruit是Apple的父类,这里我不讨论多继承的情况,我会在下个月的Desion Technique的接口设计部分中谈及.这里是关于Apple和Fruit类的uml图:

Figure 1. The inheritance relationship

关于组合

在这里的"组合"仅仅意味着一个引用其它对象的实例变量,如下:

 

class Fruit {

    //...
}

class Apple {

    private Fruit fruit = new Fruit();
    //...
}

 

 这个示例表示Apple和Fruit类为组合关系,因为Apple中的实例变量引用自一个Fruit对象,Apple类被称作"前端"类,Fruit为"后台"类,在组合关系中,"前端"包含一个对"后台"类实例的引用.uml如下图:

Figure 2. The composition relationship

 

动态绑定,多态和变化

当你在两个类之间确定的继承关系,你就获得了动态绑定和多态这两个优点.动态绑定意味着虚拟机在运行时决定对象需要执行的方法指令(子类或者父类),多态意味着你能用一个父类的声明引用不同子类的对象实例.动态绑定和多态的一个主要好处是代码的更改更加简单.当你有一段代码中准备使用很多父类引用(就像是Fruit类引用),你在随后的过程就能创建一批它的子类而无需更改那段引用代码,动态绑定会确保引用的实际对象方法被正确执行,即使你没有子类实例,你也可以能使用父类进行引用并且保证能正确编译,因此,继承能因为添加一个子类而达到代码更改方便的目的,然而,不是只有继承才具有这个效果.

 

更改父类接口

在继承关系中,父类往往是"脆弱的",因为它的一点点改动都会波及到所有包含该类型引用的地方,准确地说,父类的"脆弱"体现于它所公开的接口.如果父类采用良好面向对象设计以至于每一个接口都相当清晰和职责划分清楚,那么任何的实现改动都不应该波及其它,但是这样还是会涉及到对于该父类引用的地方,更进一步地说,父类的接口更改会破坏任何具有相同声明的子类代码.

例如,假如你更改Fruit类中的返回类型,会破坏任何对Fruit类引用或者Apple类引用的代码,更多的,它破坏了任何覆盖该方法的子类代码以至于这些地方都会编译失败,直到你更改了每一除引用以及引用的可能直接引用.有时候也会称继承提供的封装为"弱封装",因为你会由于父类的更改而导致子类的代码(Apple类).继承一个优点就是重用父类代码.就像是如果Apple如果没有实现Fruit的方法,Apple的行为就会和父类Fruit保持一致,但是Apple类仅仅是弱封装了父类Fruit的行为,以达到代码复用的目的,Fruit接口的任何更改都会破坏Apple类的行为.

 

从继承到组合

考虑到继承关系使父类对于接口的更改的变得困难,可以尝试着另一种方式来实现继承的目的-组合.它被证明为当你想使用代码重用时的更好的选择.

 

        通过继承实现的代码重用

        为了对继承和组合在代码复用这部分进行更好的阐述,考虑到下面的简单示例:

 

class Fruit {

    // Return int number of pieces of peel that
    // resulted from the peeling activity.
    public int peel() {

        System.out.println("Peeling is appealing.");
        return 1;
    }
}

class Apple extends Fruit {
}

class Example1 {

    public static void main(String[] args) {

        Apple apple = new Apple();
        int pieces = apple.peel();
    }
}

 

 当你运行Example1程序,由于Apple继承了Fruit的peel()方法,它会打印出"Peeling is appealing.".如果在未来的某个时间你想把peel()方法的返回值更改为Peel类型,你就会导致Example1程序无法编译,只要你没有明确通过Fruit实例调用peel方法,任何的peel()方法改动都会出现上述问题:

 

class Peel {

    private int peelCount;

    public Peel(int peelCount) {
        this.peelCount = peelCount;
    }

    public int getPeelCount() {

        return peelCount;
    }
    //...
}

class Fruit {

    // Return a Peel object that
    // results from the peeling activity.
    public Peel peel() {

        System.out.println("Peeling is appealing.");
        return new Peel(1);
    }
}

// Apple still compiles and works fine
class Apple extends Fruit {
}

// This old implementation of Example1
// is broken and won't compile.
class Example1 {

    public static void main(String[] args) {

        Apple apple = new Apple();
        int pieces = apple.peel();
    }
}

 

        通过组合实现的代码复用

        组合提供Apple类另一种对Fruit类的方法peel()的复用.我们能通过声明一个Fruit的实例对象并且定义一个它自己的peel()方法(仅仅是对Fruit实例peel()方法的调用封装),如下代码:

 

class Fruit {

    // Return int number of pieces of peel that
    // resulted from the peeling activity.
    public int peel() {

        System.out.println("Peeling is appealing.");
        return 1;
    }
}

class Apple {

    private Fruit fruit = new Fruit();

    public int peel() {
        return fruit.peel();
    }
}

class Example2 {

    public static void main(String[] args) {

        Apple apple = new Apple();
        int pieces = apple.peel();
    }
}

 

 在这种组合的方式里,子类成为了"前端"类,父类是"后台"类,在继承中.子类隐士继承父类所有非私有方法.相对的,在组合中"前端"类必须在它自己的实现方法中明确调用"后台"类的对应方法.这个明确的调用有时候称"后台"类的"请求定向"或者"委托".组合由于"后台"类的任何改动不会破坏"前端"类的代码而提供了更强烈的代码复用,就像是更改Fruit的peel()方法不会强制Example2的代码,一种可能的改动如下:

 

class Peel {

    private int peelCount;

    public Peel(int peelCount) {
        this.peelCount = peelCount;
    }

    public int getPeelCount() {

        return peelCount;
    }
    //...
}

class Fruit {

    // Return int number of pieces of peel that
    // resulted from the peeling activity.
    public Peel peel() {

        System.out.println("Peeling is appealing.");
        return new Peel(1);
    }
}

// Apple must be changed to accomodate
// the change to Fruit
class Apple {

    private Fruit fruit = new Fruit();

    public int peel() {

        Peel peel = fruit.peel();
        return peel.getPeelCount();
    }
}

// This old implementation of Example2
// still works fine.
class Example1 {

    public static void main(String[] args) {

        Apple apple = new Apple();
        int pieces = apple.peel();
    }
}

 

 这个示例展示了"后台"类的更改只会波及到"前端"类而导致对Apple的peel实现进行了更改,但是Example2还是无需改动的.

 

组合和继承的比较

结合上面的示例,下面列举一下组合和继承的优缺点:

 

  • "后台"类相比于父类在接口的更改上更加方便.就像前面的例子一样,"后台"类的更改要求"前端"类的的实现作出相应更改,但是这种更改不应用于"前端"类的接口.依赖于"前端"类调用的那些代码直到它自己的接口更改之前都能正常工作.相反,对于父类的接口更改不仅会波及它的子类,也会设计到对于这些依赖的调用代码.
  • "前端"类的接口更改较之于子类更加简单(父类的脆弱导致的子类的过分依赖),你不能在没有确保更改后的新接口的返回类型兼容于更改之前返回类型.例如,你不能声明一个和父类方法签名一样但是返回值不一样的方法.而组合,能在不影响"后台"类的前提下更改"前端"类接口
  • 组合能使你延后对于"后台"类实例的创建直至他们真的需要被用到,以及动态更改在"前端"对象生命周期中类型.而对于继承,父类对象的创建是立即的,它在子类的创建时就已经被创建了而且它成为了子类声明周期的一部分(无法销毁...)
  • 继承相比于组合更容易添加子类实现多态.假如你的新类中有部分行为一定会在父类中实现.这对组合来讲是不可能的,除非你通过接口进行组合逻辑.
  • 组合中的明确的方法调用或者委托较之于继承会往往会导致性能损失,这里用"往往(often)"是因为影响性能的因素很多,组合的方法调用可能只占及其微小.
  • 不管是组合或者继承,更改实现都是很方便的.波及范围只会局限在内部的类中(子类,"前端"类)
组合还是继承?
说了这么多,我们应该应该在设计中如何设计组合或是继承呢?这里有一些关于到底是选择组合还是继承的指导意见.

        确保继承具有"is-a"的关系
        我对于是否选择继承有一个主要的原因就是因为子类是否是一个父类即Apple一定是Fruit,所以我倾向于使用继承关系描述这两个类.一个选择继承关系的重要准则就是是否在两者间存在"is-a"关系,不管是其中一个对象在整个生命周期中使用另一个对象还是仅仅在某些代码段.例如,你可能觉得一个Employee在某些情况是一个Person,但是如果在另一些应用场景中,employee不是一个person而是一个manager怎么办?在这些情况下,可能使用组合就更实用.

        别仅仅因为代码复用而采用继承.
        
        别仅仅因为多态而采用继承.

 

 

分享到:
评论

相关推荐

    语法制导翻译及中间代码生成1

    2. **自顶向下翻译:**从语法树的根节点开始,按照递归下降的方式解析并翻译,常与LL(1)等解析技术结合。这种方式往往需要解决左递归和左公因子等问题。 **中间语言:** 中间代码是编译器内部使用的抽象表示,它...

    魔王语言翻译器源代码C

    面向对象编程(OOP)是一种常见的软件开发方法,它的核心概念包括类、对象、封装、继承和多态。 在C语言中实现面向对象编程并不像在C++或Java那样直接,因为C本身并不原生支持这些特性。然而,通过结构体和指针,...

    《javascript设计模式》学习笔记二:Javascript面向对象程序设计继承用法分析

    构造函数继承(类式继承,组合继承,伪经典继承) b.原型继承 c.原型赋值(遍历)继承(寄生式继承) 2.构造函数继承 所谓的构造函数继承,就是通过创建一个新对象,调用父类构造函数实现的一种继承

    Python-探索Flask中文翻译教程

    Flask基于WSGI(Web Server Gateway Interface)协议,它不是一个完整的Web框架,而是一个微框架,允许开发者根据需求自由选择和组合各种库。Flask的核心包括一个应用程序上下文、请求上下文和URL路由系统。通过定义...

    计算机外文翻译.doc

    【标题】:“计算机外文翻译.doc”涉及到的主题是MySQL与JSP在Web应用程序中的整合,以及JSP的基础和历史。 【描述】:文档主要讲解了如何利用MySQL数据库和JSP(JavaServer Pages)来构建数据驱动的Web应用程序,...

    Java翻译[参考].pdf

    在设计类时,选择继承还是聚合/组合取决于具体需求: - **继承**(is-a关系):当类之间存在概念上的层次关系,比如“狗”是“动物”的一种,可以使用继承。继承可以共享属性和方法,但过度使用可能导致类层次过于...

    计算机C语言外文翻译外文文献英文文献面向对象和C

    计算机C语言外文翻译外文文献英文文献面向对象和C 计算机C语言外文翻译外文文献英文文献面向对象和C是一个重要的概念,在计算机科学领域中扮演着至关重要的角色。面向对象编程是一种编程范例,它强调模块化、抽象、...

    Design+Patterns+Explained 设计模式解析翻译

    9. **组合模式**:允许你将对象组合成树形结构来表现“整体/部分”层次结构,使用户对单个对象和组合对象的使用具有一致性。 10. **装饰器模式**:在不改变对象自身的基础上,在运行时给对象添加新的行为或责任。 ...

    Visual_Studio.NET相关词汇中英翻译

    ### Visual_Studio.NET相关词汇中英翻译解析 #### 抽象类 (Abstract Class) 抽象类是一种特殊类型的类,它不能被实例化。抽象类的主要用途是作为其他类的基础类,这些派生类可以继承抽象类中的成员,并实现其中...

    外文翻译---Java的思考.docx

    这种通过继承和组合来构造新对象的方式是OOP中的一个关键概念,它让构建复杂系统变得更加容易和高效。 **类型与类** 在Java中,每个对象都属于某个特定的类型,类型是在创建对象时由类定义的。类作为对象的模板,...

    JAVA编程术语英语翻译.pdf

    21. **assembly** - 组合语言,低级编程语言,直接对应机器指令。 22. **assertion** - 断言,用于测试代码假设,确保程序状态正确。 23. **assign** - 赋值,将值从一个位置移动到另一个位置。 24. **assignment** ...

    java英文翻译

    7. **类设计**:遵循面向对象原则,如封装、继承和多态。合理使用访问修饰符(public, private, protected),以控制成员的可见性。 8. **导入**:避免使用通配符导入,如`import java.util.*`,以减少命名冲突。只...

    计算机专业术语50个翻译.pdf

    GUI是一个图形用户界面,提供了用户与计算机之间的交互界面。它是一个图形化的界面,提供了按钮、菜单、对话框等界面元素。 9.automatic string completion 自动字符串补全 自动字符串补全是一个编辑器功能,提供...

    scala编程电子书

    10. 组合与继承:Scala允许通过组合和继承的方式来构建模块化和可复用的程序结构。 11. 特质:特质在Scala中用于表示可以混入的接口,提供了代码复用的方式。 12. 包和引用:Scala支持包的使用来组织代码,同时...

    计算机专业外文翻译THINKINJAVA.pdf

    Scala使用单一继承,但通过特质(trait)可以实现类似接口的功能,同时支持多重特质组合。 通过这个简单的类定义,我们可以看到Scala在保持面向对象特性的同时,也引入了函数式编程的元素,比如无副作用的纯函数。...

    Lisp中文翻译学习资料.rar

    7. **CLOS(Common Lisp Object System)**: Common Lisp的面向对象系统,支持多重继承、方法组合和通用方法,提供了强大的面向对象编程能力。 **ANSI Common Lisp中文翻译版** "ANSI Common Lisp 中文翻译版.pdf...

    第六七章进度检查题-有答案1

    在语法制导翻译中,继承属性的计算通常在非终结符的产生式前端,而综合属性的计算在后端。这样可以确保属性值的正确传播和计算。 7. **自下而上的分析与综合属性计算**: 自下而上的分析,如规范规约,可以用来...

Global site tag (gtag.js) - Google Analytics