python面向对象编程设计原则之单一职责原则详解

目录
  • 一,封装
    • (一)什么是封装
    • (二)封装与访问
    • (三)私有化与访问控制
      • 1,属性与方法的私有化
      • 2,变量名压缩
      • 3,方法重载
    • (四)属性引用:getter、setter与property
  • 二,单一职责原则
    • (一)一个不满足单一职责原则的例子
    • (二)单一职责原则
  • 三,封装与单一职责原则
  • 总结

一,封装

封装是面向对象编程思想的重要特征之一。

(一)什么是封装

封装是一个抽象对象的过程,它容纳了对象的属性和行为实现细节,并以此对外提供公共访问。

这样做有几个好处:

  • 分离使用与实现。可直接使用公共接口,但不需要考虑它内部具体怎么实现。
  • 拥有内部状态隐藏机制,可实现信息/状态隐藏。

(二)封装与访问

就面向对象编程来说,类就是实现对象抽象的手段,封装的实现,就是将对象的属性与行为抽象为类中属性与方法。

举个例子:

对象 AudioFile ,需要有文件名,还需要能播放与停止播放。用类封装的话,就类似于下面这个实现:

class AudioFil:
    def __init__(self, filename):
        self.filename = filename
    def play(self):
        print("playing...")
    def stop(self):
        print("stop playing...")

self参数必须是传入类方法的第一个(最左侧)参数;Python 会通过这个参数自动填入实例对象(也就是调用这个方法的主体)。这个参数不必叫self,其位置才是重点(C++或Java程序员可能更喜欢把它称作this,因为在这些语言中,该名称反应的是相同的概念。在Python中,这个参数总是需要明确的)。

封装之后,能轻松实现访问:

if __name__ == "__main__":
    file_name = "金刚葫芦娃.mp3"
    current_file = AudioFil(filename=file_name)
    print(current_file.filename)
    current_file.play()
    current_file.stop()
>>>
金刚葫芦娃.mp3
playing 金刚葫芦娃.mp3...
stop playing 金刚葫芦娃.mp3...

同时能在外部修改内部的属性:

if __name__ == "__main__":
    file_name = "金刚葫芦娃.mp3"
    current_file = AudioFil(filename=file_name)
    print(current_file.filename)
    current_file.play()
    current_file.stop()
    current_file.filename = "舒克与贝塔.ogg"
    print(current_file.filename)
    current_file.play()
    current_file.stop()
>>>
金刚葫芦娃.mp3
playing 金刚葫芦娃.mp3...
stop playing 金刚葫芦娃.mp3...
舒克与贝塔.ogg
playing 舒克与贝塔.ogg...
stop playing 舒克与贝塔.ogg...

(三)私有化与访问控制

尽管能通过外部修改内部的属性或状态,但有时出于安全考虑,需要限制外部对内部某些属性或者方法的访问。

一些语言能显式地指定内部属性或方法的有效访问范围。比如在 Java 中明确地有 publicprivate 等关键字提供对内部属性与方法的访问限制,但 python 并提供另一种方式将它们的访问范围控制在类的内部:

  • _ 或 __来修饰属性与方法,使之成为内部属性或方法。
  • 用 __method-name__ 来实现方法重载。

1,属性与方法的私有化

举个例子:

class AudioFil:
    def __init__(self, filename):
        self._filename = filename
    def play(self):
        print(f"playing {self._filename}...")
    def stop(self):
        print(f"stop playing {self._filename}...")

if __name__ == "__main__":
    file_name = "金刚葫芦娃.mp3"
    current_file = AudioFil(filename=file_name)
    print(current_file._filename)
    current_file.play()
    current_file.stop()

注意 _filename 的格式,单下划线开头表明这是一个类的内部变量,它提醒程序员不要在外部随意访问这个变量,尽管是能够访问的。

更加严格的形式是使用双下划线:

class AudioFil:
    def __init__(self, filename):
        self.__filename = filename
    def play(self):
        print(f"playing {self.__filename}...")
    def stop(self):
        print(f"stop playing {self.__filename}...")

if __name__ == "__main__":
    file_name = "金刚葫芦娃.mp3"
    current_file = AudioFil(filename=file_name)
    print(current_file.__filename)	#AttributeError: 'AudioFil' object has no attribute '__filename'
    current_file.play()
    current_file.stop()

注意 __filename 的格式,双下划线开头表明这是一个类的内部变量,它会给出更加严格的外部访问限制,但还是能够通过特殊手段实现外部访问:

    # print(current_file.__filename)
    print(current_file._AudioFil__filename)

_ClassName__attributename

