C#设计模式(2)

 软件腐化的原因:

问题所在   设计目标
----------------------------------------------------------------------------
过于僵硬   可扩展性(新性能可以很容易加入系统)
过于脆弱   灵活性(修改不会波及其它)
复用率低  
粘度过高   可插入性(新功能容易加入系统(气囊加入方向盘))

* 提高系统可复用性的几点原则:
传统复用:
1. 代码的粘帖复用
2. 算法的复用
3. 数据结构的复用

* 可维护性与可复用性并不完全一致

* 对可维护性的支持:


一、 "开放-封闭"原则(OCP)

Open-Closed Principle原则讲的是:一个软件实体应当对扩展开放,对修改关闭。

优点:
    通过扩展已有软件系统,可以提供新的行为,以满足对软件的新的需求,使变化中的软件有一定的适应性和灵活性。
    已有软件模块,特别是最重要的抽象层模块不能再修改,这使变化中的软件系统有一定的稳定性和延续性。

例子:玉帝招安美猴王
当年大闹天宫便是美猴王对玉帝的新挑战。美猴王说:"'皇帝轮流做,明年到我家。'只教他搬出去,将天宫让于我!"对于这项挑战,太白金星给玉皇大帝提出的建议是:"降一道招安圣旨,宣上界来…,一则不劳师动众,二则收仙有道也。"

换而言之,不劳师动众、不破坏天规便是"闭",收仙有道便是"开"。招安之道便是玉帝天庭的"开放-封闭"原则。

 

招安之法的关键便是不允许更改现有的天庭秩序,但允许将妖猴纳入现有秩序中,从而扩展了这一秩序。用面向对象的语言来讲,不允许更改的是系统的抽象层,而允许更改的是系统的实现层。


二、 里氏代换原则(LSP)

Liskov Substitution Principle(里氏代换原则):子类型(subtype)必须能够替换它们的基类型。

白马、黑马
 

反过来的代换不成立
《墨子·小取》说:"娣,美人也,爱娣,非爱美人也……"娣便是妹妹,哥哥喜爱妹妹,是因为两人是兄妹关系,而不是因为妹妹是个美人。因此,喜爱妹妹不等同于喜爱美人。用面向对象语言描述,美人是基类,妹妹是美人的子类。哥哥作为一个有"喜爱()"方法,接受妹妹作为参数。那么,这个"喜爱()"方法一般不能接受美人的实例。

 

一个违反LSP的简单例子(长方形和正方形)

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
{
   private long side;
    
   public void setSide(long side)
   {
      this.side = side;
   }

   public long getSide()
   {
      return side;
   }
}


正方形不可以做长方形的子类

