Python设计模式二:接口类和抽象类(Python接口设计)

系列文章目录

Python设计模式一:面向对象编程



前言

  继第一篇面向对象编程,在文末也提到了interface这个其他OOP语言的语法规则,那让我们开始第二篇接口类和抽象类的介绍。
  在学习设计模式的时候,发现"基于接口编程而非实现"这个设计原则非常的有用,但一直只接触Python的话,对接口的概念大概是一直停留在API接口上。所以对这个设计原则会非常陌生,更别提接口类和抽象类的定义了,那这篇文章就值得一看。
  先带着问题来看:

  • 什么是接口类和抽象类?什么是”基于接口编程而非实现“
  • Python里如何实现接口类和抽象类?
  • 接口类和抽象类的应用场景有哪些?它的作用到底是啥?

一、接口类和抽象类是什么?

  我们之所以对这两个概念模糊,是因为Python的原生语法是不支持的,而C++只支持抽象类,java支持接口和抽象类,Go只支持接口类。但在Python里我们可以通过abc这个库来额外实现接口类和抽象类。
  关于接口类在其他面向对象语言里称之为interface,

  • 接口类定义:接口类相比于普通类不支持实例化,仅包含抽象方法而不包含属性,实现接口类的必须实现其中所有方法,标识一种has-a的关系,即拥有某种行为的概念。
  • 抽象类定义:抽象类相比于普通类不支持实例化,既包含方法也包含属性,子类继承抽象类的属性和方法,其中子类必须覆盖实现抽象类的所有抽象方法。标识一种is-a的关系,即属于某种东西的概念。

  这么说还是比较模糊,举个例子来说,还是拿熟悉的”鸭子类型“来说,假如我们抽象出来鸭子的抽象基类,继承这个抽象基类的所有子类即是各种类型的鸭子,比如会飞和不会飞的鸭子、会游和不会游的鸭子,他们都具备吃东西的能力这一方法,都具备嘴、眼睛、耳朵等基本属性,所以我们有了抽象基类鸭子,其他子类鸭子继承该鸭子即表示is-a的关系。另外不同的子类鸭子有不同的行为或者能力,比如会飞和不会飞的能力、会游和不会游的能力,那这时候我们可以把飞和游这些行为剥离出去作为一个接口,使用会飞的接口的鸭子即拥有飞的能力,使用会游的接口的鸭子即表示拥有游的能力,那这就是has-a的关系。
  如果你对基类为什么要用抽象类比较疑惑的话,这是因为用抽象类可以强制子类实现吃东西这一基本方法,因为不同的鸭子吃东西是不同的表现,但它们都会需要吃东西,所以我们可以用抽象基类来强制每个子类鸭子实现自己的吃东西的样子,也许你会说自己会在每个子类里都实现,但是如果在大型程序里,你如何保证其他人都会记得实现?假如这个抽象基类还不止一个抽象方法呢?那总会有人忘了要实现那些方法,所以靠人为约定是不靠谱的(这个例子会比较勉强,这里你可以理解为你设计了一个基类,这个基类的某个方法必须要所有子类继承,这样在实现多态也就是调用子类的方法时,不会报错无该方法,而这也就是抽象基类的目的)。另外,如果你还对接口的使用比较迷惑,后面会详细阐述。
  可以参照下图的思维导图有个初步的印象:
在这里插入图片描述
  说到这,还是会比较疑惑,那是因为Python里使用的非常少,包括一些源码都很少用到这两个概念,因为普通类也能达到接口类和抽象类的效果,另外Python作为动态语言,它使用一些设计模式会更加的灵活。

