接口,抽象类,具体类设计选择问题

使用接口,抽象类,具体类编程到底有什么好处和害处(就是客户怎么使用我设计的类,到底是持有接口、抽象类引用还是具体类引用),请banq大哥解释一下。
我认为接口和抽象类的提供的特性(继承,多态等等)具体类都有,不能说接口和抽象类有哪些优点,只能说具体类编程会造成画蛇添足的效果,接口和抽象类只是用来约束客户对自己设计的类的滥用。
在Java思想4版P174,说接口提供完全解耦的功能。实际上具体类也能够提供这样的功能。
不过我觉得用具体类编程最大的问题就在于对子类的耦合,具体类的更改会影响到子类。还有具体类的行为和子类行为也可能不同,也会造成一些问题。
使用接口,抽象类,具体类编程到底有什么好处和害处,在设计的时候怎么进行选择?这些东西模模糊糊的,搞不清,希望得到banq大哥和大家的指点。
[该贴被smallmonkey于2008-12-22 20:23修改过]

谢谢啦!
>具体实施,我觉得应该先定义接口,然后用抽象类实现接口,然后再用具体类继承抽象类或实现接口

如果所有类都这样设计,这不会造成设计过于复杂问题么?我觉得这三者各有优缺点,存在即合理,Java语言既然提供了相关的语言特性,应该还是有它用得着的地方。具体情况要具体分析吧。我就是不能总结归纳这些优缺点,实践经验太少。

对不起啊,刚才弄错了,刚才我说的不对。

之前为了练习,做了个坦克大战游戏,我在里边是这么定义的:

interface Moveable{
public void move();
}

interface Drawable{
public void draw(Graphics g);
}

class Tank implements Moveable,Drawable{
//...
}

class Wall implements Drawable{
//...
}

class GameContext{
List<Tank> tanks;
List<Wall> walls;
List<Moveable> moveables;
List<Drawable> Drawable;

public synchronized void createTank(Tank tank){
tanks.add(tank);
moveables.add(tank);
drawables.add(tank);
}

public synchronized void createWall(Wall wall){
walls.add(wall);
drawables.add(wall);
}

public synchronized List<Moveable> getMoveables(){
return new ArrayList<Moveable>(moveables);
}

public synchronized List<Drawable> getDrawables(){
return new ArrayList<Drawable>(drawables);
}

}

--------
具体就是说,Moveable就是能动的东西,不管是坦克还是炮弹;Drawable就是可以画出来的东西,不管是墙壁还是坦克。

要得到所有能动的,让他们动起来,就直接返回Moveable的List;
要得到所有能画的,把他们画出来,就返回Drawable的List。

我觉得接口就是干这个使的。


[该贴被darasion于2008-12-22 21:54修改过]
[该贴被darasion于2008-12-22 21:58修改过]

哇靠都改了2次了,不管怎么样真是太谢谢你了!!!

接口就是相关动作的集合比如飞这个接口,它有向右飞,左飞等等一些动作。
类就是实体概念的抽象。抽象类和具体类在概念上进行的区分,比如鸟和喜鹊,鸟在现实中没有对应实体,而喜鹊却在现实中有对应的实体。在设计中要考虑这样的问题。不知道是不是这个意思。
但是又考虑正方形和长方体之间的联系,正方形是长方体的一个子集。那是不是把长方形设计成具体类,而正方形是它的子类呢?如果按照上面我理解的(不知道我理解有没有错,请指教),会选择是,就会产生以下问题:
长方形类:
  public class Rectangle{
  ...
  setWidth(int width){
   this.width=width;
  }
  setHeight(int height){
   this.height=height
  }

  }
  正方形类:
  public class Square extends Rectangle{
  ...
  setWidth(int width){
  this.width=width;
  this. height=width;
  }
  setHeight(int height){
  this.setWidth(height);
  }
  }
外部客户调用函数:
public void resize(Rectangle r){
   while(r.getHeight()<r.getWidth){
   r.setHeight(r.getWidth+1);
  }
  }