using System;

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 interface Quadrangle
{
   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

 

posted on 2004-08-24 00:29 吕震宇 阅读(11264) 评论(33)  编辑 收藏 引用 网摘 所属分类: 设计模式

评论

# re: C#设计模式(2) 2004-09-03 17:07 runmin
我有个问题:


您所提到的可维护与下面的可扩展性、可插入性、灵活性的关系是怎么样的?可维护性包含扩展、插入、灵活性吗?

----------------------------------------------------------------------------
过于僵硬 可扩展性(新性能可以很容易加入系统)
过于脆弱 灵活性(修改不会波及其它)
复用率低
粘度过高 可插入性(新功能容易加入系统(气囊加入方向盘))  回复  更多评论
  

# re: C#设计模式(2) 2004-09-03 17:28 吕震宇
过于僵硬、过于脆弱、复用率低、粘度过高的系统都必然导致可维护性的减弱。反过来,可扩展、可插入、灵活的设计是提高可维护性的基础,但不一定能够保证可维护性。

有些人开发的系统高度灵活,所有数据库字段都可以动态添加、删除,表也可以动态构建。可以说灵活性高、可插入、可扩展性也很强。但导致代码超级复杂,读起来根本就找不到北,更不用说修改了。这时候,可维护性就显得很低。甚至在维护代码时,不小心触动一些关键部件造成系统出现问题。

有时候我提倡为开发人员写代码(注意:不是注释)。有些代码不是给客户用的,而是给开发人员用的。包括必要的错误定位工具,内部调试断点,性能计数器等等。这些代码可以帮助提高系统的可维护性。

总之,可维护性是一个综合的概念,任何人都无法预料客户需求会发生什么样的变化,“未雨绸缪”和“亡羊补牢”都是提高可维护性的一个很好切入点。测试驱动开发以及契约式开发对提高可维护性也有不少帮助。  回复  更多评论
  

# re: C#设计模式(2) 2004-09-06 09:49 runmin
......

没想到会有这么多的回答,非常感谢。  回复  更多评论
  

# re: C#设计模式(2) 2005-03-29 14:26 Harry
看来这是对矛盾:一方面为了不使程序的脆弱,复用率低而写的灵活,但是写得太灵活又会使程序的可维护性低.
这方面我也有体会,有时候为了程序不那么死板,就写得灵活点,所以用了大量得字段变量,一层层封装,做完之后看上去确实很cool,但是过了一段时间要修改bug或需求时,真的很累...
  回复  更多评论
  

# re: C#设计模式(2) 2005-06-16 09:10 xinbin1122
如果一定要有联系的话,长方形应该是正方形的子类。因为长方形比正方形多了属性。  回复  更多评论
  

# re: C#设计模式(2) 2005-06-16 12:20 o945
长方形能作为正方形的子类吗?不能!  回复  更多评论
  

# re: C#设计模式(2) 2005-06-19 10:27 往事如风
楼上的说得很对。长方形没有正方形的一些特性,正方形也没有长方形的一些特性,如果说要通过增加和删除基类的一些特性来创建子类的话,只能说明这2者是不适合有继承关系的,这正如猫之于狗。  回复  更多评论
  

# re: C#设计模式(2) 2005-09-05 16:30 刘余学
从数学的角度来说,正方形只是长方形的一个特例,所以应该是正方形是长放行的子类才对。有人可能说这样多了一条属性,有龙余,事实上这点龙余换来的可维护性是很强的。比如,将来,画图形的时候,你就不用单独对正方形做特殊处理了。
  回复  更多评论
  

# re: C#设计模式(2) 2005-09-26 22:01 netmini
正方形只是长方形有点像男人和女人的关系,大部分属性相同,就那么一点不同,但失之毫厘,差之千里,正放形和长方形在继承层次里面应该属于同一层次,可以把它们的公共属性和行为抽象出来做基类  回复  更多评论
  

# re: C#设计模式(2) 2005-09-26 22:11 netmini
public class Square : Rectangle,这个正方形类完全不符合面向对象的设计原则,setWidth,setHeight,setSide这三个接口功能完全重复,只保留setSide就行了  回复  更多评论
  

# re: C#设计模式(2) 2005-10-08 19:45 gloria
对于“正方形不可以做长方形的子类”我有点小小的看法:
从数学的角度来说正方形属于长方形的一个特例,与之对应的在面向对象中的说法叫子类,这一点应该是很明确的。
具体吕兄用“在执行SmartTest的resize方法时,如果传入的是长方形对象,当高度大于宽度时,会自动增加宽度直到超出高度。但是如果传入的是正方形对象,则会陷入死循环。”来说明“正方形不可以做长方形的子类”,我认为这是没有说服力的。因为我认为你的这个SmartTest的resize方法本身写的有问题,问题是:while的循环条件应该由r.getHeight() >= r.getWidth() 改为r.getHeight() > r.getWidth() 。这样就不会出现死循环了,而且我认为也应该是合理的,不知道是不是应该这样,还请吕兄赐教。  回复  更多评论
  

# re: C#设计模式(2) 2005-10-10 12:28 Porsche Lau
我也和gloria的看法一样,正方形是高度和宽度相等的长方形,等同于长方形是四个角为直角的矩形。吕Bloger的用这个例子不能很好的说明这个问题,不知道用图形类作为基类,长方形和等边形(5条边)作为例子怎样呢。  回复  更多评论
  

# re: C#设计模式(2) 2005-11-03 16:51 Linhen Zhang
假如“while的循环条件应该由r.getHeight() >= r.getWidth() 改为r.getHeight() > r.getWidth() ”,那么在系统中加入正方形类后代码中所有长方形相关的代码都会受影响,这不正是失去了抽象的意义了吗?这样正好违反了LSP原则 !  回复  更多评论
  

# re: C#设计模式(2) 2005-11-08 13:52 阿宇
我觉得正方形不可以做长方形的子类,原因如:
一般来说,满足“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”关系的。  回复  更多评论
  

# re: C#设计模式(2) 2005-11-08 20:33 gloria
呵呵!
我觉着单纯的从上面已有的代码来讨论问题是没太大意义的。

首先,
凭什么说你上面的代码就能正确而全面的反映“长方形”这个概念?
如果说从数学上很明显正确的东西(比如正方形属于长方形或正方形是长方形的子类)而从你的代码上解释不通,
这时候我们首先想到的应该是你的代码有问题,
而不是从很可能有问题代码来验证结论很明确的数学概念,
难道大家不这么认为吗?

再次,
你的论据:

如果正方形是长方形的子类,那么:
Square s = new Square();
s.setWidth(8);
s.setHeight(5);
那正方形的面积是25。

是有问题的。

你的前提既然是说正方形(引用你的话“如果正方形是长方形的子类”),
那为什么又出现“s.setWidth(8); s.setHeight(5); ”这样的情况呢?
既然是正方形为何两个边的长度要设为不同?
不解!  回复  更多评论
  

# re: C#设计模式(2) 2005-11-09 10:17 阿宇
在写class Rectangle 的时候或许我们并没有预见到会有Square 的出现。
那么,我们的测试用例应该如此:
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)
对于正方型这个特殊情况,它的后置条件已经发生变化。  回复  更多评论
  

