前言:最近一個認識的朋友準(zhǔn)備轉(zhuǎn)行做編程,看他自己邊看視頻邊學(xué)習(xí),挺有干勁的。那天他問我接口和抽象類這兩個東西,他說,既然它們?nèi)绱讼嘞瘢?我用抽象類就能解決的問題,又整個接口出來干嘛,這不是誤導(dǎo)初學(xué)者嗎。博主呵呵一笑,回想當(dāng)初的自己,不也有此種疑惑么!所以今天打算針對他的問題,結(jié)合一個實際的使用場景來分享下抽象類和接口的異同,到底哪些情況需要用接口?又有哪些情況需要用抽象類呢?
一、業(yè)務(wù)場景介紹
博主打算使用原來在華為做外包的時候一個場景:我們針對華為里面的設(shè)備做了一個采集設(shè)備使用率的程序,設(shè)備的類型很多,各種設(shè)備的登錄和注銷方式基本相同,但是每種設(shè)備的采集的規(guī)則又不太相同。大致的場景就這樣,我們來看代碼吧。
二、代碼示例
根據(jù)業(yè)務(wù)場景,我們簡單搭建代碼,先來看看代碼結(jié)構(gòu)圖:

ESTM.Spider:項目的入口程序,只為測試,這里就簡單用了一個控制臺程序。
ESTM.Spider.Huawei:華為設(shè)備采集規(guī)則,定義接口抽象實現(xiàn)和具體實現(xiàn)。
ESTM.Utility:解決方案的工具類和接口。
下面來看看具體的實現(xiàn)代碼:
1、工具類
namespace ESTM.Utility
{
public class LoginUser
{
public string Username { set; get; }
public string Password { set; get; }
}
public class Device
{
public string DeviceType { set; get; }
public int WaitSecond { set; get; }
}
}
2、接口設(shè)計:ISpider.cs
namespace ESTM.Utility
{
//采集接口,定義采集的規(guī)則
public interface ISpider
{
bool Login(LoginUser oLoginUser);
string Spider(Device oDevice);
void LoginOut();
}
}
3、接口抽象實現(xiàn)類:SpiderBase.cs
/// <summary>
/// 公共的采集基類
/// </summary>
public abstract class SpiderBase : ISpider
{
//華為設(shè)備統(tǒng)一采用Telnet方式登錄。統(tǒng)一用戶名密碼都是admin。
public virtual bool Login(LoginUser oLoginUser)
{
Console.WriteLine("華為設(shè)備采用Telnet方式登錄。");
var bRes = false;
if (oLoginUser.Username == "admin" && oLoginUser.Password == "admin")
{
Console.WriteLine("用戶名密碼校驗正確,登錄成功");
bRes = true;
}
else
{
Console.WriteLine("用戶名密碼校驗錯誤,登錄失敗");
}
return bRes;
}
//采集操作和具體的設(shè)備類型相關(guān),這里用抽象方法,要求子類必須重寫
public abstract string Spider(Device oDevice);
//華為設(shè)備統(tǒng)一注銷
public virtual void LoginOut()
{
Console.WriteLine("華為設(shè)備采用Telnet方式注銷");
}
}
4、接口具體實現(xiàn)類
[Export("MML", typeof(ISpider))]
public class SpiderMML:SpiderBase
{
//MML設(shè)備采集
public override string Spider(Device oDevice)
{
Console.WriteLine("MML設(shè)備開始采集");
return "MML";
}
}
[Export("TL2", typeof(ISpider))]
public class SpiderTL2:SpiderBase
{
//TL2設(shè)備采集
public override string Spider(Device oDevice)
{
Console.WriteLine("TL2設(shè)備開始采集");
return "TL2";
}
}
5、在控制臺調(diào)用
class Program
{
//通過依賴注入來注入具體的實現(xiàn)類對象(SpiderMML對象)
[Import("MML", typeof(ISpider))]
public ISpider spider { set; get; }
static void Main(string[] args)
{
var oProgram = new Program();
RegisterMEF(oProgram);
oProgram.spider.Login(new LoginUser() { Username = "admin", Password = "admin" });
oProgram.spider.Spider(new Device() { DeviceType = "HuaweiDevice", WaitSecond = 100 });
oProgram.spider.LoginOut();
}
#region 注冊MEF
private static void RegisterMEF(object obj)
{
AggregateCatalog aggregateCatalog = new AggregateCatalog();
var thisAssembly = new DirectoryCatalog(AppDomain.CurrentDomain.BaseDirectory, "*.dll");
aggregateCatalog.Catalogs.Add(thisAssembly);
var _container = new CompositionContainer(aggregateCatalog, true);
_container.ComposeParts(obj);
}
#endregion
}
6、說明
這里用到MEF的知識,不懂MEF沒關(guān)系,你暫時把它當(dāng)作一種依賴注入的容器來看吧!這并不影響你對本文的理解!
這是一種比較典型的應(yīng)用場景。接口定義規(guī)則,抽象類定義公共實現(xiàn)或者抽象方法,具體子類實現(xiàn)或者重寫抽象類方法。我們重點來看這里的中間橋梁——抽象類。我們知道,抽象類里面既可以有實現(xiàn)的方法,也可以有未實現(xiàn)的抽象方法。那么這里為什么要有一個中間的抽象類?又為什么不能用一個具體的實現(xiàn)類?請聽博主細細道來。
(1)在這里,Login、LoginOut方法由于子類是通用的具有相同邏輯的方法,所以我們需要在抽象類里面去實現(xiàn)這兩個方法,如果子類沒有特殊需求,調(diào)用的時候直接用父類的方法就好了; 如果子類有特殊需求,可以override父類的方法。這樣設(shè)計既提高了代碼的復(fù)用率,也可以靈活復(fù)寫。從這點來說,這里必須要有一個可以實現(xiàn)方法的類,至于是不是非抽象類不可,我們下面來介紹。
(2)如果這里不用抽象類,就用一個普通的類來代替行不行?博主的答案是:行!但不好!如果你非要說,我用一個普通的類,將public abstract string Spider(Device oDevice);這個方法寫成
public virtual string Spider(Device oDevice)
{
return "";
}
貌似也沒問題,反正子類要重寫的。確實,這樣設(shè)計沒問題,但是如果你不慎子類忘了override呢?編譯可以通過,程序還是會跑起來,運行的時候可能會報錯。而使用抽象類,在這里抽象類能夠約束子類必須要重寫這個方法,如果忘了重寫,編譯直接不通過,這樣看來,這里使用抽象類是不是更好呢~~
(3)綜合上述(1)和(2)來看,這里使用抽象類的好處就很明顯了,一方面抽象類可以有實現(xiàn)類,這是接口不能代替的;另一方面,抽象類可以有抽象方法,約束子類必須重寫,這是普通的類不能代替的。在一定程度上,可以說抽象類具備了接口和普通類的雙重功能。經(jīng)過這樣一分析,你有沒有理解抽象類的作用呢!
三、代碼擴展
以上我們抽象類使用的必要性和使用方法是介紹完了。那么接下來新的問題來了,可能就有人問了,你上面說了叭叭叭說了這么多,無非就是說了抽象類的必要性,那么既然抽象類這么有用,我們直接用抽象類就好了,你干嘛還要弄一個接口呢。談到這里,就要說到面向接口編程。其實,面向接口編程和面向?qū)ο缶幊滩⒉皇瞧郊壍?,它并不是比面向?qū)ο缶幊谈冗M的一種獨立的編程思想,而是附屬于面向?qū)ο笏枷塍w系,屬于其一部分?;蛘哒f,它是面向?qū)ο缶幊腆w系中的思想精髓之一。那么這里是否可以不要接口,直接用抽象類代替呢?答案還是行!但不好!
比如我們現(xiàn)在又來了新的需求,中興也要用我們的采集系統(tǒng),但是它的設(shè)備類型、登錄注銷方式和華為設(shè)備區(qū)別非常大。那么這個時候我們接口的意義就體現(xiàn)了,如果我們使用接口,我們只需要再重寫一個類似ESTM.Spider.Huawei這樣的項目就好了,我們暫且命名叫ESTM.Spider.Zhongxing。我們來看看:
代碼如下:
namespace ESTM.Spider.Zhongxing
{
/// <summary>
/// 中興設(shè)備采集基類
/// </summary>
public abstract class SpiderBase:ISpider
{
//中興設(shè)備通用登錄方法
public virtual bool Login(LoginUser oLoginUser)
{
Console.WriteLine("中興設(shè)備登錄前多了一個數(shù)據(jù)校驗:.......");
Console.WriteLine("中興設(shè)備采用WMI方式登錄。");
var bRes = false;
if (oLoginUser.Username == "root" && oLoginUser.Password == "root")
{
Console.WriteLine("用戶名密碼校驗正確,登錄成功");
bRes = true;
}
else
{
Console.WriteLine("用戶名密碼校驗錯誤,登錄失敗");
}
return bRes;
}
//定義抽象方法,要求子類必須重寫
public abstract string Spider(Device oDevice);
//中興設(shè)備通用注銷
public virtual void LoginOut()
{
Console.WriteLine("中興設(shè)備采用WMI方式注銷");
}
}
}
namespace ESTM.Spider.Zhongxing
{
[Export("ZXGC", typeof(ISpider))]
public class SpiderZXGC:SpiderBase
{
public override string Spider(Utility.Device oDevice)
{
Console.WriteLine("中興ZXGC設(shè)備開始采集");
return "ZXGC";
}
}
}
namespace ESTM.Spider.Zhongxing
{
[Export("ZXGY", typeof(ISpider))]
public class SpiderZXGY:SpiderBase
{
public override string Spider(Utility.Device oDevice)
{
Console.WriteLine("中興ZXGY設(shè)備開始采集");
return "ZXGY";
}
}
}
由于這里采用了接口,我們將ESTM.Spider.Zhongxing這個項目開發(fā)完成后生成dll,將dll放到控制臺程序中,直接通過MEF導(dǎo)入不同的子類對象就可以使用,不需要更改控制臺里面的大部分東西。如果不用接口,而是直接用抽象類代替,那么控制臺里面大部分的代碼都得改,并且控制臺程序依賴多個dll,對設(shè)計的松耦合也不利。博主這里為了簡單,用了MEF來簡單導(dǎo)入,其實正式項目中,應(yīng)該是用工廠采用反射直接創(chuàng)建出具體的實例。
從這點上來說,接口擁有抽象類不具備的特性:定義規(guī)則,實現(xiàn)設(shè)計的松耦合。
四、總結(jié)
1、接口是一組規(guī)則的集合,它主要定義的是事物的規(guī)則,體現(xiàn)了是這種類型,你就必須有這些規(guī)則的概念。它的目的主要是依賴倒置和松耦合,從這點來說,接口不能省掉或者用抽象類代替。總而言之,接口和抽象類不可同日而語。
2、抽象類主要用于公共實現(xiàn)和約束子類必須重寫。以上面的例子說明,Login、Loginout用于公共實現(xiàn),提高了代碼復(fù)用,Spider用于抽象,約束子類必須要重寫Spider方法。這也就是這里不能用普通類的原因。
3、用一句話概括接口和抽象類的區(qū)別:使用抽象類的動機是為了代碼的復(fù)用,而使用接口的動機是為了實現(xiàn)多態(tài)性(依賴倒置)。至于使用的時候到底是用接口還是抽象類,看具體的情況。
本文完!