如果传入的是正方形,那么这个函数就永远得不到结束。

看样子这里按照现实中具体的概念认识是不能够用来指导接口,抽象类和具体类的设计的,我们要重新认识现实中的具体概念,比如正方形和长方形的关系。到底在选择这三者的时候去考虑什么问题。哎...反正我搞不懂,实际经验太少了。希望我的话能起到抛砖引玉的作用。
  
  

疯了!贴代码怎么都没有缩进啊!!

你这么一说。我倒是有点明白了。当然我还是不知道说的对不对。高手指点啊。

应该说,接口就是定义了某些行为角色的实现标准,就是让实现了接口的类必须可以做出某个行为或扮演某种角色,反过来满足了接口所定义的标准的类,都可以做为这个接口的实现。
接口所定义的标准不仅可以约束实现类,举例:我们用绿色食品的标准生产蔬菜;
而且满足接口定义的标准的类都可以声明自己实现了接口所定义的标准,举例:我们生产的蔬菜符合绿色食品标准。

更具体的说,就是在设计的时候并不一定要先考虑接口怎么设计,我们同样也可以先考虑具体类是什么样子的,然后看看是否能从这些具体类中提取出统一的标准做为接口,方便调用。

举例子:
1、定义一个[可走路的]接口,人可以走路,小狗可以走路,机器人也可以走路。人和小狗可以泛化成动物,可是机器人不是动物,它仍然可以走路。这样就不能用类的继承概念来解释了。但我们可以说,人、小狗和机器人都满足了走路的标准。

2、可以考虑一下我们刚刚学习awt时候的情景,为什么有人会这样写:
public class MyFrame extends Frame implements WindowListener,ActionListener{
//...
}
我们可以说:MyFrame是一个Frame,同时MyFrame扮演了WindowListener和ActionListener的角色,MyFrame可以做WindowListener和ActionListener能做的事,MyFrame满足了做为WindowListener和ActionListener的标准(可以持证上岗了:))。

3、某局长(具体类)是一名公务员(泛化/抽象),同时局长还扮演了父亲,丈夫,党政领导人,高尔夫球运动爱好者,贪恋酒色之徒,财迷,...(各种各样的接口)等角色。

------------------
而抽象类只当作具体类的泛化,确切地说就是定义一组具体类的分类,它可以什么都不实现,根据具体情况提供这些具体类共有的一些方法,而这些方法的选择主要考虑的是这个分类中的对象在这个分类中需要做什么。

还是拿人和小狗举例:我可以定义动物这个抽象类,在我的具体应用中,当我用到“动物”这个分类中的对象时,只需要用到“年龄”这个共有属性和“吃”这个共有动作。那我就可以定义一个抽象类“动物”,它只有“年龄”属性和“吃”这个动作即可。

当我需要使用“动物”这个分类的时候,持有的就是动物的引用。

-------------------
至于长方形和正方形,首先我觉得这个设计有点问题。
在你所设计的类中,正方形的setHeight()/setWidth()与长方型的setHeight()/setWidth()虽然名字相同,但它们实际上是两种不同的动作,它们并不是在设置同一属性。

换句话说,把本应该保持一般性的方法用特殊的方法给覆盖了,Square类中的setHeight/setWidth做了不该它做的事。

如果把名字再区分开,我觉得可以这样加以改进:


public class Rectangle{
setWidth(int width){
this.width = width;
}
setHeight(int height){
this.height = height;
}
}
public class Square extends Rectangle{
setSquareWidth(int width){
setWidth(width);
setHeight(width);
}
setSquareHeight(int height){
setWidth(height);
setHeight(height);
}
}

以上,这样一看是不是觉得用setSquareWidth和setSquareHeight两个方法来设置正方形的一个边长有点多此一举??


那么继续改进Square类:


public class Square extends Rectangle{
setEdgeSize(int size){
setWidth(size);
setHeight(size);
}
}

