问题所在 设计目标
----------------------------------------------------------------------------
过于僵硬 可扩展性(新性能可以很容易加入系统)
过于脆弱 灵活性(修改不会波及其它)
复用率低
粘度过高 可插入性(新功能容易加入系统(气囊加入方向盘))
* 提高系统可复用性的几点原则:
传统复用:
1. 代码的粘帖复用
2. 算法的复用
3. 数据结构的复用
* 可维护性与可复用性并不完全一致
* 对可维护性的支持:
一、 "开放-封闭"原则(OCP)
Open-Closed Principle原则讲的是:一个软件实体应当对扩展开放,对修改关闭。
优点:
通过扩展已有软件系统,可以提供新的行为,以满足对软件的新的需求,使变化中的软件有一定的适应性和灵活性。
已有软件模块,特别是最重要的抽象层模块不能再修改,这使变化中的软件系统有一定的稳定性和延续性。
例子:玉帝招安美猴王
当年大闹天宫便是美猴王对玉帝的新挑战。美猴王说:"'皇帝轮流做,明年到我家。'只教他搬出去,将天宫让于我!"对于这项挑战,太白金星给玉皇大帝提出的建议是:"降一道招安圣旨,宣上界来…,一则不劳师动众,二则收仙有道也。"
换而言之,不劳师动众、不破坏天规便是"闭",收仙有道便是"开"。招安之道便是玉帝天庭的"开放-封闭"原则。
招安之法的关键便是不允许更改现有的天庭秩序,但允许将妖猴纳入现有秩序中,从而扩展了这一秩序。用面向对象的语言来讲,不允许更改的是系统的抽象层,而允许更改的是系统的实现层。
二、 里氏代换原则(LSP)
Liskov Substitution Principle(里氏代换原则):子类型(subtype)必须能够替换它们的基类型。
反过来的代换不成立
《墨子·小取》说:"娣,美人也,爱娣,非爱美人也……"娣便是妹妹,哥哥喜爱妹妹,是因为两人是兄妹关系,而不是因为妹妹是个美人。因此,喜爱妹妹不等同于喜爱美人。用面向对象语言描述,美人是基类,妹妹是美人的子类。哥哥作为一个有"喜爱()"方法,接受妹妹作为参数。那么,这个"喜爱()"方法一般不能接受美人的实例。
一个违反LSP的简单例子(长方形和正方形)
{
private long width;
private long height;
public void setWidth(long width)
{
this.width = width;
}
public long getWidth()
{
return this.width;
}
public void setHeight(long height)
{
this.height = height;
}
public long getHeight()
{
return this.height;
}
}
public class Square
{
private long side;
public void setSide(long side)
{
this.side = side;
}
public long getSide()
{
return side;
}
}
正方形不可以做长方形的子类
public class Rectangle
{
private long width;
private long height;
public void setWidth(long width)
{
this.width = width;
}
public long getWidth()
{
return this.width;
}
public void setHeight(long height)
{
this.height = height;
}
public long getHeight()
{
return this.height;
}
}
public class Square : Rectangle
{
private long side;
public void setWidth(long width)
{
setSide(width);
}
public long getWidth()
{
return getSide();
}
public void setHeight(long height)
{
setSide(height);
}
public long getHeight()
{
return getSide();
}
public long getSide()
{
return side;
}
public void setSide(long side)
{
this.side = side;
}
}
public class SmartTest
{
public void resize(Rectangle r)
{
while (r.getHeight() >= r.getWidth() )
{
r.setWidth(r.getWidth() + 1);
}
}
}
在执行SmartTest的resize方法时,如果传入的是长方形对象,当高度大于宽度时,会自动增加宽度直到超出高度。但是如果传入的是正方形对象,则会陷入死循环。
代码重构
{
public long getWidth();
public long getHeight();
}
public class Rectangle : Quadrangle
{
private long width;
private long height;
public void setWidth(long width)
{
this.width = width;
}
public long getWidth()
{
return this.width;
}
public void setHeight(long height)
{
this.height = height;
}
public long getHeight()
{
return this.height;
}
}
public class Square : Quadrangle
{
private long side;
public void setSide(long side)
{
this.side = side;
}
public long getSide()
{
return side;
}
public long getWidth()
{
return getSide();
}
public long getHeight()
{
return getSide();
}
}
参考文献:
阎宏,《Java与模式》,电子工业出版社
[美]James W. Cooper,《C#设计模式》,电子工业出版社
[美]Alan Shalloway James R. Trott,《Design Patterns Explained》,中国电力出版社
[美]Robert C. Martin,《敏捷软件开发-原则、模式与实践》,清华大学出版社
[美]Don Box, Chris Sells,《.NET本质论 第1卷:公共语言运行库》,中国电力出版社
http://www.dofactory.com/Patterns/Patterns.aspx
评论
您所提到的可维护与下面的可扩展性、可插入性、灵活性的关系是怎么样的?可维护性包含扩展、插入、灵活性吗?
----------------------------------------------------------------------------
过于僵硬 可扩展性(新性能可以很容易加入系统)
过于脆弱 灵活性(修改不会波及其它)
复用率低
粘度过高 可插入性(新功能容易加入系统(气囊加入方向盘))
有些人开发的系统高度灵活,所有数据库字段都可以动态添加、删除,表也可以动态构建。可以说灵活性高、可插入、可扩展性也很强。但导致代码超级复杂,读起来根本就找不到北,更不用说修改了。这时候,可维护性就显得很低。甚至在维护代码时,不小心触动一些关键部件造成系统出现问题。
有时候我提倡为开发人员写代码(注意:不是注释)。有些代码不是给客户用的,而是给开发人员用的。包括必要的错误定位工具,内部调试断点,性能计数器等等。这些代码可以帮助提高系统的可维护性。
总之,可维护性是一个综合的概念,任何人都无法预料客户需求会发生什么样的变化,“未雨绸缪”和“亡羊补牢”都是提高可维护性的一个很好切入点。测试驱动开发以及契约式开发对提高可维护性也有不少帮助。
没想到会有这么多的回答,非常感谢。
这方面我也有体会,有时候为了程序不那么死板,就写得灵活点,所以用了大量得字段变量,一层层封装,做完之后看上去确实很cool,但是过了一段时间要修改bug或需求时,真的很累...
从数学的角度来说正方形属于长方形的一个特例,与之对应的在面向对象中的说法叫子类,这一点应该是很明确的。
具体吕兄用“在执行SmartTest的resize方法时,如果传入的是长方形对象,当高度大于宽度时,会自动增加宽度直到超出高度。但是如果传入的是正方形对象,则会陷入死循环。”来说明“正方形不可以做长方形的子类”,我认为这是没有说服力的。因为我认为你的这个SmartTest的resize方法本身写的有问题,问题是:while的循环条件应该由r.getHeight() >= r.getWidth() 改为r.getHeight() > r.getWidth() 。这样就不会出现死循环了,而且我认为也应该是合理的,不知道是不是应该这样,还请吕兄赐教。
一般来说,满足“Is-A(是一个)”关系的,可以用继承关系来描述,比如:Cricle is a sharp,那么,Cricle 继承于 Sharp。
但是,这个关系是针对于对象的特性来说的,从这个角度上来说,正方形与长方形是不一样的,从上面的例子中取一个实例为证:
Rectangle r = new Rectangle();
r.setWidth(8);
r.setHeight(5);
长方形的面积是40。
如果正方形是长方形的子类,那么:
Square s = new Square();
s.setWidth(8);
s.setHeight(5);
那正方形的面积是25。
由此看来,他们的特性是不满足“Is-A”关系的。
我觉着单纯的从上面已有的代码来讨论问题是没太大意义的。
首先,
凭什么说你上面的代码就能正确而全面的反映“长方形”这个概念?
如果说从数学上很明显正确的东西(比如正方形属于长方形或正方形是长方形的子类)而从你的代码上解释不通,
这时候我们首先想到的应该是你的代码有问题,
而不是从很可能有问题代码来验证结论很明确的数学概念,
难道大家不这么认为吗?
再次,
你的论据:
“
如果正方形是长方形的子类,那么:
Square s = new Square();
s.setWidth(8);
s.setHeight(5);
那正方形的面积是25。
”
是有问题的。
你的前提既然是说正方形(引用你的话“如果正方形是长方形的子类”),
那为什么又出现“s.setWidth(8); s.setHeight(5); ”这样的情况呢?
既然是正方形为何两个边的长度要设为不同?
不解!
那么,我们的测试用例应该如此:
Rectangle r = new Rectangle();
r.setWidth(8);
r.setHeight(5);
Assert(r.Area()==40);
不是这样的吗?
另外,对于Liskov替换原则来说:
Rectangle.setWidth(double value)违反了它的不变性。
它的后置条件是否应该为:
Rectangle.Width==value && Rectangle.Height==oldHeight
(oldHeight是没有改变前的Height)
对于正方型这个特殊情况,它的后置条件已经发生变化。
to gloria:虽然我也是倾向你的想法,但事物往往是不断发展的。我们说“正方形是长方形的一个特例”,是因为我们在接受教育的时候书本上是这么说的。但随着时间的推移,人类的认识会不断发展。或许当我们认为“正方形是长方形的一个特例”这句话不恰当的时候,会对课本进行修改。许多时候,我们说是,其实我们并没有证明过,只不过是我们所认为的权威说是。我想我们应该尽量抛开我们已有的认识,尽量从“设计模式”出发去考量正方形与长方形之间更为合理的关系。
如:
class human
{
void eat(){}
}
class Chinese:human
{
void speakingChinese(){}
}
class client
{
static void Main()
{
human man=new Chinese();
man.eat() ; //可以
Chinese chinses=new human();//不可以,human中没有speakingChinese()方法
}
}
所以是否是继承关系关键是约束,不同的约束范围,会产生不同结果。在BOB的<敏捷软件>中有详细说明,大家可以去看看。
再举一个鸭嘴兽的例子,假如浦乳动物的约束是:恒温,喂奶,那么鸭嘴兽可以是浦乳动物的子类,但要是再加一个约束:必须是胎生,那么鸭嘴兽就不可做浦乳动物的子类,目前动物学术界基本上认为鸭嘴兽属于浦乳动物。
我用的是java,c#不是很清楚,但设计思路应该是差不多的。
public class FourSide {
public FourSide(int side1, int side2, int side3, int side4) {
this.side1 = side1;
this.side2 = side2;
this.side3 = side3;
this.side4 = side4;
}
private int side1 = 0;
private int side2 = 0;
private int side3 = 0;
private int side4 = 0;
public int getSide1() {
return side1;
}
public void setSide1(int side1) {
this.side1 = side1;
}
public int getSide2() {
return side2;
}
public void setSide2(int side2) {
this.side2 = side2;
}
public int getSide3() {
return side3;
}
public void setSide3(int side3) {
this.side3 = side3;
}
public int getSide4() {
return side4;
}
public void setSide4(int side4) {
this.side4 = side4;
}
protected int getRoundLength() {
return side1 + side2 + side3 + side4;
}
}
public class Rectangle extends FourSide {
public Rectangle(int width, int height) {
super(width, height, width, height);
}
public int getArea() {
int width = super.getSide1();
int height = super.getSide2();
return width * height;
}
}
public class Square extends Rectangle {
public Square(int sideLenght) {
super(sideLenght, sideLenght);
}
}
public class TestMain {
/**
* @param args
*/
public static void main(String[] args) {
Rectangle rectangle = new Square(4);
while (rectangle.getSide1() >= rectangle.getSide2() )
{
rectangle.setSide2(rectangle.getSide2() + 1);
}
System.out.println("rectangle:");
System.out.println(rectangle.getSide1());
System.out.println(rectangle.getSide2());
System.out.println();
}
}
我传入的矩形是一个正方形,结果也没什么死循环啊。
首先,最初的基类是4边行,具有4条边(没考虑角的情形),按相邻的顺序依次是边1,边2,边3,边4,同时建立构造函数,是需要知道4条边的。然后矩形继承4边行,可以只有长宽就构造一个矩形,当然,原4边形的4条边的属性应该分别对应这里的,宽,长,宽,长(你要说长,宽,长,宽也一样)。其周长的得到式都是4边之和,因此就写在基类就可以了。而面积,对于矩形来说,边1和边2的乘积就可以。
下面是正方形,正方形,是继承矩形的,但其构造函数应该只要一个边就可以了,因此,这里的构造函数,对应矩形构造函数里的两个参数,宽和高都用正方形的边就可以了。这时候,就可以通过getRoundLength()和getArea()很轻松的得到他们的周长和面积。
注:普通的四边形要知道面积,关靠边是不行的,还要引入角的概念,这里就没有复杂化了。
说的对,关键是限制条件的原因:
//四边形
public class FourSide {
public FourSide(int side1, int side2, int side3, int side4) {
this.side1 = side1;
this.side2 = side2;
this.side3 = side3;
this.side4 = side4;
}
public FourSide(Rectangle rectangle){
this.side1 = rectangle.getSide1();
this.side2 = rectangle.getSide2();
this.side3 = rectangle.getSide3();
this.side4 = rectangle.getSide4();
}
public FourSide(Square square){
this.side1 = square.getSide1();
this.side2 = square.getSide2();
this.side3 = square.getSide3();
this.side4 = square.getSide4();
}
private int side1 = 0;
private int side2 = 0;
private int side3 = 0;
private int side4 = 0;
public int getSide1() {
return side1;
}
public void setSide1(int side1) {
this.side1 = side1;
}
public int getSide2() {
return side2;
}
public void setSide2(int side2) {
this.side2 = side2;
}
public int getSide3() {
return side3;
}
public void setSide3(int side3) {
this.side3 = side3;
}
public int getSide4() {
return side4;
}
public void setSide4(int side4) {
this.side4 = side4;
}
protected int getRoundLength() {
return side1 + side2 + side3 + side4;
}
}
//矩形
public class Rectangle extends FourSide {
public Rectangle(int width, int height) {
super(width, height, width, height);
this.width = width;
this.height = height;
}
public Rectangle(Square square) {
super(square);
}
private int width = 0;
private int height = 0;
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = width;
super.setSide1(width);
super.setSide3(width);
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = height;
super.setSide2(height);
super.setSide4(height);
}
public void setSide1(int side1) {
setWidth(side1);
}
public void setSide2(int side2) {
setHeight(side2);
}
public void setSide3(int side3) {
setWidth(side3);
}
public void setSide4(int side4) {
setHeight(side4);
}
public int getArea() {
return this.width * this.height;
}
}
//正方形
public class Square extends Rectangle {
public Square(int sideLenght) {
super(sideLenght, sideLenght);
this.sideLenght = sideLenght;
}
private int sideLenght = 0;
public int getSideLenght() {
return sideLenght;
}
public void setSideLenght(int sideLenght) {
this.sideLenght = sideLenght;
super.setWidth(sideLenght);
super.setHeight(sideLenght);
}
public void setWidth(int width) {
setSideLenght(width);
}
public void setHeight(int height) {
setSideLenght(height);
}
public void setSide1(int side1) {
setSideLenght(side1);
}
public void setSide2(int side2) {
setSideLenght(side2);
}
public void setSide3(int side3) {
setSideLenght(side3);
}
public void setSide4(int side4) {
setSideLenght(side4);
}
}
Rectangle r = new Rectangle();
r.setWidth(8);
r.setHeight(5);
长方形的面积是40。
正方形是长方形的子类:
Square s = new Square();
s.setWidth(8);
s.setHeight(5);
正方形的面积是25,这是很正常的,因为在setHeight和setWidth中,子类重载了方法,会按正方形的约束把相邻的两边都置为相等!如果你认为一定是非得8*5=40就错了,面积是子类的内部逻辑算出来的,并不是输入的人认为是多少就多少。
数学上是可以说正方形是长方形的特例,但也存在,圆是正多边形的特例,也就是说一个正无穷变形就是圆,如果按此设计,圆继承自多边形,那就没什么意义了。
所以类设计,并不是想当然的,要面向应用,提取共同点。
------------------------------
Square s = new Square();
s.setWidth(8);
s.setHeight(5);
正方形的面积是25
----------------------------
这个是从一开始就假设为矩形了,正方形明显的不可能邻边不等~
{
private long width;
private long height;
public virtual void setWidth(long width)
{
this.width = width;
}
public long getWidth()
{
return this.width;
}
public virtual void setHeight(long height)
{
this.height = height;
}
public long getHeight()
{
return this.height;
}
}
public class Square: Rectangle
{
public override void setWidth(long width)
{
base.setWidth(width);
base.setHeight(width);
}
public override void setHeight(long height)
{
base.setHeight(height);
base.setWidth(height);
}
}
就正方形和长方形来说,正方形就是一个长宽相等的长方形,为什么不能继承?