从零开始学设计模式第二天之面向对象和面向四大特性

584 阅读7分钟
  • 什么是面向对象和面向对象编程其它相关概念?
    • 面向对象编程:面向对象编程是一种编程范式或编程风格。它以类或对象作为组织代码的基本单元,并将封装、抽象、继承、多态四个特性,作为代码设计和实现的基石
    • 面向对象编程语言:是支持类或对象的语法机制,并有现成的语法机制,能方便地实现面向对象编程四大特性(封装、抽象、继承、多态)的编程语言。
    • 面向对象分析:面向对象分析就是要搞清楚做什么
    • 面向对象设计:搞清楚怎么做,这两个阶段的最终产出就是类的设计,包括程序被拆解为哪些类、每个类有哪些方法,类与类之间如何交互等等。
  • 面向对象的四大特性
  • 封装:封装也叫做信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据
  • 举例:

"""面向对象封装例子"""

import uuid
from time import time


class Wallet(object):
    def __init__(self):
        self._id = uuid.uuid4()
        self._create_time = time()
        self._balance = 0
        self._balance_modified_time = time()
        self.test = 'xxx'

    def get_wallet_id(self):
        return self._id

    def get_wallet_create_time(self):
        return self._create_time

    def get_balance_modified_time(self):
        return self._balance_modified_time

    def increase_balance(self, increase_balance):
        self._balance += increase_balance

    def decrease_balance(self, decrease_balance):
        self._balance -= decrease_balance


if __name__ == '__main__':
    w = Wallet()
    print(w.test)
    print(w._id)
  • 例子说明

    • 从代码的角度来看,我们可以发现,Wallet类主要有四个属性(也可以叫做成员变量),就是我们前面定义的信息或者数据。其中_id表示钱包的唯一编号,create_time表示钱包的创建时间,balance表示钱包中的余额,balance_modified_time表示上次钱包余额变更的时间。我们运用封装的特性,对钱包的四个属性的访问方式进行了限制。调用者只允许通过上面这5个方法来访问钱包中的属性或者修改
    • get_wallet_id()
    • get_wallet_create_time()
    • get_balance_modified_time()
    • increase_balance()
    • decreace_balance()
    • 这样设计是目的:从业务的角度来说,id、create_time是创建钱包的时候就确定好了,之后不应该再改动,所以,我们并没有在Wallet类中,暴露id、create_time这两个属性的任何修改方法,比如说set方法。而且,这两个属性的初始化设置,对于wallet类的调用者来说也是透明的,因此是内部初始化的,不需要传进来的。对于钱包余额balance这个属性来说,从业务的角度来说,只能增或者减,不会被重新设置。所以在wallet类中,只暴露了increase_balance()和decrease_balance()方法,并没有暴露set方法。对于balance_modified_time这个属性的修改操作完全封装在了increase_balance()和decrease_balance这两个方法中,不对外暴露任何修改这个属性的方法也细节。
    • 所以总的来说,封装主要是为了隐藏数据或者数据访问保护,对于封装这个特性,我们需要编程语言本身提供一定的语法机制来支持。这个语法机制就是访问权限控制。比如说private、public等关键字就是java语言中的访问权限控制语法。python就是通过单下划线或者双下划线来定义私有
    • 封装的意义和解决哪些编程问题
      • 如果对类中的属性的访问不做任何限制,那任何代码都可以访问、修改类中的属性,这样是不可控制的,属性可以被随意以各种奇葩的方式修改,而且修改逻辑可能散落在代码中的各个角落,势必影响代码的可读性和可维护性。比如说某个同事可以偷偷修改wallet中的余额。另外一方面,类通过有限的方法暴露必要的操作,也能提高类的易用性。如果所有方法都暴露了,那么调用者必须熟悉业务才行。相反如果隐藏了,调用者不必知道背后太多的业务细节,用错的概率会少很多

  • 抽象:封装主要讲的是如何隐藏信息、保护数据、而抽象讲的是如何隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的。

  • 实际上,类的方法是通过编程语言中的“函数”这一语法机制来实现的。通过函数包裹具体的实现逻辑,这本身就是一种抽象。也就是说,我们其实定义的函数就是一种抽象的特性了。调用者在使用函数的时候,并不需要去研究函数内部的具体实现逻辑,只需要通过函数的命名、注释或者文档,了解其提供了什么功能,就可以额直接使用了

  • 抽象的意义和解决哪些编程问题?

    • 抽象和封装其实都是人类处理复杂性的手段。面对复杂的系统的时候,人脑能承受的信息复杂程度有限,所以我们需要忽略掉非关键性的实现细节。而抽象作为一种只关注功能点不关注实现的设计思路,正好符合这种逻辑。抽象在代码设计中,起到非常重要的指导作用。很多设计原则都体现了抽象的思想,比如说基于接口而非实现编程,开放封闭。换一个角度,在命名类的方法的时候,也要有抽象思维,不要再方法名定义中暴露太多细节,保证在某个时间点需要改变方法的实现逻辑的时候,不用去修改其定义。比如说,get_aliyun_picture,就不是一个具有抽象思维命名,应该是一个get_picture()

  • 继承:继承用来表示类之间的is-a关系,比如说猫是一种哺乳动物。从继承关系上来讲,继承可以分成两种模式,单继承和多继承。单继承表示一个子类只继承一个父类,多继承表示一个子类可以继承多个父类,比如说猫既是哺乳动物又是爬行动物

  • 继承的意义和解决哪些编程问题?

    • 继承的最大的好处就是代码复用。假如两个类有一些相同的属性和方法,就可以将这些相同的部分抽取到父类中,让两个子类继承父类。这样子,两个子类就可以重用父类中的代码,避免代码重复写多遍。不过继承会有一个过度使用继承的问题,也就是说继承层次过深过于复杂,就会导致代码可读性、可维护性变差。导致为了查看一个功能,需要不停的深入查看父类代码

  • 多态:多态是指,子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现。多态这种特性也是需要编程语言提供特殊的语法机制来实现的,而python崇尚的是鸭子模型,也就是只要两者都有相同的方法,就可以实现多态,并不需要要求它们有什么关联。

  • 举例子:

  • 利用鸭子类型 duck-typing来实现多态特性,如下面的例子所示,鸭子类型实现多态的方式非常灵活,虽然Logger和DB两个类没有任何关系,既不是继承关系,也不是接口跟实现关系,但是他们都定义了record()方法,就可以被传递到test(recorder)方法中,在实际运行的时候,执行对应的record(),也就是说,两个类只要具有相同的方法,就可以实现多态,并不要求两个类之间有任何关系,这就是duck-typing。

class Logger:
    def record(self):
        print("I write a log into file.")


class DB:
    def record(self):
        print("I insert data into db. ")


def test(recorder):
    recorder.record()


def demo():
    logger = Logger()
    db = DB()
    test(logger)
    test(db)
  • 多态实现的意义和解决的编程问题是什么?
    • 多态能够提高代码的可扩展性和复用性。就像上面的test(recorder)方法实现的是数据记录的功能,对于新添加的类,比如说Logger和DB,我们只要实现了record()方法,那么就能够传入到test(recorder)中进行实现数据记录的功能,不管有多少地方和类需要实现record方法,只需要新增加一个类,然后实现自己的record方法就行了,原来的test(recorder)方法甚至不需要改动,因此这样代码的可扩展性和复用性就很高。多态也是很多设计模式、设计原则的代码实现基础。