至此,是不是觉得思路有些清晰了呢?

然而,在定义了setEdgeSize这个方法之后,正方形的setHeight/setWidth方法又失去了实际意义,不小心调用后可能会出现宽高不一致的现象,更容易出错了。

考虑到正方形的“宽度高度相等”只不过是个特殊条件而已,并不是正方形比长方形更特殊的属性或行为。
也就是说矩形完全可以表示正方形。
这么一看,定义正方形就有点多此一举了。

最终我们只需要一个类就可以了,根本用不到继承:


public class Rectangle{
setWidth(int width){
this.width = width;
}
setHeight(int height){
this.height = height;
}
}

还有就是,使用长方型(矩形)的客户端,应该有责任去判断矩形是否构成一个正方形,这是client端份内的事。


再有,客户端调用函数时出现的问题似乎永远不会出现,如果传入的确实是正方形,那么while循环根本进不去:
public void resize(Rectangle r){
while(r.getHeight()<r.getWidth){
r.setHeight(r.getWidth+1);
}
}

[该贴被darasion于2008-12-23 14:08修改过]
[该贴被darasion于2008-12-23 16:29修改过]
[该贴被darasion于2008-12-23 17:58修改过]

非常感谢。你认真修改和热心帮助的态度让我不得不为你致敬。
至于在现实世界的一些概念是否可以完整地运用到面向对象编程的世界中去还是个有待考虑的问题。本身人就对现实世界有一定的曲解,不能说十分的准确。那个长方形和正方形也可以说明问题,正方形其实不是长方形的子类,其本身就是长方形,不过是长方形集合中的一个小的子集罢了。子集难道就是它的子类么?不是!这样就像你说的,一个类就行了。
而且在面向对象这个虚拟世界里也有其自身的特殊性,这三种选择方案归根到底还是要考虑到底怎么样组织程序,由编写软件的目的来决定的。比如低耦合,易于理解等等一类问题。
不过其中你对接口对应现实世界确实精辟,可以说恰到好处吧。至少我认为。接口本身就是提供功能的一个抽象,而现实世界中的角色就有这个意思,抽象类是具体类的一个抽象。而类又充当很多角色。继承接口就充当角色了。哇哈哈...爽!!!接口在现实生活中就是这么理解的。

不瞒你说,我也学到了不少东西。

我这还有关于正方形和长方型的补充,也是刚刚学到的:

就是在实际中有这样一条原则:尽量避免“属性的覆盖”。

一开始定义的正方形的setter等于覆盖了父类的setter,这样就造成了这样一个事实:正方形不再是一个长方型了。

因为一旦我们覆盖了属性,那么这个属性就不再是父类和子类所共有的特征了,这种继承关系也就没法再维持下去了。

方法的覆盖则不同,方法是定义做出了某行为,覆盖后行为本身没有变化,变化的是行为所产生的效果。

摘取要点,写入空间。至此,对接口,抽象类和具体类有了一个初步了解。你很厉害!

>>>>应该说,接口就是定义了某些行为或角色的实现标准,就是让实现了接口的类必须可以做出某个行为或扮演某种角色,反过来满足了接口所定义的标准的类,都可以做为这个接口的实现。
接口所定义的标准不仅可以约束实现类,举例:我们用绿色食品的标准生产蔬菜;而且满足接口定义的标准的类都可以声明自己实现了接口所定义的标准,举例:我们生产的蔬菜符合绿色食品标准。

darasion 对接口的解释很好啊,个人补充一点看法,我认为接口就是行为规范或标识规范,行为规范很好理解,就是darasion所说的行为标准,标识规范就是指为实现类帖上一个标签,标识某种权限,相当于一个令牌!如java.io.Serializable接口,就是这样的一种标识规范。 而抽象类是指对现实世界的实体进行提取的抽象性表达,具有排它性,是对事物本质的描述,标识此非彼!

每天关注一下,加深理解。

接口:不同事物间的共性
抽象类:同一事物间的个性