总之,这种私有化的手段“防君子不防小人”,更何况这并非是真的私有化——伪私有化。有一个更加准确的概念来描述这种机制:变量名压缩。

2,变量名压缩

Python 支持变量名压缩(mangling,起到扩展作用)的概念——让类内某些变量局部化。

压缩后的变量名通常会被误认为是私有属性,但这其实只是一种把类所创建的变量名局部化的方式而已:名称压缩并无法阻止类外代码对它的读取。

这种机制主要是为了避免实例内的命名空间的冲突,而不是限制变量名的访问。因此,压缩过的变量名最好称为“伪私有”,而不是“私有”。

类内部以 _ 或 __ 开头进行命名的操作只是一个非正式的惯例,目的是让程序员知道这是一个不应该修改的名字(它对Python自身来说没有什么意义)。

3,方法重载

python 内置的数据类型自动地支持有些运算操作,比如 + 运算、索引、切片等,它们都是通过对应对象的类的内部的以 __method-name__ 格式命名的方法来实现的。

方法重载可用于实现模拟内置类型的对象(例如,序列或像矩阵这样的数值对象),以及模拟代码中所预期的内置类型接口。

最常用的重载方法是__init__构造方法,几乎每个类都使用这个方法为实例属性进行初始化或执行其他的启动任务。

方法中特殊的self参数和__init__构造方法是 Python OOP的两个基石

举个例子:

class AudioFil:
    def __init__(self, filename):
        self.__filename = filename
    def __str__(self):
        return f"我是《{self.__filename}》"
    def play(self):
        print(f"playing {self.__filename}...")
    def stop(self):
        print(f"stop playing {self.__filename}...")

if __name__ == "__main__":
    file_name = "金刚葫芦娃.mp3"
    current_file = AudioFil(filename=file_name)
    print(current_file)		#>>> 我是《金刚葫芦娃.mp3》

(四)属性引用:getter、setter 与 property

一些语言使用私有属性的方式是通过 getter 与 setter 来实现内部属性的获取与设置。python 提供 property 类来达到同样的目的。举个例子:

class C:
    def __init__(self):
        self._x = None
    def getx(self) -> str:
        return self._x
    def setx(self, value):
        self._x = value
    def delx(self):
        del self._x
    x = property(getx, setx, delx, "I'm the 'x' property.")

if __name__ == '__main__':
    c = C()
    c.x = "ccc" # 调用setx
    print(c.x)  # 调用getx
    del c.x     # 调用delx

property的存在让对属性的获取、设置、删除操作自动内置化。

更加优雅的方式是使用@property装饰器。举个例子:

class C:
    def __init__(self):
        self._x = None
    @property
    def x(self):
        """I'm the 'x' property."""
        return self._x
    @x.setter
    def x(self, value):
        self._x = value
    @x.deleter
    def x(self):
        del self._x

if __name__ == '__main__':
    c = C()
    c.x = "ccc"
    print(c.x)
    del c.x

二,单一职责原则

(一)一个不满足单一职责原则的例子

现在需要处理一些音频文件,除了一些描述性的属性之外,还拥有播放、停止播放和信息存储这三项行为:

class AudioFile:
    def __init__(self, filename, author):
        self.__filename = filename
        self.__author = author
        self.__type = self.__filename.split(".")[-1]
    def __str__(self):
        return f"我是《{self.__filename}》"
    def play(self):
        print(f"playing {self.__filename}...")
    def stop(self):
        print(f"stop playing {self.__filename}...")
    def save(self, filename):
        content = {}
        for item in self.__dict__:
            key = item.split("__")[-1]
            value = self.__dict__[item]
            content[key] = value
        with open(filename+".txt", "a") as file:
            file.writelines(str(content)+'\n')

if __name__ == '__main__':
    file_name = "金刚葫芦娃.mp3"
    author_name = "姚礼忠、吴应炬"
    current_file = AudioFile(filename=file_name,author=author_name)
    current_file.save(filename="info_list")

这个类能够正常工作。

注意观察 save 方法,在保存文件信息之前,它做了一些格式化的工作。显然后面的工作是“临时添加”的且在别的文件类型中可能也会用到。
随着项目需求的变更或者其他原因,经常会在方法内部出现这种处理逻辑的扩散现象,即完成一个功能,需要新的功能作为前提保障。

从最简单的代码可重用性的角度来说,应该将方法内可重用的工作单独提出来:

至于公共功能放在哪个层次,请具体分析。

def info_format(obj):
    content = {}
    for item in obj.__dict__:
        key = item.split("__")[-1]
        value = obj.__dict__[item]
        content[key] = value
    return content
