(文章目录)
前言
面向对象设计(OOD)是现代软件工程中的核心,其核心思想在于通过抽象化实体的特征和行为来模拟现实世界,这种方法不仅仅是一种编程范式,更是一种设计哲学。在编程领域,它帮助开发者通过类和对象的组织和交互,来构建出模块化、灵活且易于维护的软件系统。而面向对象设计的七大原则,常被称为“OOD七大宝典”,它们分别是单一职责原则(SRP)、开闭原则(OCP)、里氏替换原则(LSP)、依赖倒置原则(DIP)、接口隔离原则(ISP)、迪米特法则(LoD)和合成复用原则(CRP)。
一、单一职责原则(SRP)
单一职责原则(SRP)其核心观点是一个类应该仅有一个变化的原因。这个原则是由罗伯特·C·马丁提出,他在《敏捷软件开发:原则、模式与实践》一书中明确提到,单一职责原则的目标是实现类的高内聚与低耦合。高内聚指的是一个类或模块专注于完成一项任务或有一系列相关性很强的功能,而低耦合则意味着类与类之间的依赖关系简单明了,易于理解和维护。
1.定义与解释
SRP 告诉我们,不应该有多于一个导致类变化的原因,因为这通常意味着类承担了太多职责。当一个类有多个责任时,变化的可能性增加,因为有多个功能可能需要变化。这会导致软件的脆弱性增加,因为变更可能影响到与该类有依赖关系的其他部分。另外,这样的类很难进行测试和维护,因为改动一处可能需要在多个地方进行测试和调整。
对于面向对象设计而言,SRP 有着重要的意义。首先,它减轻了理解单个类的负担,因为每个类只负责一件事情。其次,它减少了代码的修改成本,当修改一个类时,不必担心会影响到类的其他功能。最后,它提升了代码的可复用性,因为职责单一的类更容易被其他部分的代码所复用。
2.示例
违反SRP的代码:
class User:
def __init__(self, name: str):
self.name = name
def get_user_data(self):
pass # Logic to fetch user data from a database
def save_user_data(self, user_data):
pass # Logic to save user data to a database
def generate_user_report(self):
pass # Logic to generate a report of the user's data
def send_user_report(self):
pass # Logic to send the user's report through email
在这个例子中,User
类负责了多个任务:从数据库中获取和保存用户数据、生成用户报告和发送报告。如果需要改变报告的格式或发送方式,就可能需要修改 User
类,这违反了 SRP。
遵循SRP的代码:
class User:
def __init__(self, name: str):
self.name = name
class UserDataBase:
@staticmethod
def get_user_data(user: User):
pass # Logic to fetch user data from a database
@staticmethod
def save_user_data(user: User, user_data):
pass # Logic to save user data to a database
class UserReport:
@staticmethod
def generate(user: User):
pass # Logic to generate a report of the user's data
class ReportMailer:
@staticmethod
def send(report):
pass # Logic to send the report through email
在遵循SRP的版本中,我们将不同的职责分配到了不同的类中。现在,如果报告的发送方式需要变更,我们只需修改 ReportMailer
类,而不会影响到其他类。这样每个类都只有一个变化的原因,从而遵循了单一职责原则。
二、开闭原则(OCP)
开闭原则(OCP)为软件的可维护性和可扩展性提供了指导。该原则由 Bertrand Meyer 提出,其核心思想是“软件实体应当对扩展开放,对修改封闭”,这意味着软件的行为应该可以扩展,而不需要修改现有代码。
1.定义与解释
开闭原则鼓励我们写出可以在不更改现有代码的情况下增加新功能的代码。这可以通过抽象化和使用接口或抽象类来实现,让系统的各个部分相互独立,从而能够适应变化。当需求变化或系统需要扩展新功能时,我们可以通过添加新的代码而不是修改旧代码来实现,这降低了系统可能因更改而产生的风险。
OCP对软件开发的影响深远,因为它促使开发者进行更精心的设计。它要求设计者考虑到代码未来的变化,这样,当新需求出现时,可以通过新增代码而不是修改旧代码来应对。这种做法不仅能够减少可能引入的错误,也使得软件系统更加稳定和灵活。
2.示例
考虑一个简单的报表生成系统,要求能够支持不同类型的报表格式。而不使用OCP,我们可能会得到以下设计:
class ReportGenerator:
def generate(self, data, report_type):
if report_type == "PDF":
# Logic to generate PDF report
pass
elif report_type == "HTML":
# Logic to generate HTML report
pass
# To add a new report format, we have to modify this class
这种设计违反了 OCP,因为每次添加新的报表格式时,我们都需要修改 ReportGenerator
类。按照 OCP 重构的设计应该是这样的:
class ReportGenerator:
def generate(self, data, report_formatter):
return report_formatter.format(data)
class ReportFormatter:
def format(self, data):
raise NotImplementedError("Subclasses should implement this!")
class PDFReportFormatter(ReportFormatter):
def format(self, data):
# Logic to generate PDF report
pass
class HTMLReportFormatter(ReportFormatter):
def format(self, data):
# Logic to generate HTML report
pass
# Now if we need to add a new format, we just add a new formatter.
# No need to change any existing class.
在这个遵循OCP的设计中,我们定义了一个 ReportFormatter
抽象基类,并为每种报表格式提供了具体的实现。ReportGenerator
类不需要知道具体的报表格式,它通过 ReportFormatter
的接口与报表格式进行交互。如果我们想要添加一个新的报表格式,我们只需添加一个新的 ReportFormatter
子类,无需修改 ReportGenerator
或其他任何已存在的格式器代码。这样,我们就实现了对扩展的开放和对修改的封闭。
三、里氏替换原则(LSP)
里氏替换原则(LSP)是由芭芭拉·里提出的一个面向对象的设计原则。它是一个关于继承和子类型的原则,要求子类对象能够替换其父类对象被使用,而不破坏程序的正确性。这意味着一个程序中的对象应当可以在不改变程序正确性的前提下,被它的子类所替换。
1.定义与解释
LSP原则的关键是确保子类可以完整地替代父类。一个使用基类对象的函数在使用子类对象替代后,不应当出现任何错误或异常。子类在继承和扩展父类的行为时必须保持一致性,不能改变父类原有的行为。
遵循LSP可以提高代码的模块化和可重用性,同时也使得类的新旧版本兼容。它强制性地规定了父类与子类之间行为的一致性,使得任何基于父类的代码都可以安全地使用子类对象,从而提高了代码的可维护性。
2.示例
违反LSP的代码:
class Rectangle:
def set_width(self, width):
self.width = width
def set_height(self, height):
self.height = height
def get_area(self):
return self.width * self.height
class Square(Rectangle):
def set_width(self, width):
self.width = width
self.height = width # This change breaks the LSP
def set_height(self, height):
self.width = height # This change breaks the LSP
self.height = height
def print_area(rect: Rectangle):
rect.set_width(4)
rect.set_height(5)
assert rect.get_area() == 20, "Area not correct."
my_square = Square()
print_area(my_square) # This will fail the assert
在这个例子中,Square
是从 Rectangle
继承的,但是通过重写 set_width
和 set_height
方法来保持两边相等,它违反了LSP。在 print_area
函数中使用 Square
代替 Rectangle
会导致断言失败,因为它期望长方形的面积,但 Square
改变了行为。
遵循LSP的代码:
class Shape:
def get_area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def get_area(self):
return self.width * self.height
class Square(Shape):
def __init__(self, side):
self.side = side
def get_area(self):
return self.side * self.side
def print_area(shape: Shape):
assert shape.get_area() == 20, "Area not correct."
my_rectangle = Rectangle(4, 5)
print_area(my_rectangle) # This will pass the assert
my_square = Square(4.472)
print_area(my_square) # This will also pass the assert
在遵循LSP的例子中,我们引入了一个共同的基类 Shape
,它定义了 get_area
方法的接口,而 Rectangle
和 Square
都各自实现了这个方法。这样,不管我们传给 print_area
函数的是 Rectangle
对象还是 Square
对象,它都会正确地计算面积,因为两个类都遵守了 Shape
接口的契约。
四、依赖倒置原则(DIP)
依赖倒置原则(DIP)是一个促进高度解耦和可维护代码设计的面向对象设计原则。这个原则的核心是要求代码依赖于抽象而不是具体实现,进而实现了高层模块与低层模块的相对独立。
1.定义与解释
DIP指出高层模块(比如策略决定者或业务规则)不应该依赖于低层模块(比如具体的数据访问实现或具体的服务实现),而应当两者都依赖于一套预先定义好的抽象接口或类。同样,抽象不应依赖于具体的实现细节,而是具体的实现细节应该依赖于抽象。
DIP对设计灵活性的提升意义重大,它允许开发者改变应用程序的行为而不需要改变高层模块。只要底层模块遵循同样的抽象,就可以自由地更换它们。这种灵活性使得应用程序更容易适应变化,易于扩展,且具有更好的可维护性。
2.示例
不遵循DIP的代码:
class LightBulb:
def turn_on(self):
print("LightBulb: turned on...")
def turn_off(self):
print("LightBulb: turned off...")
# 高层模块直接依赖于低层模块的具体实现。
class Switch:
def __init__(self):
self.bulb = LightBulb()
def operate(self):
self.bulb.turn_on() # Tight coupling here
# 如果我们决定更换LightBulb类,Switch类也必须修改。
在这个例子中,Switch
类(一个高层模块)直接依赖于 LightBulb
类(一个低层模块)。如果我们想要替换 LightBulb
类(例如,使用一个LED灯类),我们也必须修改 Switch
类。
遵循DIP的代码:
from abc import ABC, abstractmethod
class Switchable(ABC): # This is the abstraction
@abstractmethod
def turn_on(self):
pass
@abstractmethod
def turn_off(self):
pass
class LightBulb(Switchable): # Low-level module depends on abstraction
def turn_on(self):
print("LightBulb: turned on...")
def turn_off(self):
print("LightBulb: turned off...")
class Fan(Switchable): # Another low-level module depending on the same abstraction
def turn_on(self):
print("Fan: turned on...")
def turn_off(self):
print("Fan: turned off...")
class Switch: # High-level module also depends on the abstraction
def __init__(self, device: Switchable):
self.device = device
def operate(self):
self.device.turn_on() # Using the abstraction
# Now we can pass any class that implements Switchable without changing the Switch class.
my_switch = Switch(LightBulb())
my_switch.operate()
my_switch = Switch(Fan())
my_switch.operate()
在遵循DIP的版本中,我们定义了一个 Switchable
抽象基类,LightBulb
和 Fan
都实现了这个接口。Switch
类现在依赖于 Switchable
接口,而不是具体的 LightBulb
类,这使得我们可以不修改 Switch
类的情况下,轻松地替换 LightBulb
为任何实现了 Switchable
的类,比如 Fan
。这样,即使底层的实现细节变化了,高层模块也不需要做出任何改
五、接口隔离原则(ISP)
接口隔离原则(Interface Segregation Principle, ISP)是面向对象设计原则中的一个重要原则,它提倡在设计接口时应当小而专一,而不是大而全面。
1.定义与解释
ISP的核心思想是客户端不应该被迫依赖于它们不使用的接口。一个类对另一个类的依赖应该基于最小的接口。这意味着,如果一个接口变得太“肥大”,我们应该将它分割成更小且更具针对性的几个接口,这样客户端只需要知道它们真正使用的方法。这样做可以减少系统中不必要的依赖关系,从而降低变更引入的风险,使系统更容易维护和扩展。
2.示例
不遵循ISP的代码:
class Worker:
def work(self):
print("Working...")
def eat(self):
print("Eating...")
# 这里的Robot类被迫实现了它不需要的eat方法。
class Robot(Worker):
def work(self):
print("Robot working...")
def eat(self):
# Robots do not eat, but we have to implement the method
pass
# Human workers can use both methods.
class Human(Worker):
def work(self):
print("Human working...")
def eat(self):
print("Human eating...")
在上面的代码中,Robot
类被迫实现了 eat
方法,这是它不需要的,因为机器人不需要“吃”。
遵循ISP的代码:
from abc import ABC, abstractmethod
class Workable(ABC):
@abstractmethod
def work(self):
pass
class Eatable(ABC):
@abstractmethod
def eat(self):
pass
class Worker(Workable, Eatable):
def work(self):
print("Working...")
def eat(self):
print("Eating...")
class Robot(Workable):
def work(self):
print("Robot working...")
# Robots only need to implement Workable interface.
# Humans can implement both interfaces.
class Human(Worker):
def work(self):
print("Human working...")
def eat(self):
print("Human eating...")
在遵循ISP的版本中,我们创建了两个接口:Workable
和 Eatable
。Robot
类只实现了 Workable
接口,而 Human
类可以实现两个接口。这样,每个类只需要关心它真正需要的接口,从而避免了不必要的依赖。
六、迪米特法则(LoD)
迪米特法则(Law of Demeter, LoD),也称为最少知识原则,是一种用于降低软件实体之间耦合的设计指导原则。
1.定义与解释
迪米特法则建议,一个软件实体应当尽可能少地与其他实体发生相互作用。这意味着一个对象应该对其他对象保持最少的了解,并且只与直接的朋友通信。在这个原则下,每个单元对于其他单元只知道必须的信息,不需要了解那些不必要的内部细节。
遵循LoD原则的系统通常具有更好的维护性和更高的模块化程度,因为它减少了对象间的直接依赖。在面向对象编程中,迪米特法则可以通过只调用属于以下范畴的方法来应用:
- 当前对象本身的方法。
- 作为方法参数传入的对象的方法。
- 当前对象的任何成员对象的方法。
- 对象自身创建或实例化的任何对象的方法。
2.示例
不遵循LoD的代码:
class Paper:
def getDetails(self):
# returns details about the paper
return "This is a paper."
class Printer:
def printPaper(self, paper):
print(paper.getDetails())
class Copier:
def startCopy(self):
paper = Paper()
printer = Printer()
# Copier has knowledge of Paper's methods, which it shouldn't have.
print_details = paper.getDetails()
printer.printPaper(print_details)
copier = Copier()
copier.startCopy()
在上述代码中,Copier
类直接调用了 Paper
类的 getDetails
方法。这违反了LoD原则,因为 Copier
知道了 Paper
类的详细实现。
遵循LoD的代码:
class Paper:
def getDetails(self):
return "This is a paper."
class Printer:
def printPaper(self, paper):
# Printer is responsible for knowing how to print paper.
print(paper.getDetails())
class Copier:
def startCopy(self):
paper = Paper()
printer = Printer()
# Copier now only talks to Printer, does not need to know about Paper details.
printer.printPaper(paper)
copier = Copier()
copier.startCopy()
在遵循LoD的代码中,Copier
类不再直接调用 Paper
类的方法,而是通过 Printer
类来间接完成工作。Copier
不需要知道 Paper
的内部细节,这减少了类之间的依赖,降低了耦合度。
七、合成复用原则(CRP)
合成复用原则(Composition over Inheritance Principle, CRP)是面向对象设计的一项重要原则,它鼓励使用合成/聚合关系代替继承关系来达到代码复用的目的。
1.定义与解释
合成复用原则主张在设计软件时,应当尽量使用合成/聚合的方式,而非继承关系来实现代码的复用。继承虽然能提供一种直观的复用机制,但它也带来了紧密的耦合和泛化的问题,尤其是在层次较深的继承体系中,底层改动可能导致链式反应。相反,合成/聚合不仅提高了代码的复用性,还增加了代码的灵活性和可维护性。
合成(Composition)是指一个类中包含了另一个类的实例,从而能够调用其方法进行工作。聚合(Aggregation)类似于合成,但它允许单独的生命周期,即组成对象可以独立于创建它的对象存在。
2.示例
使用继承的代码:
class Vehicle:
def move(self):
print("Moving...")
class Car(Vehicle):
def move(self):
print("Car is moving.")
class Boat(Vehicle):
def move(self):
print("Boat is moving.")
# Car and Boat inherit move behavior from Vehicle, tightly coupling them.
上述代码通过继承来复用 move
方法,但这样 Car
和 Boat
类与 Vehicle
类紧密耦合。
使用合成复用的代码:
class Movement:
def move(self):
print("Moving...")
class Vehicle:
def __init__(self, movement):
self.movement = movement
def startMoving(self):
self.movement.move()
class Car:
# Car has-a Movement, not is-a Vehicle.
def __init__(self):
self.movement = Movement()
def move(self):
self.movement.move()
class Boat:
# Similarly, Boat has-a Movement.
def __init__(self):
self.movement = Movement()
def move(self):
self.movement.move()
# Vehicle composition allows for flexibility and reuse without tight coupling.
car = Car()
boat = Boat()
car.move() # Outputs: Moving...
boat.move() # Outputs: Moving...
在使用合成复用原则的代码中,Car
和 Boat
类都包含了 Movement
类的实例。这允许 Car
和 Boat
独立于 Movement
类变化,只要 Movement
的接口保持不变。此外,如果需要为特定类型的车辆添加特殊的移动方式,我们可以很容易地扩展 Movement
类或创建一个新的移动类来满足需求,而不需要改变现有的车辆类。这就是合成复用原则的强大之处,它提高了代码的灵活性,减少了因继承而产生的风险。
总结
遵循这些原则能够帮助避免常见的设计陷阱,比如过度耦合、不易维护的代码和难以扩展的系统等。尽管应用这些原则可能会增加初期的设计复杂性,但长远来看,它们为软件的可持续发展奠定了坚实的基础。正确应用这些原则,将使我们能够构建出可扩展、易于维护且高效的面向对象系统。
标签:代码,原则,self,七大,面向对象,print,class,def From: https://blog.51cto.com/u_16202095/9126168