SOLID:面向对象的设计原则
SOLID 是面向对象编程中类设计的助记词首字母缩写词。这些原则制定了有助于培养良好编程习惯和可维护代码的实践。
从长远来看,通过考虑代码维护和可扩展性,SOLID 原则丰富了敏捷代码开发环境。考虑和优化代码依赖关系有助于创建更直接、更有条理的软件开发生命周期。
什么是 SOLID 原则?
SOLID 代表了一组设计类的原则。 Robert C. Martin(鲍勃叔叔)介绍了大多数设计原则并创造了首字母缩略词。
SOLID 代表:
- 单一职责原则
- 开闭原则
- 里氏替换原则
- 接口隔离原则
- 依赖倒置原则
SOLID 原则代表了软件设计最佳实践的集合。每个想法都代表一个设计框架,从而带来更好的编程习惯、改进的代码设计和更少的错误。
SOLID:解释 5 条原则h2>
了解 SOLID 原则如何工作的最佳方式是通过示例。所有原则都是互补的,适用于个别用例。原则的应用顺序并不重要,并非所有原则都适用于所有情况。
下面的每个部分都概述了 Python 编程语言中的每个 SOLID 原则。 SOLID 的一般思想适用于任何面向对象的语言,例如 PHP、Java 或 C#。概括规则使其适用于现代编程方法,例如微服务。
单一职责原则 (SRP)
单一职责原则 (SRP) 规定:“改变班级的理由不应该不止一个。”
当改变一个类时,我们应该只改变一个功能,这意味着每个对象应该只有一个工作。
例如,看下面的类:
# A class with multiple responsibilities
class Animal:
# Property constructor
def __init__(self, name):
self.name = name
# Property representation
def __repr__(self):
return f'Animal(name="{self.name}")'
# Database management
def save(animal):
print(f'Saved {animal} to the database')
if __name__ == '__main__':
# Property instantiation
a = Animal('Cat')
# Saving property to a database
Animal.save(a)
对 save()
进行任何更改时 方法,更改发生在 Animal
班级。进行属性更改时,修改也发生在 Animal
类。
类有两个原因改变和违反单一责任原则。即使代码按预期工作,但不遵守设计原则会使代码从长远来看更难管理。
要实现单一职责原则,请注意示例类有两个不同的工作:
- 属性管理(构造函数和
get_name()
)。 - 数据库管理
(save()
)。
因此,解决该问题的最佳方法是将数据库管理方法分离到一个新类中。例如:
# A class responsible for property management
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'Animal(name="{self.name}")'
# A class responsible for database management
class AnimalDB:
def save(self, animal):
print(f'Saved {animal} to the database')
if __name__ == '__main__':
# Property instantiation
a = Animal('Cat')
# Database instantiation
db = AnimalDB()
# Saving property to a database
db.save(a)
更改 AnimalDB
类不影响 Animal
应用单一职责原则的类。代码直观易修改。
开闭原则(OCP)
开闭原则 (OCP) 规定:“软件实体应该对扩展开放,对修改关闭。”
向系统添加功能和用例不需要修改现有实体。措辞似乎自相矛盾——添加新功能需要更改现有代码。
通过下面的例子,这个想法很容易理解:
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'Animal(name="{self.name}")'
class Storage:
def save_to_db(self, animal):
print(f'Saved {animal} to the database')
Storage
类保存来自 Animal
的信息 实例到数据库。添加新功能(例如保存到 CSV 文件)需要将代码添加到 Storage
类:
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'Animal(name="{self.name}")'
class Storage:
def save_to_db(self, animal):
print(f'Saved {animal} to the database')
def save_to_csv(self,animal):
printf(f’Saved {animal} to the CSV file’)
save_to_csv
方法修改现有的 Storage
类来添加功能。这种做法违反了开闭原则,在新功能出现时更改现有元素。
该代码需要删除通用 Storage
类并创建用于以特定文件格式存储的单独类。
以下代码演示了开闭原则的应用:
class DB():
def save(self, animal):
print(f'Saved {animal} to the database')
class CSV():
def save(self, animal):
print(f'Saved {animal} to a CSV file')
代码遵循开闭原则。完整的代码现在看起来像这样:
class Animal:
def __init__(self, name):
self.name = name
def __repr__(self):
return f'"{self.name}"'
class DB():
def save(self, animal):
print(f'Saved {animal} to the database')
class CSV():
def save(self, animal):
print(f'Saved {animal} to a CSV file')
if __name__ == '__main__':
a = Animal('Cat')
db = DB()
csv = CSV()
db.save(a)
csv.save(a)
扩展附加功能(例如保存到 XML 文件)不会修改现有类。
里氏替换原则 (LSP)
Liskov 替换原则 (LSP) 规定:“使用指向基类的指针或引用的函数必须能够在不知情的情况下使用派生类的对象。”
该原则指出,父类可以替换子类,而功能不会发生任何明显变化。
查看下面的文件写入示例:
# Parent class
class FileHandling():
def write_db(self):
return f'Handling DB'
def write_csv(self):
return f'Handling CSV'
# Child classes
class WriteDB(FileHandling):
def write_db(self):
return f'Writing to a DB'
def write_csv(self):
return f"Error: Can't write to CSV, wrong file type."
class WriteCSV(FileHandling):
def write_csv(self):
return f'Writing to a CSV file'
def write_db(self):
return f"Error: Can't write to DB, wrong file type."
if __name__ == "__main__":
# Parent class instantiation and function calls
db = FileHandling()
csv = FileHandling()
print(db.write_db())
print(db.write_csv())
# Children classes instantiations and function calls
db = WriteDB()
csv = WriteCSV()
print(db.write_db())
print(db.write_csv())
print(csv.write_db())
print(csv.write_csv())
父类(FileHandling
) 包含两种用于写入数据库和 CSV 文件的方法。该类处理这两个函数并返回一条消息。
两个子类(WriteDB
和 WriteCSV
) 从父类继承属性 (FileHandling
)。但是,两个孩子在尝试使用不合适的 write 函数时都会抛出错误,这违反了 Liskov Substitution 原则,因为覆盖函数与父函数不对应。
以下代码解决了问题:
# Parent class
class FileHandling():
def write(self):
return f'Handling file'
# Child classes
class WriteDB(FileHandling):
def write(self):
return f'Writing to a DB'
class WriteCSV(FileHandling):
def write(self):
return f'Writing to a CSV file'
if __name__ == "__main__":
# Parent class instantiation and function calls
db = FileHandling()
csv = FileHandling()
print(db.write())
print(csv.write())
# Children classes instantiations and function calls
db = WriteDB()
csv = WriteCSV()
print(db.write())
print(csv.write())
子类正确对应父函数。
接口隔离原则(ISP)
接口隔离原则 (ISP) 指出:“许多特定于客户端的接口优于一个通用接口。”
换句话说,更广泛的交互界面被分割成更小的交互界面。该原则确保类只使用他们需要的方法,减少整体冗余。
下面的例子演示了一个通用接口:
class Animal():
def walk(self):
pass
def swim(self):
pass
class Cat(Animal):
def walk(self):
print("Struts")
def fly(self):
raise Exception("Cats don't swim")
class Duck(Animal):
def walk(self):
print("Waddles")
def swim(self):
print("Floats")
子类继承自父 Animal
类,其中包含 walk
和 fly
方法。虽然这两种功能对某些动物来说都是可以接受的,但有些动物具有多余的功能。
要处理这种情况,请将界面拆分为更小的部分。例如:
class Walk():
def walk(self):
pass
class Swim(Walk):
def swim(self):
pass
class Cat(Walk):
def walk(self):
print("Struts")
class Duck(Swim):
def walk(self):
print("Waddles")
def swim(self):
print("Floats")
Fly
类继承自 Walk
,为适当的子类提供附加功能。该示例满足接口隔离原则。
添加另一种动物,例如鱼,需要进一步雾化界面,因为鱼不能行走。
依赖倒置原则(DIP)
依赖倒置原则指出:“依赖于抽象,而不是具体。”
该原理旨在通过添加抽象层来减少类之间的连接。将依赖项移至抽象使代码更加健壮。
下面的例子演示了没有抽象层的类依赖:
class LatinConverter:
def latin(self, name):
print(f'{name} = "Felis catus"')
return "Felis catus"
class Converter:
def start(self):
converter = LatinConverter()
converter.latin('Cat')
if __name__ == '__main__':
converter = Converter()
converter.start()
该示例有两个类:
LatinConverter
使用虚构的 API 获取动物的拉丁名称(硬编码“Felis catus
” 为简单起见)。Converter
是一个使用LatinConverter
实例的高级模块 及其转换提供的名称的功能。Converter
严重依赖LatinConverter
类,这取决于 API。这种做法违反了原则。
依赖倒置原则需要在两个类之间增加一个抽象接口层。
示例解决方案如下所示:
from abc import ABC
class NameConverter(ABC):
def convert(self,name):
pass
class LatinConverter(NameConverter):
def convert(self, name):
print('Converting using Latin API')
print(f'{name} = "Felis catus"')
return "Felis catus"
class Converter:
def __init__(self, converter: NameConverter):
self.converter = converter
def start(self):
self.converter.convert('Cat')
if __name__ == '__main__':
latin = LatinConverter()
converter = Converter(latin)
converter.start()
Converter
类现在依赖于 NameConverter
接口而不是 LatinConverter
直接地。未来的更新允许通过 NameConverter
使用不同的语言和 API 定义名称转换 界面。
为什么需要 SOLID 原则?
SOLID 原则有助于解决设计模式问题。 SOLID 原则的总体目标是减少代码依赖,添加新功能或更改部分代码不会破坏整个构建。
由于将 SOLID 原则应用于面向对象的设计,代码变得更易于理解、管理、维护和更改。由于这些规则更适合大型项目,因此应用 SOLID 原则可以提高整个开发生命周期的速度和效率。
SOLID 原则仍然相关吗?
尽管 SOLID 原则已有 20 多年的历史,但它们仍然为软件架构设计提供了良好的基础。 SOLID 提供了适用于现代程序和环境的合理设计原则,而不仅仅是面向对象的编程。
SOLID 原则适用于代码由人编写和修改、组织成模块并包含内部或外部元素的情况。
结论
SOLID 原则有助于为软件架构设计提供良好的框架和指南。本指南中的示例表明,即使是像 Python 这样的动态类型语言也可以从将这些原则应用于代码设计中受益。
接下来,阅读 9 条 DevOps 原则,这将帮助您的团队充分利用 DevOps。
云计算