class AudioFile:
    ...
    def save(self, filename):
        content = info_format(self)
        with open(filename+".txt", "a") as file:
            file.writelines(str(content)+'\n')

但是,给改进后的代码在遇到功能变更时,任然需要花费大力气在原有基础上进行修改。比如需要提供信息搜索功能,就可能出现这种代码:

class AudioFile:
    ...
    def save(self, filename):
        ...
    def search(self, filename, key=None):
        ...

如果后期搜索条件发生变更、或者再新增功能,都会导致类内部出现功能扩散,将进一步增加原有代码的复杂性,可读性逐渐变差,尤其不利于维护与测试。

(二)单一职责原则

单一职责原则(Single-Responsibility Principle,SRP)由罗伯特·C.马丁于《敏捷软件开发:原则、模式和实践》一书中提出。这里的职责是指类发生变化的原因,单一职责原则规定一个类应该有且仅有一个引起它变化的原因,否则类应该被拆分。

该原则提出对象不应该承担太多职责,如果一个对象承担了太多的职责,至少存在以下两个缺点:

  • 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力;
  • 当客户端需要该对象的某一个职责时,不得不将其他不需要的职责全都包含进来,从而造成冗余代码或代码的浪费。

举个例子:一个编译和打印报告的模块。想象这样一个模块可以出于两个原因进行更改。

首先,报告的内容可能会发生变化。其次,报告的格式可能会发生变化。这两件事因不同的原因而变化。单一职责原则说问题的这两个方面实际上是两个独立的职责,因此应该在不同的类或模块中。

总之,单一职责原则认为将在不同时间因不同原因而改变的两件事情结合起来是一个糟糕的设计。

看一下修改后的代码:

class AudioFile:
    def __init__(self, filename, author):
        self.__filename = filename
        self.__author = author
        self.__type = self.__filename.split(".")[-1]
    def __str__(self):
        return f"我是《{self.__filename}》"
    def play(self):
        print(f"playing {self.__filename}...")
    def stop(self):
        print(f"stop playing {self.__filename}...")

class AudioFileDataPersistence:
    def save(self, obj, filename):
        ...
class AudioFileDataSearch:
    def search(self, key, filename):
        ...

if __name__ == '__main__':
    file_name = "金刚葫芦娃.mp3"
    author_name = "姚礼忠、吴应炬"
    current_file = AudioFile(filename=file_name, author=author_name)
    data_persistence = AudioFileDataPersistence()
    data_persistence.save(current_file, filename="info_list")
    data_search = AudioFileDataSearch()
    data_search.search(file_name, filename="info_list")

但这样将拆分代码,是不是合理的选择?

三,封装与单一职责原则

从封装的角度看来说,它的目的就是在对外提供接口的同时,提高代码的内聚性和可重用性,但功能大而全的封装更加的不安全。

单一职责原则通过拆分代码实现更低的耦合性和更高的可重用性,但过度拆分会增加对象间交互的复杂性。

关于两这的结合,有一些问题需要事先注意:

  • 需求的粒度是多大?
  • 维护的成本有多高?

作为面向对象编程的基础概念与实践原则,二者实际上是因果关系——如果一个类是有凝聚力的,如果有一个更高层次的目的,如果它的职责符合它的名字,那么 SRP 就会自然而然地出现。SRP 只是代码优化后的实际的结果,它本身并不是一个目标。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注我们的更多内容!

(0)