# re: C#设计模式(2) 2005-12-21 17:36 Lantaio
  确实我看了之后也感觉有些别扭,好像“在数学上很明显的东西”怎么到了面向对象程序设计时却有这样那样的问题?虽然我在图形学方面一片空白,但觉得吕先生的例子还是有些勉强。我只找到 fx 的类库中有长方形的类,但找不到正方形的类,而且长方形类的定义也不是继承自平衡四边形类,不知道微软是怎么处理这样的问题的。希望有高手知道并赐教。
to gloria:虽然我也是倾向你的想法,但事物往往是不断发展的。我们说“正方形是长方形的一个特例”,是因为我们在接受教育的时候书本上是这么说的。但随着时间的推移,人类的认识会不断发展。或许当我们认为“正方形是长方形的一个特例”这句话不恰当的时候,会对课本进行修改。许多时候,我们说是,其实我们并没有证明过,只不过是我们所认为的权威说是。我想我们应该尽量抛开我们已有的认识,尽量从“设计模式”出发去考量正方形与长方形之间更为合理的关系。  回复  更多评论
  

# re: C#设计模式(2) 2005-12-25 22:27 gshope
这个例子举的的确不是很清楚,如果要证明LSP原则,我各人人为最简单的解释就是:父类中的成员在子类中都有实现,反之则不一定.因此可以用子类实例替换父类实例,以达到相同的功能(具体实现可能不一样),反之则不可以.
如:
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()方法

}
}  回复  更多评论
  

# re: C#设计模式(2) 2005-12-27 11:42 lingate
关于正方形是否是长方形的子类是否是数学上的定论,个人认为暂且不讨论,我认为这里的例子并不是为了说明正方形在任何时候都不适合于做长方形的子类,而是说明在某些约束的情况下,如果在长方形类中约束是:是一个4边形,每一个角都是90度,并且对边相等。那么这时候正方形作为子类就完全合理,而假如在基础类添加一条约束:每一条边和邻边不一定要相等。那么正方形就无法适合于这条约束,那么将出现无法继承的情况。这时候可能就要建立一个直角4方形这么个类,让长方形和正方形都直接这里继承。

所以是否是继承关系关键是约束,不同的约束范围,会产生不同结果。在BOB的<敏捷软件>中有详细说明,大家可以去看看。

再举一个鸭嘴兽的例子,假如浦乳动物的约束是:恒温,喂奶,那么鸭嘴兽可以是浦乳动物的子类,但要是再加一个约束:必须是胎生,那么鸭嘴兽就不可做浦乳动物的子类,目前动物学术界基本上认为鸭嘴兽属于浦乳动物。  回复  更多评论
  