基于接口编程而非实现

  让我们讲讲什么是”基于接口编程而非实现“,在《headfirst设计模式》一书的第一章,就讲述了该设计原则,而同时也是第一章,结合的场景和分析非常的引人入胜,强烈推介这本书,无关语言,就是单纯地讲不同场景和案例下适用的设计模式。
  基于接口而非实现编程的英文是:Program to an interface, not an implementation,更多强调的是抽象意识。在《headfirst设计模式》一书中就举了上述鸭子的例子,可能你会好奇,为什么我一定要用接口来表示鸭子的行为?为什么不直接在抽象类里定义好剩下的行为,比如定义通用的方法fly和swim来让所有的鸭子来继承,然后每个鸭子各自实现该方法。那这样行不行?坦白说,当然可以,但是有点问题:

  • 问题1:如果除了飞、游以外还有很多行为,比如是否会下蛋、是否会叫等等,那是不是每个鸭子都得去一一审视这些方法,然后逐一重写?如果忘了重写,可能不会下蛋的鸭子就自动继承了基类的会下蛋方法,那程序就混乱了。
  • 问题2:针对问题1,要定义那么多方法,不好管理;随着需求的演进,以后每新增一种行为,那所有子类都得考虑到,这个带来的影响非常巨大。

  对于问题1和问题2,它们根本的问题是基于一种实现的理念来编程,也就是子类都继承于父类的实现,如果父类不具备我要的实现能力,那我就重写该方法,这随着程序的庞大,后续的维护和扩展会更加的困难。所以此时引入接口,来将这部分行为剥离出去,比如飞则剥离出去FlyWithWings和FlyNoWay这两个类里,这两个类分别实现flyable这个接口;同样的游这个行为剥离出去为SwimEnable和SwimNoWay这两个类,分别实现swimable这个接口,这样每个鸭子只要对应的需要哪些能力就使用哪些接口即可。
  接口的引入大大加强了扩展性和可维护性,比如鸭子A初始的设计是只具备会飞和会游,那我们实现鸭子A,使用FlyWithWings和SwimEnable这两个接口类即可,随着需求的演进,需要让鸭子A还要会下蛋,此时再让其使用LayEggs接口类即可;或者鸭子A的飞要改为跳跃飞,那就实现一个FlyWithJump接口类,替换原来的FlyWithWings即可;以后要实现鸭子B只会吃,其他啥都不会,那就继承抽象基类,剩下的接口都不需要继承即可;可以发现扩展能力非常强,对于大部分需求的演变,都不需要去改动基类或者增加基类的方法。
  而如果懂了上述的那些,那也代表着你掌握了策略模式这一设计模式,不同鸭子具备的能力,即为一个个的策略,针对不同的场景我们可以使用不同的策略,详细的后续章节再聊。

二、Python里如何实现接口类和抽象类?

  Python里通过abc这个库来实现接口类和抽象类,不同于Java这种有明确的语法区分接口类和抽象类,在Python里,这两者只是概念上的区别,在实现上都一致。因为Python没有interface这语言特性,所以接口类本质上也是通过继承来实现的。而接口类的实现类也不同于java的implements关键字,只能通过继承来实现接口特性。
  那我们实现鸭子抽象基类,有基本属性和抽象方法eat,代码如下:

from abc import ABCMeta, abstractclassmethod


class Duck(metaclass=ABCMeta):
    mouth = None
    ear = None
    eye = None

    @abstractclassmethod
    def eat(self):
        pass


class DuckA(Duck):
    pass

duck = Duck()
ducka = DuckA()

  按上述执行,两个实例化都会报错,在Python里都是报没有实现抽象方法eat的TypeError,如下图所示:
  那接口类如何实现呢?此时我们再定义FlyEnable这一接口类,然后定义FlyWithWings和FlyNoWay继承接口类,然后再让duckA继承需要的实现类来解决,如下:

class FlyEnable(metaclass=ABCMeta):
    @abstractclassmethod
    def fly(self):
        pass


class FlyWithWings(FlyEnable):
    def fly(self):
        print("fly with wings")


class DuckA(Duck, FlyWithWings):
    def eat(self):
        print("duckA eat ...")



ducka = DuckA()
ducka.eat()
ducka.fly()

结果是:
在这里插入图片描述

三、接口类和抽象类的应用场景有哪些

总结

  没想到在解释概念的时候花费了较多的篇幅,希望能都理解~~

  • 11
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值