`
songofhawk
  • 浏览: 26515 次
  • 来自: ...
社区版块
存档分类
最新评论

如何减少子类对超类的依赖——一个设计问题

    博客分类:
  • Java
阅读更多

一般来说,根据所谓好莱坞原则,我们不应该在子类中显示调用超类的方法,而是通过重写超类的方法来实现特殊的逻辑,以此来避免循环依赖。不过,调用超类中被重写的同名方法,通常是可以接受的,比如:

Class A
{
  public void go()
  {
     System.out.println("do by A");
  }
}

Class B extends A
{
  @Override
  public void go()
  {
     System.out.println("do by B");
     super.go();
  }
}

 

如果超类要做的事,总在子类之前,或者之后,是没有问题的,而一旦子类do的前后各需要一段公共的代码,这个办法就行不通了。

 

于是我们干脆把一个方法拆成两个,其中一个“大”方法调用另一个“小”方法,由子类去重写那个小方法,这样子类干脆就不用掉父类的方法了,显得更加纯粹:

 

abstract Class A
{
  public void go() //大方法
  {
     prepare()
     going();
     clearup()
  }

  abstract public void going(); //小方法

  protected void prepare()
  {
     System.out.println("prepare by A");
  }

  protected void clearup()
  {
     System.out.println("clearup by A");
  }
}

Class B extends A
{
  @Override 
  public void going() //重写小方法
  {
     System.out.println("do by B");
  }
}

 

问题是,如果Class B还有子类呢,它自己也需要在子类的执行逻辑前后插入一些东西,难道又把doing这个“小”函数拆开?这是不是太复杂了点?再说我都不知道怎么跟下面的“小小”函数起名字了wink 。当然,让子类反过来调Class B的函数更不好,不但违反了前面的“Don't call me”原则,而且本身就很麻烦——每个子类都得写。

 

有没有更好的办法呢?  看来这个问题没写清楚,引起了一些误会,有必要进一步解释一下。首先,我这里并不是想验证或者实践某种模式,确实是一个实际的开发项目遇到了需要权衡的地方。

 

目前我的设计大致是像下面的样子:

 

设计问题类图

 

只所以选用继承结构,是因为子类所代表的几个概念与父类在自然意义上确实是"is a"的关系,并且在子类间是互斥的;而且A/B/C这几个类都很稳定,可能发生的变化主要是B可能会增加子类B3,B4,或者A会增加子类D,E之类的。

 

使用它们的客户程序在获得一个实例之后,一般情况下只会调用它们的go方法,也就是说,不关心具体的实例是属于哪种类型的。当然,有很多方式都可以实现这个需求,而我关心的是,怎样才能让可能新增的B3、B4、D、E这些类实现起来最简单、可靠。所以,最终还是选择了逐步细分函数的方式。

 

代码如下:

abstract Class A
{
  public void go() //大方法
  {
     prepare()
     going();
     clearup()
  }

  abstract public void going(); //小方法

  protected void prepare()
  {
     System.out.println("prepare by A");
  }

  protected void clearup()
  {
     System.out.println("clearup by A");
  }
}

abstract Class B extends A
{
  @Override 
  public void going() //重写小方法
  {
  	 beforeRun();
  	 run(); //小小方法
  	 afterRun();
  }
  
  abstract public void run();
  
  protected void beforeRun()
  {
  	 System.out.println("beforeRun by B");
  }

  protected void afterRun()
  {
  	 System.out.println("beforeRun by B");
  }
}

Class B1 extends B
{
  @Override 
  public void run() //重写小小方法
  {
     System.out.println("go by B1");
  }
}

Class B2 extends B
{
  @Override 
  public void run() //重写小小方法
  {
     System.out.println("go by B2");
  }
}

Class C extends A
{
  @Override 
  public void going() //重写小方法
  {
     System.out.println("go by C");
  }
}

 

 

这样,B在自己的层次,“要求”本类别的Class在go的时候,必须按顺序调用beforeRun和afterRun,新增的B?子类只需实现自己的run方法即可被正确使用;万一真有很特殊的情况,新的子类希望在go的时候不要调用beforeRun或者afterRun,那么用一个空函数覆写它们即可。

 

反之,如果beforeRun/afterRun这样的函数需要B?子类来调用,那么绝大多数子类都要编写调用的代码,也包括将来可能扩充的,这显然造成了一定的代码重复。

分享到:
评论
26 楼 bleakoasis 2008-04-11  
这帖子气氛真不怎么好
楼主提到了好莱坞原则,猜测是想应用Template Method模式吧。不过好像没有体会好莱坞原则的重点,好莱坞原则的关键是好莱坞对项目的完全控制,演艺人员只能服从好莱坞领导的安排,在需要的的时候完成自己的表演。

在这个例子里Class A的go()方法定义好顶层的逻辑,换句话说go()是模板方法,最好应该定义成final防止子类重写。Class A的prepare()和clearup() 方法属于顶层逻辑里的具体实现,最好也定义成final防止子类给他置换掉。Class A的going()抽象方法是要求子类实现的。设计好Class A后,生成A的子类时只需要考虑要去置换掉哪些操作,这样会使各个层次类的责任变的很清晰。Template Method模式在设计框架的时候会经常用到。

如果楼主事先说明Template Method模式也许效果会好些
懂点设计的人都知道继承不能乱用,但是并不代表不用

以上无论对错希望大家以一个有好的气氛来探讨
25 楼 srdrm 2008-04-10  
就模式说模式, 一点意义没有
24 楼 rappy 2008-04-07  
这是非常经典的工厂方法模式.
为何会被这么多人唾弃,
很明显是LZ应用环境描述不清.

工厂方法模式也是利用了继承,并且还实现了灵活性.
可见,继承并不是不好,关键是看你如何用.

任何东西的存在都有其道理.
问题是:你懂得利用吗?

支持楼主,你的原意是无可厚非的,很好。

只是要描述清楚了.:)
23 楼 rtdb 2008-04-07  
设计模式是很容易滥用的。继承也是。

楼主的问题的由来,就是滥用了继承。

22 楼 rappy 2008-04-07  
第一遍被标题误导,愣是没看明白讲得啥.

看了回复,再回头去认真看了LZ的帖,

才发现LZ讲半天原来讲的是工厂方法模式.

真汗,还什么好莱钨原则,算我孤陋寡闻,没听过.
学习...
21 楼 lyxh_2003 2008-04-05  
我觉得还是尽量少用继承,多用组合或者聚合,毕竟继承的耦合度太高了。。
20 楼 darkjune 2008-04-05  
如果lz的意思就是要在子类增强一下父类方法,可以用楼上的AOP方法. 如果是想每个子类都用单独的逻辑,那还是不要用继承算了
19 楼 lllyq 2008-04-04  
就事论事,这个需求可以这样做,直接在文本编辑器写的,简单意思到就行
首先有接口
IGoAroundInterceptor
before();
after();

AGoAroundInterceptor implements IGoAroundInterceptor

A类

go() {
	IGoAroundInterceptor interceptorget = getGoAroundInterceptor()
	interceptorget.before()
	run()
	interceptorget.after()
}
abstract run();
IGoAroundInterceptor getGoAroundInterceptor() {
	return AGoAroundInterceptor.INSTANCE;//简单起见
}

要增加A的子类B,可先增加
BGoAroundInterceptor extends AGoAroundInterceptor

before() {
	super.before();
	.....//special before
}
after() {
	.....//special after
	super.after();
}


B类
run() {

}
IGoAroundInterceptor getGoAroundInterceptor() {
	return BGoAroundInterceptor.INSTANCE;
}

对B的子类C类似,而且CGoAroundInterceptor可以决定before/after的策略,可以继承AGoAroundInterceptor也可以继承BGoAroundInterceptor,非常自由

实际应用中可能还要考虑IGoAroundInterceptor的初始化等问题,这里只是简单起见介绍一个思路
18 楼 slaser 2008-04-03  
LZ的设计企图在每个子类中对父类方法调用进行拦截处理,AOP是最合适的,其次就是运用decorator模式来实现。现在的设计不具备可扩展性,每加一个子类,都会造成不小修改,系统变大后,可读性也急剧变差。
17 楼 oldrat 2008-04-03  
为什么有的人 要把 楼主 要表达的东西和 这个那个的设计模式 来对比,要说谁优谁劣。
这样没有必要。 设计模式无非是代码的优美有效的组织形式。

楼主 提出了一个 模式(或者说一个模式片断),并对这个模式片段进行了阐述。这对以后真实的设计有很好的参考意义。

一定要和那个设计模式模式比的话,我觉得范了个错误,因为模式是设计有效参考,但不是来套设计的(当然有些模式的确有很好的可套用性)。

可能大家的疑虑是 这样的一个设计模式片段,在实际设计中有什么应用,应用的方式是什么?

那是另一个问题,不见得要楼主一起给出。
(模式的提出和模式的应用,就好像 电脑 和 电脑的应用 一样,造电脑的人 不见得要知道 电脑应用在什么方面效益最好)

支持楼主的独立见解!支持楼主!!!
16 楼 nmvr2600 2008-04-02  
撇开模式什么不要谈,我觉得lz那个代码里面子类B根本没法避免调用父类A的方法。按照lz的意图,执行B.go,是不是在call父类A的方法呢? 另外关于子类调用父类方法会照成循环依赖的说法如何而来? 依赖是对的,不知道怎么是循环了依赖了。

就算lz这样的做法是避免所谓的循环依赖,但是lz真的是觉得这种继承真的“清晰和简洁”吗? A的go前后是prepare()和cleanup(),B中又增减了run,假如你又需要新的动作,那么子类里最后到底有多少这种差异性的方法? lz也说假如有D,E。So,清晰性和简洁性都是无从谈起。不仅仅是类爆炸了,方法也在爆炸。

当然在没有知道lz所面对的到底是一种什么样的应用场景下,说decorator可以搞定也有些不太合适。所以建议lz重新考虑下自己的应用需求和设计。你的这些BCDE到底和A应不应是种继承关系?lz到底需要的一种树形结构还是别的什么? 因为我看lz的继承只是为了从父类那里继承个方法。

如果真的是继承关系,那么到底是不是该有对象自己决定该怎么做,还是把那些go啊run转交给别的对象。

其实lz拒绝decorator的理由并不充分,使用decorator绝不会比你现在这种方式更“重”。另外所谓的好莱坞原则是该用到父子类上的吗?



15 楼 kenees 2008-04-01  
先不说技术,我感觉讨论的氛围太差了吧....
14 楼 小蚯蚓 2008-04-01  
还是少用继承为妙,如果是大系统的话
建议多用用组合的方式把
13 楼 bocar 2008-04-01  
LZ代码的耦合性太高了。借用C3PO的说法
引用
1. 父类的"go"方法粒度太大。可以把“go”分解成一系列互相独立的protected小方法,让子类调用,“组装”各自所需要的逻辑。

这种方法类似于Spring调用Hibernate的HibernateDaoSupport
引用
2. 如果第一步做不了,说明你的类太monolith,表达的逻辑太多,建议拆分父类并重新考虑继承结构。

LZ你看设计模式的时候一定看过一个叫“单一职能原则”的吧。一个类只做一件事情。

不能为了设计模式而设计模式,所谓的模式是在有大量编码经验基础上总结出来的经验。属于经验之谈,每个都有自己特定的使用环境。
其实我觉得让你的代码保存简约精致就是最好的模式。就是说明明一个System.out.println("Hello, world.");就能解决的问题,不用非要去用什么工厂模式、单例模式、……模式什么的吧。
12 楼 linhong_1001 2008-04-01  
总觉得此帖的作者是没有理解什么设计模式、继承,既然是继承了,又怎么不依赖父类,那你继承干什么,不知所谓。好莱坞原则不是你这个意思吧
11 楼 galaxystar 2008-04-01  
聚合优于继承,对复杂的结构来说
10 楼 dennis_zane 2008-04-01  
如果以后还会新增B3、B4、D、E,那么重构为Decorator是最好的选择,如果以后不会新增这么多类,那么现在的你在担心什么?
9 楼 williamou 2008-04-01  
我觉得楼主有点走火入魔了。。

父类的一个方法在子类进行重写,本身就是一个封装性很强的一个东西,里面的实现是很独立的。如果不断的考虑将其共有的东西抽象,分拆,就会走进一个死胡同。。

我做的话,要对这些方法内的共有代码冗余置之不理(毕竟都是实现细节的问题),要是真的与心不忍,就将它们抽到一个Utility类去。。

这样还解决不了,就真的是本身方法的划分有问题了,粒度过大?重构吧
8 楼 songofhawk 2008-04-01  
原来对于使用情景交代得不太清楚,现在补充说明了一下,还请大家指正
7 楼 Eastsun 2008-03-29  
关于继承,我还是很推崇<Effictive JAVA>里面的说法:
引用

除非專為繼承而設計並提供文件,否持不要使用繼承


所以,如果设计一个类时,需要考虑到继承,就应该考虑到各种可能,并把父类中各个方法的依赖性用文档详细说明.就如Collection Framework里的那些Abstract XXX一样.
否则,要么把该类设计为final的,要么在说明文档中注明:该类不是为了继承而设计的

相关推荐

    UML课件——第二章

    - **例子**:如果一个类`Customer`使用了一个类`Address`的实例来存储客户地址信息,则`Customer`对`Address`有一个依赖关系。 2. **类属(Generalization)关系** - **定义**:类属关系也称为泛化或继承关系,...

    12.java学习第十二章——继承extends.pdf

    - **术语定义**:当一个类(B类)继承另一个类(A类)时,A类被称为超类、父类或基类;而B类则被称为子类、派生类或扩展类。通常情况下,人们更倾向于使用“父类”和“子类”这样的术语。 - **示例代码**:`class A ...

    深入浅出设计模式第一章中文

    - “SimUDuck”游戏采用面向对象(OO)设计,其中`Duck`作为一个超类,各种鸭子作为其子类继承自`Duck`。 - `Duck`类包含通用行为,如`quack()`(呱呱叫)和`swim()`(游泳),而`display()`(显示外观)则在每个子类中具体...

    java初涉设计模式

    最初的设计采用了面向对象的技术,通过定义一个`Duck`超类以及一系列继承自该超类的具体鸭子子类来实现这一目标。 - **Duck** 超类定义了所有鸭子共有的行为,如`quack()`和`swim()`。 - 每种鸭子(如`MallardDuck`...

    继承与多态PPT学习教案.pptx

    1. **继承的定义**:继承机制允许一个类(子类)基于另一个已存在的类(父类)进行定义,这样子类就可以直接继承父类的属性和方法,减少了代码重复,增强了代码的复用性。在某些文献中,父类也被称为超类或基类。 2...

    Enterprise Architect学习笔记

    它表示了一般与特殊之间的关系,即一个类(子类)可以从另一个类(超类)继承属性和行为。在 UML 图中,泛化关系用实线加上三角箭头表示,箭头指向超类。这种关系允许子类扩展或重写超类的行为,同时保留其所有属性...

    继承与多态性 实验五实验报告(面向对象程序设计).docx

    在面向对象编程中,继承是一种重要的机制,它允许我们定义一个类(称为子类或派生类)来继承另一个类(称为父类或基类)的属性和方法。这种机制可以有效地复用代码,并支持类之间的共享行为。继承的基本概念包括: ...

    Java语言程序设计(一)04747汇总(2016——2017全)推荐文档.doc

    继承:面向对象编程的一个核心概念,允许创建新类来继承现有类的特性。 **4. 数组创建** - **选项分析**: - A. `int myA[5]={1,2,3,4,5);`:语法错误,数组声明和初始化的方式不正确。 - B. `int myA[]=new...

    UML 经典教程 统一建模语言

    - **依赖**(Dependency):依赖关系表示一个模型元素(依赖方)对另一个模型元素(提供方)的依赖性。例如,一个`CourseSchedule`类可能依赖于`Course`类来进行课程的添加和删除操作。 - **泛化**(Generalization):...

    UML类关系之JAVA代码实现

    2. **泛化(Generalization)**:泛化是类的继承关系,一个类(子类)继承另一个类(超类)。在Java中,这通过`extends`关键字实现。例如: ```java public class A extends B {} ``` 泛化关系在UML中用一个带空心箭头...

    04735数据库系统原理200710.doc

    5. 如果一个实体X的存在依赖于另一个实体Y的存在,且X的主键部分或全部来自Y,则X被称为弱实体(C.弱实体)。 6. 插入异常(A.不该插入的数据被插入)是关系规范化中的一个问题,指的是在数据库中不应该出现的插入...

    31天重构速成

    箭头抗模式(Arrowhead)是指在一个类中过度依赖另一个类的情况。重构时,应该尽可能减少这种依赖,提高代码的解耦性。 #### 23. Introduce Design By Contract checks 通过在代码中引入契约式设计(Design By ...

    JAVA程序设计实用教程课后习题简答(第3版).doc )

    封装是一种信息隐藏技术,它将数据和对数据的操作捆绑在一起,形成一个不可分割的独立单位——类。封装的好处在于它允许我们隐藏类的实现细节,只暴露必要的接口,从而增强了代码的安全性和稳定性。封装还简化了类的...

    写给大家看的面向对象编程书(第3版).[美]Matt Weisfeld(带详细书签).pdf

    1.7.1 超类和子类 16 1.7.2 抽象 16 1.7.3 is-a关系 17 1.8 多态 18 1.9 组合 20 1.9.1 抽象 21 1.9.2 has-a关系 21 1.10 小结 21 1.11 本章使用的示例代码 21 1.11.1 TestPerson示例:C#.NET 21 1.11.2 ...

    java基础反射IO流线程模式

    - **抽象工厂模式**:提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。 #### 单例模式 - **饿汉式单例模式**:在类加载时就完成了初始化,所以类加载速度较慢。 - **懒汉式单例模式**:第一...

Global site tag (gtag.js) - Google Analytics