我们首先来看下抽象class能发挥优势的使用场景。
假设有一个Cars基类,具体型号的Car继承该基类,并实现自己独有的属性或方法。
public class Cars { public string Wheel() { return "I have 4 wheeler"; } }
有两种具体型号的汽车CarA和CarB均继承自Cars基类。也即它们拥有Cars基类的属性和方法。现在有一个需求,即需要添加一些对CarA和CarB类通用(commen)的但各自的实现不同的方法,比如colors方法,我们如何做到这一点呢?下面罗列了可能想到的解决方法。
- 直接在Cars基类中添加方法,让CarA和CarB继承
- 新创建一个interface,让CarA和CarB继承Cars类和该interface
- 新创建一个抽象class,让CarA和CarB仅继承该抽象class
下面论证每种方案的可行性。
- 对于方案1而言,由于普通的Cars基类中仅能添加有共同实现(common implementation)的普通(normal)方法,故不能满足需求。
- 方案2可行,但需要子类继承interface和Cars基类,具体实现如下:
interface IExtra { string colors(); }
3. 方案3可行,将具有共同实现的方法定义为普通方法,将通用但各自实现不同的方法定义为抽象方法,这两种方法均封装在抽象class中,子类只需继承该抽象class。另外,该抽象基类中还可以添加一些公共fields,这是interface所不能实现的。
public abstract class Cars { //将公有但实现(implementation)不同的功能声明为抽象方法 public abstract string colors(); //将公有且实现(implementation)相同的功能声明为普通方法 public string Wheel() { return "4 wheeler"; } }
从以上代码实现可以看到,使用抽象class能封装字段,公有且实现相同的方法和公有但实现不同的方法(抽象方法),其作用相当于一个普通class和interface。故当我们需要实现一个包含公有且实现相同的方法和公有但实现不同的方法的基类(或许还可能包含字段)时,应该首先考虑创建抽象class,然后才是interface(需要和一个普通class配合来达到和抽象class相同的效果)。
现在,我们来看下抽象class的第二种用途。
还是拿上面的Cars基类举例,我们让CarA类继承Cars基类,并添加自己的DataRecorder方法。本来用户能通过创建CarA类的实例对象来访问CarA类和Cars基类的属性和方法,但这里的主要问题是允许创建Cars基类的实例对象,但基类对象不能够访问子类独有的属性和方法,这并不满足我们的要求。为了限制这一点,我们应禁止在子类中创建基类的对象,仅允许创建子类对象来同时访问子类和基类的属性和方法。
接下来我们讨论接口interface的使用场景,在讨论之前,我们先看一个OOP中存在的多继承(Multiple Inheritance)问题。
在C++中存在多继承(Multiple Inheritance)的概念,但是多继承存在一个严重的问题,即Diamond Problem。
public class print1 { public void print() { Console.WriteLine("hello"); } } public class print2 { public void print() { Console.WriteLine("world"); } } class Program : print1, print2 { static void Main(string[] args) { } }
以上就是多继承(Multiple Inheritance)的例子,该例子不能通过编译,因为在class Program中,如果调用继承的print方法,系统将不能确定到底调用从class print1还是从class print2继承来的print方法,从而引发程序错误。通过使用interface可以解决这个问题。
public interface print1 { void print(); } public interface print2 { void print(); } class Program : print1, print2 { void print1.print() { Console.WriteLine("hello"); } void print2.print() { Console.WriteLine("world"); } static void Main(string[] args) { Program p = new Program(); ((print2)p).print(); Console.ReadLine(); } }
现在,我们看看interface如何在实际开发场景中使用interface。还是拿上面的Cars类例子来讲,现在CarA要添加行车记录仪新功能DataRecorder,但CarB不具备该功能。如何实现这一需求?
对于这一新需求,我们通常能想到以下4种解决方案:
- 创建一个新的普通class,该class定义来DataRecorder方法,然后让CarA类继承该新class
- 直接在Cars类中添加DataRecorder方法,并将该方法声明为abstract方法
- 创建一个抽象class,该抽象class定义一个抽象DataRecorder方法,然后让CarA类继承该抽象class并实现DataRecorder方法
- 直接在CarA类中创建DataRecorder方法并使用它
- 利用interface实现
下面依次验证每个解决方案的可行性。
- 对于方案1来讲,由于CarA已经继承了Cars基类,此时不能同时继承新创建的普通class,故此方案不可行。
- 对于方案2来讲,当CarB继承Cars类时也必须override该DataRecorder方法,这违背了我们的需求。
- 方案3和方案1情形相同。
- 方案4似乎是能想到的最直接的方式,简单粗暴。这个方案在当添加的方法较少且我们能保证记住每个要添加的方法情形下工作良好,但是当添加的方法较多时,若我们忘记添加其中一两个方法时,没有一种机制通知我们这种疏忽。若没有语法错误,程序就能编译通过,但执行时不能得到期望输出。
- 方案5能克服上述方案的缺点,可行,代码实现见下面:
public class Cars { public string Wheel() { return "I have 4 wheeler"; } }
interface IDataRecorder { void DataRecorder(); }
interface IDataRecorder { void DataRecorder(); } public class CarA:Cars, IDataRecorder { public void DataRecorder() { Console.WriteLine("DataRecorder supported."); } static void Main(string[] args) { CarA car = new CarA(); Console.WriteLine(car.Wheel()); car. DataRecorder(); Console.ReadLine(); } }
以上方案5代码实现克服了方案1和3多继承(Multiple Inheritance)报错问题,同时克服了方案2中CarB类必须override DataRecorder方法的缺陷,并且在后期维护添加多个方法时由于疏忽导致忘记添加某些方法时,能通过编译错误提醒开发者,从而克服了方案4的问题。
除了从使用场景等表象层区分抽象类和接口,我们还能从以下三个更接近本质的方面对接口和抽象类进行区分:
- 类是对对象的抽象,抽象类是对类的抽象,接口是对行为的抽象。接口是对类的局部(行为)进行的抽象,而抽象类是对类整体(字段、属性、方法)对抽象。如果只关心行为抽象,那么也可以认为接口就是抽象类。
- 如果行为跨越不同类的对象,可使用接口;对于一些相似的类对象,用继承抽象类。抽象类本质上表达的是“is-A”(是...的一种)的关系,而接口本质上表达的是can-do的关系,即实现了接口就具备了某种功能。实现接口和继承抽象类并不冲突。
- 从设计角度讲,抽象类是从子类中发现了公有的东西,然后进行提取形成抽象类,然后子类继承父类,而接口根本不知道子类的存在,方法如何实现还不确定,预先定义。可以说,抽象类自底而上抽象出来的,而接口是自顶而下设计出来的。
具体展开第三点来讲,就是说往往我们在开发一个大型应用时,事先定义一个类,之后某个时间点随着功能的增加又添加了一个类,此时发现该类和事先定义的一个类有较多类似之处,此时就可以泛化出抽象类,让子类继承抽象类,复用代码的同时也提升了应用后期的可维护性。这也体现了敏捷开发的思想,通过重构改善既有代码的设计。事实上,在只有一个类时,就去考虑定义抽象类,极有可能造成过度设计。所以说抽象类往往都是通过重构得来的。当然,如果你事先就意识到多种分类的可能,提前设计出抽象类也是完全可以的。