python面向对象编程设计原则之单一职责原则详解
作者:dangfulin
一,封装
封装是面向对象编程思想的重要特征之一。
(一)什么是封装
封装是一个抽象对象的过程,它容纳了对象的属性和行为实现细节,并以此对外提供公共访问。
这样做有几个好处:
- 分离使用与实现。可直接使用公共接口,但不需要考虑它内部具体怎么实现。
- 拥有内部状态隐藏机制,可实现信息/状态隐藏。
(二)封装与访问
就面向对象编程来说,类就是实现对象抽象的手段,封装的实现,就是将对象的属性与行为抽象为类中属性与方法。
举个例子:
对象 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 中明确地有 public
、private
等关键字提供对内部属性与方法的访问限制,但 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 只是代码优化后的实际的结果,它本身并不是一个目标。
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注脚本之家的更多内容!