# re: C#设计模式(2) 2006-02-05 13:24 eric164
同意lingate的看法,在上面的例子当中,长方形的类定义中隐含着height不等于width这一约束,否则后面的test本身对于长方形本身来说,其正确性就是不完备的。此时,作为子类必须满足父类的所有约束,而显然正方形是无法满足这个条件的!因而,在此情况下的正方形是绝不能够成为长方形的子类的,他们只能是平级的两个不同的类而已。  回复  更多评论
  

# re: C#设计模式(2) 2006-02-08 11:30 孙涛
只怕楼主说的死循环是设计上的缺陷吧~
我用的是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();
}

}

我传入的矩形是一个正方形,结果也没什么死循环啊。  回复  更多评论
  

# re: C#设计模式(2) 2006-02-08 13:00 孙涛
我简单说下上面的做法。
首先,最初的基类是4边行,具有4条边(没考虑角的情形),按相邻的顺序依次是边1,边2,边3,边4,同时建立构造函数,是需要知道4条边的。然后矩形继承4边行,可以只有长宽就构造一个矩形,当然,原4边形的4条边的属性应该分别对应这里的,宽,长,宽,长(你要说长,宽,长,宽也一样)。其周长的得到式都是4边之和,因此就写在基类就可以了。而面积,对于矩形来说,边1和边2的乘积就可以。
下面是正方形,正方形,是继承矩形的,但其构造函数应该只要一个边就可以了,因此,这里的构造函数,对应矩形构造函数里的两个参数,宽和高都用正方形的边就可以了。这时候,就可以通过getRoundLength()和getArea()很轻松的得到他们的周长和面积。
注:普通的四边形要知道面积,关靠边是不行的,还要引入角的概念,这里就没有复杂化了。  回复  更多评论
  

# re: C#设计模式(2) 2006-02-08 15:43 孙涛
不过代码改进后还是出现了和楼主一样的情况,看来还是lingate
说的对,关键是限制条件的原因:
//四边形
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);
}
}

  回复  更多评论
  

# re: C#设计模式(2) 2006-05-18 16:29 why
为什么要将矩形和长方形划为不同的两个类呢?一个类完全可以,如果真的想区分,只要一个标记属性就可以吧  回复  更多评论
  

# re: C#设计模式(2) 2006-06-13 17:10 LEEM
正方形确实就是长方形的一个特例,在一般逻辑下就是长方形的一个子类。在长方形的定义当中,并没有硬性规定相邻的两边一定要不相等,在上面的例子中却添加了这一条约束,当然就把正方形拒之门外了。

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就错了,面积是子类的内部逻辑算出来的,并不是输入的人认为是多少就多少。  回复  更多评论
  

# re: C#设计模式(2) 2006-07-25 00:51 limon7
具体是否符合LSP是要在具体的应用中检验的,本身是否符合数学定义在这里不是决定因素,虽然这个SmartTest的类取的不是太妥当,但如果实际应用中的确有这样的要求就应该重新考虑OO的实现关系,既有的对象关系在新的需求下很可能被打破,所谓具体问题要具体分析  回复  更多评论
  

# re: C#设计模式(2) 2006-09-26 15:26 PureEviL
正方形可否是长方形的子类,是由用途决定的,如果只是算面积,就可以是子类。但是,如果这样设计,扩展性就太有限了,是很不好的设计。
数学上是可以说正方形是长方形的特例,但也存在,圆是正多边形的特例,也就是说一个正无穷变形就是圆,如果按此设计,圆继承自多边形,那就没什么意义了。
所以类设计,并不是想当然的,要面向应用,提取共同点。  回复  更多评论
  

# re: C#设计模式(2) 2006-11-27 17:10 max[匿名]
gloria 他的意见,我还是比较赞同的~


------------------------------
Square s = new Square();
s.setWidth(8);
s.setHeight(5);
正方形的面积是25
----------------------------
这个是从一开始就假设为矩形了,正方形明显的不可能邻边不等~  回复  更多评论
  

# re: C#设计模式(2) 2006-12-30 14:35 dan
public class Rectangle : Quadrangle
{
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);
}
}

就正方形和长方形来说,正方形就是一个长宽相等的长方形,为什么不能继承?
原文地址:https://www.cnblogs.com/nianshi/p/781751.html