Python 中的面向介面編程
前言
」面向介面編程「
寫 Java
的朋友耳朵已經可以聽出干繭了吧,當然這個思想在 Java
中非常重要,甚至幾乎所有的程式語言都需要,畢竟程式具有良好的擴展性、維護性誰都不能拒絕。
最近無意間看到了我剛開始寫 Python
時的部分程式碼,當時實現的需求有個很明顯的特點:
- 不同對象具有公共的行為能力,但具體每個對象的實現方式又各不相同。
說人話就是商戶需要接入平台,接入的步驟相同,但具體實現不同。
作為一個」資深「 Javaer
,需求還沒看完我就洋洋洒洒的把各個實現類寫好了:
當然最終也順利實現需求,甚至把組裡一個沒寫過 Java
的大哥唬的一愣一愣的,直呼牛逼。
不過事後也給我吐槽:
- 你這設計是不錯,但是感覺好複雜,跟程式碼時要找到真正的業務邏輯(實現類)得繞幾圈。
截止目前 Python
寫多了,我總算是能總結他的感受:就是不夠 Pythonic
。
雖說 Python
沒有類似 Java
這樣的 Interface
特性,但作為面向對象的高級語言也是支援繼承的;
在這裡我們也可以利用繼承的特性來實現面向介面編程:
class Car:
def run(self):
pass
class Benz(Car):
def run(self):
print("benz run")
class BMW(Car):
def run(self):
print("bwm run")
def run(car):
car.run()
if __name__ == "__main__":
benz = Benz()
bmw = BMW()
run(benz)
run(bmw)
程式碼非常簡單,在 Python
中也沒有類似於 Java
中的 extends
關鍵字,只需要在類聲明末尾用括弧包含基類即可。
這樣在每個子類中就能單獨實現業務邏輯,方便擴展和維護。
類型檢查
由於 Python
作為一個動態類型語言,無法做到 Java
那樣在編譯期間校驗一個類是否完全實現了某個介面的所有方法。
為此 Python
提供了解決辦法,那就是 abc(Abstract Base Classes)
,當我們將基類用 abc
聲明時就能近似做到:
import abc
class Car(abc.ABC):
@abc.abstractmethod
def run(self):
pass
class Benz(Car):
def run(self):
print("benz run")
class BMW(Car):
pass
def run(car):
car.run()
if __name__ == "__main__":
benz = Benz()
bmw = BMW()
run(benz)
run(bmw)
一旦有類沒有實現方法時,運行期間便會拋出異常:
bmw = BMW()
TypeError: Can't instantiate abstract class BMW with abstract methods run
雖然無法做到在運行之前(畢竟不需要編譯)進行校驗,但有總比沒有好。
鴨子類型
以上兩種方式看似已經畢竟優雅的實現面向介面編程了,但實際上也不夠 Pythonic
。
在繼續之前我們先聊聊介面
的本質到底是什麼?
在 Java
這類靜態語言中面向介面編程是比較麻煩的,也就是我們常說的子類向父類轉型,因此需要編寫額外的程式碼。
帶來的好處也是顯而易見,只需要父類便可運行。
但我們也不必過於執著於介面,它本身只是一個協議、規範,並不特指 Java
中的 Interface
,甚至有些語言壓根沒有這個關鍵字。
動態語言的特性也不需要強制校驗是否實現了方法。
在 Python
中我們可以利用鴨子類型來優雅的實現面向介面編程。
在這之前先了解下鴨子類型,借用維基百科的說法:
- 「當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。」
我用大白話翻譯下就是:
即便兩個完全不想乾的類,如果他們都實現了相同的方法,那就可以把他們當做同一類型的類來使用。
舉個簡單例子:
class Order:
def create(self):
pass
class User:
def create(self):
pass
def create(obj):
obj.create()
if __name__ == "__main__":
order = Order()
user = User()
create(order)
create(user)
這裡的 order
和 user
本身完全沒有關係,只是他們都有相同方法,又得益於動態語言沒法校驗類型的特點,所以完全可以在運行的時候認為他們是同一種類型。
因此基於鴨子類型,之前的程式碼我們可以稍作簡化:
class Car:
def run(self):
pass
class Benz:
def run(self):
print("benz run")
class BMW:
def run(self):
print("bwm run")
def run(car):
car.run()
if __name__ == "__main__":
benz = Benz()
bmw = BMW()
run(benz)
run(bmw)
因為在鴨子類型中我們在意的是它的行為,而不是他們的類型;所以完全可以不用繼承便可以實現面向介面編程。
總結
我覺得平時沒有接觸過動態類型語言的朋友,在了解完這些之後會發現新大陸,就像是 Python
老手第一次使用 Java
時;雖然覺得語法啰嗦,但也會羨慕它的類型檢查、參數驗證這類特點。
動靜語言之爭這裡不做討論了,各有各的好,鞋好不好穿只有自己知道。
隨便提一下其實不止動態語言具備鴨子類型,有些靜態語言也能玩這個騷操作,感興趣下次再介紹。