面向对象程序设计最基本的一个特点在于类可以继承——即通常我们所谓之“代码复用”原则优先。继承可以来自一个普通类,也可以来自一个抽象类,那么哪个更好一些呢?或者继承自普通类和抽象类在应用上有何区别呢?今天主要来讨论这个问题。
一、继承自普通类+virtual方法:
譬如我设计了一个点的类(Point),这个“点”类其中包含X,Y两个基本公共属性。那么以后随着需求增加我或许要定义一个三维的店DPoint,由于从数学角度分析,三维坐标比二维坐标只是多了一个Z,其余属性基本相似。因此直接可以通过继承二维坐标的Point类扩展一个Z属性即可。
进一步考虑。如果我已知一个点,输入另外一个点的坐标或者实体类,求该两点间距离公式的话。我们可以在Point类中增加一个计算两点间距离公式的函数,但是考虑这个公式不能为子类所使用(因为子类是三维坐标点,父类却是二维坐标点),因此子类的方法必须覆盖父类计算自己的坐标距离公式,因而通过virtual做这样的扩展(给出完整代码):
[C#]
namespace CSharp { /// <summary> /// 父类 /// </summary> public class Point { public double X { get; set; } public double Y { get; set; } public virtual double Distance(Point p) { //省略实现部分 } } public class DPoint:Point { public double Z { get; set; } public override double Distance(Point p) { DPoint dp = p as DPoint; if (dp!=null) { //省略实现部分 } else { throw new Exception("二维坐标点不能和三维坐标点混淆计算。"); } } } }
[VB.NET]
Namespace CSharp ''' <summary> ''' 父类 ''' </summary> Public Class Point Public Property X() As Double Get Return m_X End Get Set m_X = Value End Set End Property Private m_X As Double Public Property Y() As Double Get Return m_Y End Get Set m_Y = Value End Set End Property Private m_Y As Double Public Overridable Function Distance(p As Point) As Double '省略实现部分 End Function End Class Public Class DPoint Inherits Point Public Property Z() As Double Get Return m_Z End Get Set m_Z = Value End Set End Property Private m_Z As Double Public Overrides Function Distance(p As Point) As Double Dim dp As DPoint = TryCast(p, DPoint) '省略实现部分 If dp IsNot Nothing Then Else Throw New Exception("二维坐标点不能和三维坐标点混淆计算。") End If End Function End Class End Namespace
注意1:父类计算的是Point类距离,子类却要计算DPoint的,因此子类覆盖父类的函数须判断传入的那个Point是不是DPoint(这一点在继承关系的时候尤为注意,此例是巧合——参数类型恰巧是继承类,如果考虑到子类覆盖父类的时候,子类的参数类型——无论是传参还是返回参数,如与父类差别较大,或许要定义object等来解决)。
注意2:其实两点间距离公式不属于某个类的实体,应该定义成静态类(如下,因为静态类不能被继承)。不过这里为了说明virtual在继承中的应用故不采取此类方法。
namespace CSharp { /// <summary> /// 父类 /// </summary> public class Point { public double X { get; set; } public double Y { get; set; } public static double Distance(Point p,Point p2) { //省略实现部分 } } public class DPoint:Point { public double Z { get; set; } public static double Distance(DPoint p1,DPoint p2) { //省略实现部分 } } }
[VB.NET]
Namespace CSharp ''' <summary> ''' 父类 ''' </summary> Public Class Point Public Property X() As Double Get Return m_X End Get Set m_X = Value End Set End Property Private m_X As Double Public Property Y() As Double Get Return m_Y End Get Set m_Y = Value End Set End Property Private m_Y As Double Public Shared Function Distance(p As Point, p2 As Point) As Double '省略实现部分 End Function End Class Public Class DPoint Inherits Point Public Property Z() As Double Get Return m_Z End Get Set m_Z = Value End Set End Property Private m_Z As Double Public Shared Overloads Function Distance(p1 As DPoint, p2 As DPoint) As Double '省略实现部分 End Function End Class End Namespace
总而言之,如果说A和B两个类之间语义上存在着Is关系,并且后者(指子类)大都沿用前者(父类)的属性/方法等(一般地,子类仅对父类进行部分方法重写和扩展)推荐使用的是直接继承自普通类的方法,并且重写带有virtual方法/属性等。
顺便提及一句,不少书不提倡普通类相互继承,对此我的看法是视情况而定:因为继承自一个普通类势必是继承其全部可继承的方法/属性/事件等全部“遗物”,因此,子类实例化时必将加载父类全部信息这势必增加内存开销,如果父类大部分功能子类不用,或者使用得很少,或者大多数情况下子类是重写父类的方法,而不是直接引用的话,这样的设计也是失败的,至少是不理想的。
这里给出一个典型的例子:譬如我要设计一个计算长方形和正方形的类——按照数学上的定义:正方形是长方形的特例(因为正方形的长宽相等)。但实际情况是正方形只需设置“长”或者“宽”(其中之一)就可以了,根本无需两个都设置(也就是说——正方形的长宽应该自动调整到一致,而非用户手动去调整)。就这一点而言,显然让正方体去继承长方体不是很合适(因为正方体不会直接用到长方法的属性定义,而是要重新定义这些属性以便同步的)。然而对于长方形而言,长和宽没有关系,可以分别设置。同时无论是长方形还是正方形,面积计算都是“长”乘以“宽”。因此我们可以把这两个类的公共方法(长*宽)提取出来,并且把“长”和“宽”也作为公共属性提取出来。这一类情况中,明显覆盖要比继承多得多,但是也有继承的关系存在。对于此类覆盖远大于继承,且两个类之间并不存在明显的Is关系,同时两个类(或更多的这些类)却有一个共同的“始祖”的情况下,用抽象类设计较为适合。下面就来谈一谈继承自抽象类的问题。
二、继承自抽象类:
先根据以上分析,我们不妨先定义一个“四边形”的抽象类,如下:
[C#]
/// <summary> /// 一般利用宽和高求出面积的四边形抽象类 /// </summary> public abstract class QuadRangle { public abstract double Width { get; set; } public abstract double Height { get; set; } public double GetSquare() { return Width * Height; } }
[VB.NET]
''' <summary> ''' 一般利用宽和高求出面积的四边形抽象类 ''' </summary> Public MustInherit Class QuadRangle Public MustOverride Property Width() As Double Public MustOverride Property Height() As Double Public Function GetSquare() As Double Return Width * Height End Function End Class
实现的“长方形”和“正方形”分别如下:
[C#]
/// <summary> /// 正方形 /// </summary> public class Square : QuadRangle { public Square(double _size) { Width = _size; } private double size = 0.0; public override double Width { get { return size; } set { if (value != size) { size = value; } } } public override double Height { get { return Width; } set { Width = value; } } } /// <summary> /// 长方形 /// </summary> public class Retangle : QuadRangle { public Retangle(double width,double height) { Width = width; Height = height; } public override double Width { get; set; } public override double Height { get; set; } }
[VB.NET]
''' <summary> ''' 正方形 ''' </summary> Public Class Square Inherits QuadRangle Public Sub New(_size As Double) Width = _size End Sub Private size As Double = 0.0 Public Overrides Property Width() As Double Get Return size End Get Set If value <> size Then size = value End If End Set End Property Public Overrides Property Height() As Double Get Return Width End Get Set Width = value End Set End Property End Class ''' <summary> ''' 长方形 ''' </summary> Public Class Retangle Inherits QuadRangle Public Sub New(width__1 As Double, height__2 As Double) Width = width__1 Height = height__2 End Sub Public Overrides Property Width() As Double Get Return m_Width End Get Set m_Width = Value End Set End Property Private Overrides m_Width As Double Public Overrides Property Height() As Double Get Return m_Height End Get Set m_Height = Value End Set End Property Private Overrides m_Height As Double End Class
注:虽然可以考虑直接把长方形的Width和Height设置成为virtual对象,然后正方形继承这个长方形类,并且重写virtual属性的赋值方法,我认为其实也未尝不可。只不过不是非常符合数学正常的思维(因为正方形不是长方形),但是正方形、长方形都是四边形。因此提取一个“四边形”的抽象类的做法比较适宜。
三、不是Is关系的不能继承吗?是Is关系的就一定有共同属性吗?
通过以上说法我们明白无论是继承自普通类,还是继承自抽象类,那个父类和子类始终保持Is关系。那么现在的问题是——假设两个类不是Is关系,但是它们照样相似度很高,是否就一定不能使用继承了呢?我们看这样一个例子:
现在要实现一个“梯形”面积和“三角形”求面积的方法类。
我们分析:在数学上“梯形” Is “四边形”的一种。但是求面积的方法却和“长方形”和“正方形”截然不同——前者“长*宽”,后者确实“(上底+下底)*高/2”(属性名字不同,公式也完全不同)。显然让“梯形”继承“四边形”,属性名称会给调用者带来很大的困惑(梯形有“长”和“宽”?)
进一步我们发现“梯形”属性以及计算公式和三角形类似(因为三角形的上底恒等于0)。但是“梯形”并不属于“三角形”,它们在数学角度(广义上而言就是我们认识世界万物的角度),并不符合!
结论:这两个类不能直接继承自“四边形”,而是需要另外开辟一个抽象类专门计算(上底+下底)*高/2的模板抽象类(暂时命名为TopButtom),则程序如下:
[C#]
/// <summary> /// 通过上底和下底求出面积的一般抽象类x /// </summary> public abstract class TopBottomBase { public abstract double Top { get; set; } public double Bottom { get; set; } public double Height { get; set; } public double GetSquare() { return (Top + Bottom) * Height / 2; } }
[VB.NET]
''' <summary> ''' 通过上底和下底求出面积的一般抽象类x ''' </summary> Public MustInherit Class TopBottomBase Public MustOverride Property Top() As Double Public Property Bottom() As Double Get Return m_Bottom End Get Set m_Bottom = Value End Set End Property Private m_Bottom As Double Public Property Height() As Double Get Return m_Height End Get Set m_Height = Value End Set End Property Private m_Height As Double Public Function GetSquare() As Double Return (Top + Bottom) * Height / 2 End Function End Class
继承此类的类实现如下:
[C#]
/// <summary> /// 梯形 /// </summary> public class TiXing : TopBottomBase { public override double Top { get; set; } } /// <summary> /// 三角形 /// </summary> public class SanJiaoXing : TopBottomBase { public SanJiaoXing(double bottom) { Bottom = bottom; } public override double Top { get { return 0; } set { throw new Exception("三角形没有上底!"); } } }
[VB.NET]
''' <summary> ''' 梯形 ''' </summary> Public Class TiXing Inherits TopBottomBase Public Overrides Property Top() As Double Get Return m_Top End Get Set m_Top = Value End Set End Property Private Overrides m_Top As Double End Class ''' <summary> ''' 三角形 ''' </summary> Public Class SanJiaoXing Inherits TopBottomBase Public Sub New(bottom__1 As Double) Bottom = bottom__1 End Sub Public Overrides Property Top() As Double Get Return 0 End Get Set Throw New Exception("三角形没有上底!") End Set End Property End Class
显然地,OOP虽说是“人性化设计”(即按照人类认识自然界发现物种之间规律进行继承式设计),但并不完全如此(如“三角形”和“梯形”两者相似度很高,数学上却完全不是一个概念),这就是计算机程序设计中“继承”和人类认知世界“继承”的差异,两者没有绝对的关系。在面向对象程序设计中,只要是继承的总是Is关系;所以此处的Is关系是仅针对程序中“对象”而言。其判断的标准在于进行继承者和被继承者之间究竟有多少相似性,代码复用(包括可继承的/可重写的)是否达到了最大。满足此类条件才使用继承(也就符合了Is关系),其次尽可能地考虑这些类符合人类认知的“继承”关系。满足前者的不一定满足后者,或许需要一个抽象类来作为中间媒介。