Sealyu

--- 博客已迁移至: http://www.sealyu.com/blog

  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  618 随笔 :: 87 文章 :: 225 评论 :: 0 Trackbacks

长方形和正方形

正方形是否是长方形的子类的问题,西方一个很著名的思辨题。

正确的写法是:

长方形类:两个属性,宽度和高度;正方形类:一个属性,边。

(LY注:这是至少流行了十年的思辨题目,最早来自于C++和Smalltalk领域。类似的这种思辨问题还有哪些呢?让我不禁对哲学又感冒起来了。查阅资料时意外找到了一个讨论区,里面有读者和作者关于此处的拓展讨论,真让人高兴。)

(LY注:书中没有提契约即Design by Contract的概念。子类应当完全继承父类的contract。《敏 捷软件开发:原则、模式与实践》一书中这样写,"基于契约设计(Design By Constract),简称DBC"这项技术对LISKOV代换原则提供了支持.该项技术Bertrand Meyer伯特兰做过详细的介绍:使用DBC,类的编写者显式地规定针对该类的契约.客户代码的编写者可以通过该契约获悉可以依赖的行为方式.契约是通过 每个方法声明的前置条件(preconditions)和后置条件(postconditions)来指定的.要使一个方法得以执行,前置条件必须为真. 执行完毕后,该方法要保证后置条件为真.就是说,在重新声明派生类中的例程(routine)时,只能使用相等或者更弱的前置条件来替换原始的前置条件, 只能使用相等或者更强的后置条件来替换原始的后置条件。  本书中长方形的Contract是width和height可以独立变化,这个contract在正方形中被破坏了。)

(LY注:注意,我们所有讨论的基础都应由类的行为决定。这使得长方形等类是动态的,而不是象现实生活中一样是静态的概念。)

 

正方形不可以作为长方形的子类

如果设定一个resize方法,一直增加长方形的宽度,直到增加到宽度超过高度才可以。

那么如果针对子类正方形的对象调用resize方法,这个方法会导致正方形的边不断地增加下去,直到溢出为止。换言之,里氏法则被破坏掉了。

这个例子很重要,它意味着里氏代换与通常的数学法则和生活常识有不可混淆的区别。

(LY 注:常识认为,正方形is a 长方形,而且是一类特殊的长方形。但是在这里出了问题,如果我们系统中不会有这样的resize操作,是否正方形就可以作为长方形的子类了呢?看后文是可以的)

代码的重构

长方形和正方形到底应该是什么关系呢?

它们应该都是四边形类的子类。四边形类中没有赋值方法,因类似上文的resize()方法不可能适用于四边形类,而只能只用于不同的具体子类长方形和正方形。因此里氏代换原则不会被破坏。(LY注:针对需要赋值操作的情况)

从抽象类继承

应尽量从抽象类继承,而不是从具体类继承。

上文对长方形和正方形的重构使用了重构的第一种方法。增加一个抽象类,让两个具体类都成为抽象类的子类。

记住一条指导性的原则,如果有一个由继承关系形成的等级结构的话,在等级结构树图上的所有树叶节点都应当是具体类;而所有的树枝节点都应当是抽象类或者Java接口。

问答题

1、  一个有名的思辨题,filename能不能作为string类的子类?

答:不能。Filename对象不能实现string对象的所有行为。比如两个string对象相加可以给出一个新的有效的string对象。而两个filename对象相加未必会得到一个新的有效的Filename对象。

       另外,Java中的String类型是final类型,因此不可以继承。

2、  如果正方形的边长不会发生改变,是否可以成为长方形的子类呢?(LY注:不变正方形,就是边长不会发生变化的正方形,也就是遵守不变模式的正方形。不变(Immutable)模式,一个对象在对象在创建之后就不再变化。)

答:可以。实现时,父类有两个属性宽度和高度。子类有三个属性宽度、高度和边。针对每一个属性,包含一个内部变量,一个Set值方法,一个Get值方法。子类正方形只需要将Set值方法不写任何语句即可。

3、  从里式代换角度看JavaPropertiesHashtable的关系是否合适?

答:不合适。在Java中,PropertiesHashtable的子类。显然Properties是一种特殊的Hashtable,它只接受string类型的键(Key)和值(Value)。但是,父类Hashtable可以接受任何类型的键和值。这意味着,在一些需要非String类型的键和值的地方,Properties不能取代Hashtable

(LY注:合成/聚合复用原则中有更详细的讨论,应使用合成/组合而不是继承。它们是has a的关系而不是is a的关系。)




另外,有一篇有意思的文章不赞同这个观点:

本文假定读者已经了解有关正方形不是长方形的相关内容。
        
        之前人们讨论的正方形长方形的问题的关键 在哪里?我觉得就在于改动长方形的边的长度。我们可以这么考虑一下,一个长方形的instance的边长应该是可变的吗?我觉得一旦一个长方形的边长改变 之后它就成了另一个长方形了(一个新的instance)。所以长方形类里面不应该有改变其边长的方法,一个长方形实例各个的边长应当在new它的时候确 定下来,并且它们应当是immutable的。基于这种考虑,我设计的长方形和正方形的类如下所示:
//长方形
public class Rectangle {
  private final int width;
  private final int height;
 
  public Rectangle(int width, int height) {
    this.width = width;
    this.height = height;
  }
 
  public int getWidth() {
    return width;
  }

  public int getHeight() {
    return height;
  }
 
  public int getArea() {
    return width*height;
  }
}

//正方形
public class Square extends Rectangle{
  private final int side;
 
  public Square(int side) {
    super(side, side);
    this.side = side;
  }
 
  public int getSide() {
    return side;
  }
}

        这种继承关系就既符合现实中的父子关系也遵循LSP。之所以这么设计,我的想法是一个类所具有的方法不应当能够改变其本质。比如有一个Men类,它可以有 eat(),sleep(),work(),makeLovewith(Person p)方法,但是如果你在里面定义denatureToWomen(),denatureToEunuch()就很不恰当了,因为这改变了其本质,导致这个 Men的实例不再属于Men类(至少已经和现实不吻合)了。除非这两个方法不能改变该实例本质,否则在Men里面定义这两个方法本身就是有问题的。不过如 果用下面这种方式定义也许可行:
public Women denatureToWomen() {
  Women w = new Women();
  //set attributes here
  return w;
}

public Eunuch denatureToEunuch() {
  Eunuch e = new Eunuch();
  //set attributes here
  return e;
}

这样一来,调用denatureToWomen()会产生一个新的实例,原来的那个Men实例依然存在,这和现实生活依然不吻合,现实生活中一个实例不光可以上型(upcast),还可以平行型,寒。。。

总之一句话,一个类的方法不应该改变其实例的本质。


posted on 2009-12-23 15:39 seal 阅读(2451) 评论(0)  编辑  收藏 所属分类: 设计模式

只有注册用户登录后才能发表评论。


网站导航: