软件设计原则

软件设计原则软件设计原则 软件设计原则是指在编写程序时可引导程序员遵循的一些原则和准则 若程序员能够遵循这些准则 在组织代码 保证代码质量等方面会更有信心 代码也会更易于维护 升级和扩展 以下是五个常见的软件设计原则 1 单一职责原则 Single Responsibili Principle SRP 定义

大家好,我是讯享网,很高兴认识大家。

软件设计原则

软件设计原则是指在编写程序时可引导程序员遵循的一些原则和准则。若程序员能够遵循这些准则,在组织代码、保证代码质量等方面会更有信心,代码也会更易于维护、升级和扩展。以下是五个常见的软件设计原则:

1. 单一职责原则 (Single Responsibility Principle, SRP)

定义:一个类应该只有一个引起它变化的原因。

含义:即每个类只负责完成一个功能或者任务,不包罗万象,这样可以使得这个类各自独立,内部高内聚,彼此之间低耦合,方便拓展和复用。

2. 开闭原则 (Open/Closed Principle, OCP)

定义:软件实体应当对扩展开放,对修改关闭。

含义:即对于新加入的需求,我们不去更改原有的代码,而是通过采用增加新的代码或者新的类来进行拓展。保证原有类的稳定性和复用性。

3. 里氏替换原则 (Liskov Substitution Principle, LSP)

定义:所有引用基类(父类)的地方必须能透明地使用其子类的对象。

含义:即所有使用基类的地方都能够快乐(无痛)地接受子类的实例作为基类对象,而且保证使用后不会对原有代码造成任何问题或改变。

4. 接口隔离原则 (Interface Segregation Principle, ISP)

定义:客户端不应该强制依赖它不需要的接口。

含义:即尽量将接口拆分成更小更具体的接口,让客户端只需关心自己需要的接口,避免出现无用接口污染的情况。同时,还要注意接口的灵活性和可扩展性,方便后期拓展与维护。

5. 依赖倒转原则 (Dependency Inversion Principle, DIP)

定义:高层模块不应该依赖底层模块,二者都应该依赖其抽象;抽象不应该依赖细节;细节应该依赖抽象。

含义:即要尽量减少类之间的依赖关系,使得系统更加稳定,同时采用高层模块调低层模块的方式进行设计,通过抽象进行通信,达到解耦的目的。

详细解说:

SRP 单一职责原则(Single Responsibility Principle)

指一个模块或类在设计时只负责一项职责,也就是说,它要么仅仅处理一种类型的请求,要么维护一个单一数据结构。

SRP 的主要优点是降低了单个模块或类的复杂度,使其更易于理解、测试和维护。如果一个模块依赖于多个职责,那它变得更加复杂,难以理解和测试。同时,当我们在修改这个模块时,需要更多的关注点,并且很容易引入潜在的错误。

下面是一个示例:

假设我们有一个处理邮件发送的 Mailer 类:

class Mailer: def __init__(self, smtp_server): self.smtp_server = smtp_server def send_email(self, from_addr, to_addrs, subject, body): # 与 SMTP 服务器建立连接 with SMTP(self.smtp_server) as smtp: message = 'From: {}\nTo: {}\nSubject: {}\n\n{}'.format( from_addr, ', '.join(to_addrs), subject, body ) # 发送邮件 smtp.sendmail(from_addr, to_addrs, message) 

讯享网

这个类封装了向给定的电子邮件地址发送电子邮件的功能。但是,在实践中,几乎不可能将所有的邮件发送操作都放在一个模块或类中。

为了符合 SRP 原则,我们可能会将发送 email 的操作从 Mailer 类中分离出来并创建一个新的类:

讯享网class Email: def __init__(self, from_addr, to_addrs, subject, body): self.from_addr = from_addr self.to_addrs = to_addrs self.subject = subject self.body = body class SmtpSender: def __init__(self, smtp_server): self.smtp_server = smtp_server def send(self, email): with SMTP(self.smtp_server) as smtp: message = 'From: {}\nTo: {}\nSubject: {}\n\n{}'.format( email.from_addr, ', '.join(email.to_addrs), email.subject, email.body ) smtp.sendmail(email.from_addr, email.to_addrs, message) 

上面的示例中,我们创建了一个 Email 类来封装电子邮件的相关信息,并创建了一个发送类 SmtpSender 来处理电子邮件的物理发送。

此外,经常在实践中发现,我们只需要封装部分与主题有关的功能,如邮件地址验证、邮件内容格式化等。这些可重复使用的代码段可以被提取到一个专门的模块或类中,并通过接口暴露给其他使用者。

总之,当我们设计一个函数、方法或类时,应该聚焦于单独的任务,使其更简洁和可维护。

开闭原则(Open/Closed Principle, OCP)

是面向对象设计中一个非常重要的软件设计原则。它由Bertrand Meyer提出,其定义如下:软件实体(类、模块、函数等)应该对扩展开放,对修改关闭。

换句话说,当需要改变一个程序的功能或者增加新的功能时,我们应该尽量通过扩展来实现变化,而不是通过修改原有代码来实现。

下面给出一个示例来解释开闭原则:

假设我们现在有一个类Person,包含了每个人的姓名和职业,其中职业有三种类型:EngineerDoctorTeacher

class Person: def __init__(self, name, position): self.name = name self.position = position def get_name(self): return self.name def get_position(self): return self.position 

现在如果我们想要新增一种职业,比如律师,那么按照开闭原则,我们应该考虑通过扩展来实现这个需求。

具体的实现方式是,定义一个新的类Lawyer,继承自Person,并且在Lawyer中增加律师独有的属性和方法。


讯享网

讯享网class Lawyer(Person): def __init__(self, name): super().__init__(name, "lawyer") def argue(self): print("I am arguing in the court.") 

在新的类中,我们不仅仅增加了律师独有的属性position,还新增了一个方法argue()。这样,在新增功能时,我们并没有修改原有的程序代码,而是通过扩展来实现这个功能。

综上所述,开闭原则可以使我们的代码更加灵活和可扩展,符合面向对象编程的核心思想。

接口隔离原则 (Interface Segregation Principle, ISP)

是指“客户端不应该依赖它不需要的接口”。“ISP”主要解决接口设计问题,其核心思想是:定义粒度小的接口,不要建立庞大臃肿的接口。

在实际编程中,当一个接口太大时,我们需要将它分割成一些更细粒度的小接口,使用接口隔离原则可以保证系统的灵活性和可维护性,同时也会相应的带来结构上的优势,例如:

  • 通过避免包含无用的方法,实现了代码的高内聚低耦合。
  • 更好的利用多态特性,提高代码的复用率,降低了对具体实现类的依赖。

接口隔离原则的实现方法从两个方面考虑:

  1. 类间接口的设计:

在接口设计时,应该保证接口的功能单一,尽量小而精,而且要有针对性。如果接口过于冗杂,拆分开来后再根据业务场景重组,可以使接口与业务场景高度契合,让实现这些接口的类只代表客户感兴趣的行为。

  1. 接口的使用:

使用接口的地方尽量不要强依赖整个接口列表,而是采用或组合的形式去使用接口,降低了客户使用接口的难度。

以下示例可以更好地说明接口隔离原则的应用:

假设我们有一个图形计算器,其中支持正方形、长方形和圆形三种形状的计算功能。那么我们首先需要设计一个形状接口:

public interface Shape { 
    double area(); // 计算面积 } 

然后,分别实现三个具体的形状类:

讯享网/ * 四边形-正方形 */ public class Square implements Shape { 
    public double area() { 
    // 计算区域 } } / * 长方形 */ public class Rectangle implements Shape { 
    public double area() { 
    // 计算区域 } } / * 圆形 */ public class Circle implements Shape { 
    public double area() { 
    // 计算区域 } } 

这个设计看起来很简单,但是存在问题:如果未来需要新增一些形状,如三角形等,则我们需要在 Shape 接口中新增方法,同时也要在所有具体类中实现新加入的方法,导致代码冗余且难以维护。所以我们需要应用 接口隔离原则进行改造:

  1. Shape 接口拆分为不同的子接口:
public interface Rectangle { 
    double area(); // 计算面积 } public interface Square { 
    double area(); // 计算面积 } public interface Circle { 
    double area(); // 计算面积 } 
  1. 根据业务场景动态组合各个接口:
讯享网public class GraphicEditor { 
    public void drawRectangle(Rectangle r) { 
    // 绘制矩形.. } public void drawSquare(Square s) { 
    // 绘制正方形... } public void drawCircle(Circle c) { 
    // 绘制圆形... } public static void main(String[] args) { 
    GraphicEditor graphicEditor = new GraphicEditor(); graphicEditor.drawRectangle(new SmallRectanble()); // 只需要小矩形的功能,使用SmallRectangle类来声明参数即可 graphicEditor.drawSquare(new BigSquare()); // 只需要大正方形的功能,使用BigSquare类来声明参数即可 graphicEditor.drawCircle(new MediumCircle()); // 只需要中等大小的圆形的功能,使用MediumCircle类来声明参数即可 } } 

这样一来,每个实现类都只需要关注自己需要支撑的接口即可,例如, SmallRectangle 只需要实现 Rectangle 接口,MediumCircle 只需要实现 Circle 接口。使用者则可以根据业务场景选择适合自己的接口来使用,系统的灵活性和复用性也随之提升。

里氏替换原则(Liskov Substitution Principle, LSP)

是 OO 面向对象编程中的基本原则之一,由 Barbara Liskov 在 1987 年提出。它是指如果用一个父类的对象来代替一个子类的对象,那么程序将不会产生任何错误和异常,而且原本子类所拥有的功能仍然可用。

简单来说,LSP 要求“任何时候,只要父类出现的地方,子类必定可以出现”,即子类可以完全替代父类并实现父类的所有功能,但是反过来却不行。这也就是著名的“里氏替换原则”。

理解 LSP 的重点在于“子类可以完全替代父类”。所以,在使用 LSP 进行对象设计的时候,我们需要注意以下几点:

  1. 子类不能修改父类已经实现的方法。
  2. 子类应该尽量保留父类的行为,不应删除父类的方法。
  3. 子类可以增加自己特有的方法,但是不能通过修改父类的方法来完成它们。
  4. 当子类的方法不能满足父类的需求时,可以通过抛出异常或者返回错误信息等方式进行说明。

下面给出一个示例来说明里氏替换原则:

假设有如下两个类:

public class Bird { 
    public void fly() { 
    System.out.println("I can fly."); } } public class Penguin extends Bird { 
    @Override public void fly() { 
    throw new UnsupportedOperationException(); } } 

上述代码中,Bird 类是一只鸟的基类,拥有 fly() 方法;而另一个类 Penguin 是一只企鹅,它继承了 Bird 类但是不会飞,因此在重写 fly() 方法时直接抛出了一个异常。

可以看到,这里的 Penguin 类继承了 Bird 类,但是其实现与 Bird 类并不完全一致。尽管最终程序依然能正确运行,但是这种方式违反了 LSP 原则,因为子类不能完全替代父类,也就是说,我们不能用一个 Penguin 对象来代替 Bird 对象。最好的做法是将 fly() 方法从 Bird 类中移除,并创建一个新的接口如 IFlyable,在需要飞翔的动物类中进行实现。

讯享网public interface IFlyable { 
    void fly(); } public class Bird implements IFlyable { 
    public void fly() { 
    System.out.println("I can fly."); } } public class Penguin implements IFlyable { 
    @Override public void fly() { 
    // 啥也不干,因为它不会飞 } } 

修改后的代码中,我们将 fly() 方法移到了一个新的接口 IFlyable 中,同时将其从 Bird 类中删除。这样一来,每个具有飞翔能力的动物都可以实现 IFlyable 接口,并在其中完成自己特定的行为,而对于那些不能飞翔的动物,则可以不做任何操作。通过这种方式,我们就保证了子类可以完全替代父类,并且尊重了 LSP 原则。

依赖倒转原则 (Dependency Inversion Principle, DIP)

是 SOLID 设计原则中的其中一条,它包括了两个核心思想:

  1. 高层模块不应该依赖于底层模块。两者都应该依赖于抽象。
  2. 抽象不应该依赖于细节。细节应该依赖于抽象。

在该原则中,高层模块和底层模块指的是软件代码中模块之间的依赖关系。高层模块是指更加抽象、一般化的模块,而底层模块是指更加具体化的、执行某些实际任务的模块。通过该原则,可以让系统达到灵活性和可扩展性的平衡。

举个例子,我们有如下代码:

class EventLogger: def log(self, event): print(f"Logging { 
     event}") class OrderProcessor: def __init__(self): self.logger = EventLogger() def process(self, order): # Process the order and log an event. self.logger.log("Order processed") 

这里 OrderProcessor 应该依赖于一个抽象的记录器,而非具体的 EventLogger 类。因此我们可以改写成以下方式:

讯享网from abc import ABC, abstractmethod class Logger(ABC): @abstractmethod def log(self, event): pass class EventLogger(Logger): def log(self, event): print(f"Logging { 
     event}") class OrderProcessor: def __init__(self, logger: Logger): self.logger = logger def process(self, order): # Process the order and log an event. self.logger.log("Order processed") 

现在,OrderProcessor 类不依赖于具体的 EventLogger 类,而是依赖于一个抽象的 Logger 接口。任何类都可以实现这个接口并替代原有的日志记录器。

通过实现该原则,系统可以更加灵活、健壮和可扩展,并且在需要进行模块重构时,也能够更加容易地进行修改。

小讯
上一篇 2025-02-22 10:46
下一篇 2025-02-13 13:53

相关推荐

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容,请联系我们,一经查实,本站将立刻删除。
如需转载请保留出处:https://51itzy.com/kjqy/121140.html