相关推荐

  • Python面向对象编程之类的引用

    目录 1.引用的概念 2.对象的拷贝 2.1 实例方法的引用 2.2 类的特性装饰器 3.类的名称修饰 3.1 _单下划线开头的名称修饰 3.2 _单下划线结尾的名称修饰 3.3 __双下划线开头的名称修饰 3.4 __name__双下划线开头和结尾的名称修饰 3.5 单下划线 4.Python的最小空类 1.引用的概念 引用 (Reference)是对象的指针 引用是内存中真实对象的指针,表示为变量名或者内存地址 每个对象存在至少一个引用,id()函数用于获得引用 在传递参数和赋值时,Pyth

  • Python面向对象编程之类的封装

    目录 1.封装的理解 2.私有类属性.公开类属性.私有实例属性和公开实例属性 2.1 公开类属性 2.2 私有类属性 2.3 公开实例属性 2.4 私有实例属性 2.5 私有属性不一定真的私有 3.私有方法和公开方法 4.类的保留属性 5.类的保留方法 1.封装的理解 封装(Encapsulation):属性和方法的抽象 属性的抽象:对类的属性(变量)进行定义.隔离和保护 分为私有属性和公开属性: 私有属性:只能在类内部访问 公开属性:可以通过类.对象名访问 可以选择公开或隐藏属性,隐藏属性的内

  • Python面向对象编程之类的继承

    目录 1.对继承的理解 2.类继承的构建 3.Python中最基础的类 4.ython类的重载 4.1 属性重载 4.2 方法重载 5.类的多继承 1.对继承的理解 继承(Inheritance) :代码复用的高级抽象 继承是面向对象设计的精髓之一 实现了以类为单位的高级抽象级别代码复用 继承是新定义类能够几乎完全使用原有类属性与方法的过程 不管是基类还是派生类,只是一种继承说法,这都是普通的Python类 也可以按子类.父类和超类划分. 最基础的类是基类,经过一次继承得出派生类,还可以再一次继

  • Python编程应用设计原则详解

    目录 1.单一职责原则 SRP 2.开闭原则 OCP 3.里氏替换原则 (LSP) 4.接口隔离原则 (ISP) 5.依赖反转原则 (DIP) 最后的话 写出能用的代码很简单,写出好用的代码很难. 好用的代码,也都会遵循一此原则,这就是设计原则,它们分别是: 单一职责原则 (SRP) 开闭原则 (OCP) 里氏替换原则 (LSP) 接口隔离原则 (ISP) 依赖倒置原则 (DIP) 提取这五种原则的首字母缩写词,就是 SOLID 原则.下面分别进行介绍,并展示如何在 Python 中应用. 1.

  • Python面向对象编程之类的概念

    目录 1.面向对象基本概念 1.1 万物皆对象 1.2 面向对象编程 1.3 面向对象的特征 2.Python面向对象的术语 3.Python类的构建 3.1 类的基本构建 3.2 类的构造函数 3.3 类的属性 3.4 类的方法 1.面向对象基本概念 1.1 万物皆对象 Python语言的中所有数据类型都是对象.函数是对象.模块是对象 Python所有类都是继承最基础的类object Python语言中的数据类型的操作功能都是类方法的体现 1.2 面向对象编程 面向对象编程又叫OOP(Obje

  • 总结的几个Python函数方法设计原则

    在任何编程语言中,函数的应用主要出于以下两种情况: 1.代码块重复,这时候必须考虑用到函数,降低程序的冗余度 2.代码块复杂,这时候可以考虑用到函数,增强程序的可读性 当流程足够繁杂时,就要考虑函数,及如何将函数组合在一起.在Python中做函数设计,主要考虑到函数大小.聚合性.耦合性三个方面,这三者应该归结于规划与设计的范畴.高内聚.低耦合则是任何语言函数设计的总体原则. 1.如何将任务分解成更有针对性的函数从而导致了聚合性 2.如何设计函数间的通信则又涉及到耦合性 3.如何设计函数的大小用以

  • Python面向对象编程之类的运算

    目录 1.运算概念的理解 2.运算符的重载 2.1 算术运算符 2.2 比较运算符 2.3 成员运算 2.4 其他运算 3.Python类的多态 1.运算概念的理解 运算(Operation)是操作逻辑的抽象 运算体现一种操作逻辑,在广义角度来说任何程序都是一种运算 Python解释器通过保留方法预留了一批运算的接口,需要重载 保留方法一般对应运算符,Python中运算体现为运算符的重载 运算本质上体现了交互关系.包含关系和常规的操作关系 运算重载的限制 不能重载Python语言内置类型的运算符

  • PHP面向对象五大原则之单一职责原则(SRP)详解

    本文实例讲述了PHP面向对象五大原则之单一职责原则(SRP).分享给大家供大家参考,具体如下: 单一职责原则(Single Pesponsibility Principle, SRP) 单一职责有两个含义: 一个是避免相同的职责分散到不同的类中, 别一个是避免一个类承担太多职责 为什么要遵守SRP呢? (1)可以减少类之间的耦合 如果减少类之间的耦合,当需求变化时,只修改一个类,从而也就隔离了变化:如果一个类有多个不同职责,它们耦合在一起,当一个职责发生变化时,可能会影响到其他职责. (2)提高

  • C#实现六大设计原则之单一职责原则

    单一职责(SRP)定义: 不要存在多于一个导致类变更的原因,通俗的说,即一个类只负责一项职责. 问题由来: 类T负责两个不同的职责:职责P1,职责P2.当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障. 解决方案: 遵循单一职责原则.分别建立两个类T1.T2,使T1完成职责P1功能,T2完成职责P2功能.这样,当修改类T1时,不会使职责P2发生故障风险:同理,当修改T2时,也不会使职责P1发生故障风险. ps: 说到单一职责原则,很多人都会不屑一顾.因为

  • Java设计模式七大原则之单一职责原则详解

    目录 定义 案例 需求 方案一 方案二 对比分析 总结 如何遵守单一职责原则 定义 单一职责原则(Single Responsibility Principle, SRP),有且仅有一个原因引起类的变更.简单来说,就是针对一个java类,它应该只负责一项职责.例如一个Test.java类,它有两个职责:职责1,职责2.当职责1进行修改时,有可能影响到职责2,所以需要将Test.java类拆分成Test1.java和Test2.java两个单一职责的类. 案例 需求 有一个交通工具类,里面定义一个

  • python面向对象编程设计原则之单一职责原则详解

    目录 一,封装 (一)什么是封装 (二)封装与访问 (三)私有化与访问控制 1,属性与方法的私有化 2,变量名压缩 3,方法重载 (四)属性引用:getter.setter与property 二,单一职责原则 (一)一个不满足单一职责原则的例子 (二)单一职责原则 三,封装与单一职责原则 总结 一,封装 封装是面向对象编程思想的重要特征之一. (一)什么是封装 封装是一个抽象对象的过程,它容纳了对象的属性和行为实现细节,并以此对外提供公共访问. 这样做有几个好处: 分离使用与实现.可直接使用公共

  • C#面向对象设计原则之单一职责原则

    单一职责原则(SRP) 定义:系统中的每一个类都应该只有一个职责. 好处:高内聚.低耦合. 解释说明: 单一职责也就是说我们应该让一个类或一个对象只做一件事情,每个类所要关注的就是自己要完成的职责是什么,能够引起这个类变化的原因也应该只有一个,这也是后面提到的所有的设计模式都会遵守的一个原则. 高内聚:先按照面向对象的封装特性来理解,封装也就是我们说的,应该把一个类或对象它所有相关的属性.方法.行为放到一起,放到一个类中,这样就实现了一个封装的特性.那么内聚,就是一个类里面应该包含它所有的属性和

  • JS面向对象编程实现的拖拽功能案例详解

    本文实例讲述了JS面向对象编程实现的拖拽功能.分享给大家供大家参考,具体如下: 原始的面向过程代码: <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style> #box { width: 100px; height: 100px; background: blue; position: absolute; } </style> <title>

  • Python函数式编程指南(四):生成器详解

    4. 生成器(generator) 4.1. 生成器简介 首先请确信,生成器就是一种迭代器.生成器拥有next方法并且行为与迭代器完全相同,这意味着生成器也可以用于Python的for循环中.另外,对于生成器的特殊语法支持使得编写一个生成器比自定义一个常规的迭代器要简单不少,所以生成器也是最常用到的特性之一. 从Python 2.5开始,[PEP 342:通过增强生成器实现协同程序]的实现为生成器加入了更多的特性,这意味着生成器还可以完成更多的工作.这部分我们会在稍后的部分介绍. 4.2. 生成

  • Python函数式编程指南(三):迭代器详解

    3. 迭代器 3.1. 迭代器(Iterator)概述 迭代器是访问集合内元素的一种方式.迭代器对象从集合的第一个元素开始访问,直到所有的元素都被访问一遍后结束. 迭代器不能回退,只能往前进行迭代.这并不是什么很大的缺点,因为人们几乎不需要在迭代途中进行回退操作. 迭代器也不是线程安全的,在多线程环境中对可变集合使用迭代器是一个危险的操作.但如果小心谨慎,或者干脆贯彻函数式思想坚持使用不可变的集合,那这也不是什么大问题. 对于原生支持随机访问的数据结构(如tuple.list),迭代器和经典fo

  • Java面向对象编程中final关键字的使用方法详解

    在Java中通过final关键字来声明对象具有不变性(immutable),这里的对象包括变量,方法,类,与C++中的const关键字效果类似. immutable指对象在创建之后,状态无法被改变 可以从三个角度考虑使用final关键字: 代码本身:不希望final描述的对象所表现的含义被改变 安全:final对象具有只读属性,是线程安全的 效率:无法修改final对象本身,对其引用的操作更为高效 final 变量 定义final Object a,则a只能被初始化一次,一旦初始化,a的数据无法

  • JS面向对象编程——ES6 中class的继承用法详解

    本文实例讲述了 ES6 中class的继承用法.分享给大家供大家参考,具体如下: JS是一种基于对象的语言,要实现面向对象,写法跟传统的面向对象有很大的差异.ES6引入了Class语法糖,使得JS的继承更像面向对象语言的写法. 此篇博客,分为:基本介绍.Vue使用案例 基本介绍 Class可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多: class Father { } class Son extends Father { } 代码定义了一个Son 类

随机推荐