深入理解设计模式的设计原则

1、如何理解单一职责原则(SRP)?

  • 单一职责原则(Single Responsibility Principle,简称SRP),它要求一个类或模块
    应该只负责一个特定的功能。这有助于降低类之间的耦合度,提高代码的可读性和可
    维护性。
  • 上边的概念中提到了类(class)和模块(module),关于这两个概念,在后边的学习中可以视作一样。
  • 我们可以把模块看作比类更加抽象的概念,类也可以看作模块。或者把模块看作比类更加粗粒度的代码块,模块中包含多个类,多个类组成一个模块。

1. 如何理解单一职责原则(SRP)?

一个类只负责完成一个职责或者功能。不要设计大而全的类,要设计粒度小、功能单一的类。单一职责原则是为了实现代码高内聚、低耦合,提高代码的复用性、可读性、可维护性。

2. 如何判断类的职责是否足够单一?

不同的应用场景、不同阶段的需求背景、不同的业务层面,对同一个类的职责是否单一,可能会有不同的判定结果。实际上,一些侧面的判断指标更具有指导意义和可执行性,比如,出现下面这些情况就有可能说明这类的设计不满足单一职责原则:

  • 类中的代码行数、函数或者属性过多;
  • 类依赖的其他类过多,或者依赖类的其他类过多;
  • 私有方法过多;
  • 比较难给类起一个合适的名字;
  • 类中大量的方法都是集中操作类中的某几个属性。

3. 类的职责是否设计得越单一越好?

  • 单一职责原则通过避免设计大而全的类,避免将不相关的功能耦合在一起,来提高类的内聚性。
  • 同时,类职责单一,类依赖的和被依赖的其他类也会变少,减少了代码的耦合性,以此来实现代码的高内聚、低耦合。
  • 但是,如果拆分得过细,实际上会适得其反,反倒会降低内聚性,也会影响代码的可维护性。

开闭原则

英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。我们把它翻译成中文就是:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”
说人话就是,当我们需要添加一个新的功能时,应该在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

展示一个简化的电商平台的订单折扣策略。你觉得下边的代码有问题吗?

class Order {
    private double totalAmount;
    public Order(double totalAmount) {
        this.totalAmount = totalAmount;
   }
    // 计算折扣后的金额

    public double getDiscountedAmount(String discountType) {
        double discountedAmount = totalAmount;
        if (discountType.equals("FESTIVAL")) {
            discountedAmount = totalAmount * 0.9; // 节日折扣,9折

       } else if (discountType.equals("SEASONAL")) {
            discountedAmount = totalAmount * 0.8; // 季节折扣,8折

       }
        return discountedAmount;
   }
}

上述代码中, Order 类包含一个计算折扣金额的方法,它根据不同的折扣类型应用
折扣。当我们需要添加新的折扣类型时,就不得不需要修改 getDiscountedAmount 方法的代码,这显然是不合理的,这就违反了开闭原则。

遵循开闭原则的代码:

// 抽象折扣策略接口

interface DiscountStrategy {
    double getDiscountedAmount(double totalAmount);
}

// 节日折扣策略
class FestivalDiscountStrategy implements DiscountStrategy {
    @Override
    public double getDiscountedAmount(double totalAmount) {
        return totalAmount * 0.9; // 9折

   }
}
// 季节折扣策略
class SeasonalDiscountStrategy implements DiscountStrategy {
    @Override

    public double getDiscountedAmount(double totalAmount) {
        return totalAmount * 0.8; // 8折

   }
}
class Order {
    private double totalAmount;
    private DiscountStrategy discountStrategy;
    public Order(double totalAmount, DiscountStrategy discountStrategy) {
        this.totalAmount = totalAmount;
        this.discountStrategy = discountStrategy;
   }

    public void setDiscountStrategy(DiscountStrategy discountStrategy) {
        this.discountStrategy = discountStrategy;
   }
    // 计算折扣后的金额
    public double getDiscountedAmount() {
        return discountStrategy.getDiscountedAmount(totalAmount);
   }
}

在遵循开闭原则的代码中,我们定义了一个抽象的折扣策略接口 DiscountStrategy ,然后为每种折扣类型创建了一个实现该接口的策略类。 Order 类使用组合的方式,包含一个 DiscountStrategy 类型的成员变量,以便在运行时设置或更改折扣策略,(可以通过编码,配置、依赖注入等形式)。这样,当我们需要添加新的折扣类型时,只需实现 DiscountStrategy 接口即可,而无需修改现有的 Order 代码。这个例子遵循了开闭原则。

2、修改代码就意味着违背开闭原则吗

开闭原则的核心思想是要尽量减少对现有代码的修改,以降低修改带来的风险和影响。在实际开发过程中,完全不修改代码是不现实的。当需求变更或者发现代码中的错误时,修改代码是正常的。然而,开闭原则鼓励我们通过设计更好的代码结构,使得在添加新功能或者扩展系统时,尽量减少对现有代码的修改。

如何做到“对扩展开放、修改关闭”

  1. 抽象与封装:通过定义接口或抽象类来封装变化的部分,将共性行为抽象出来。
    当需要添加新功能时,只需要实现接口或继承抽象类,而不需要修改现有代码。

  2. 组合/聚合:使用组合或聚合的方式,将多个不同功能模块组合在一起,形成一个更大的系统。当需要扩展功能时,只需要添加新的组件,而不需要修改现有的组件。

  3. 使用依赖注入:

  4. 使用设计模式:设计模式是针对某些特定问题的通用解决方案。很多设计模式都是为了支持“对扩展开放、修改关闭”的原则。例如,策略模式、工厂模式、装饰器模式等,都是为了实现这个原则。

  5. 使用事件和回调:通过事件驱动和回调函数,可以让系统在运行时根据需要动态
    地添加或修改功能,而无需修改现有代码。

  6. 使用插件机制:通过插件机制,可以允许第三方开发者为系统添加新功能,而无需修改系统的核心代码。这种机制常用于框架和大型软件系统中。

  7. 需要注意的是,遵循开闭原则并不意味着永远不能修改代码。在实际开发过程中,完全不修改代码是不现实的。开闭原则的目标是要尽量降低修改代码带来的风险和影响,提高代码的可维护性和可复用性。在实际开发中,我们应该根据项目需求和预期的变化来平衡遵循开闭原则的程度。

接口隔离原则

客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。

接口隔离原则(Interface Segregation Principle,ISP)是一种面向对象编程的设计原则,它要求我们将大的、臃肿的接口拆分成更小、更专注的接口,以确保类之间的解耦。这样,客户端只需要依赖它实际使用的接口,而不需要依赖那些无关的接口。接口隔离原则有以下几个要点:

  1. 将一个大的、通用的接口拆分成多个专用的接口。这样可以降低类之间的耦合
    度,提高代码的可维护性和可读性。

  2. 为每个接口定义一个独立的职责。这样可以确保接口的粒度适当,同时也有助于
    遵循单一职责原则。

  3. 在定义接口时,要考虑到客户端的实际需求。客户端不应该被迫实现无关的接口
    方法。

结合一个例子来讲解。微服务用户系统提供了一组跟用户相关的 API 给其他
系统使用,比如:注册、登录、获取用户信息等。具体代码如下所示:

public interface UserService {
    boolean register(String cellphone, String password);
    boolean login(String cellphone, String password);
    UserInfo getUserInfoById(long id);
    UserInfo getUserInfoByCellphone(String cellphone);
}

public class UserServiceImpl implements UserService {
    //...

现在,我们的后台管理系统要实现删除用户的功能,希望用户系统提供一个删除用户的接口。这个时候我们该如何来做呢?你可能会说,这不是很简单吗,我只需要在 UserService 中新添加一个 deleteUserByCellphone() 或 deleteUserById() 接口就可以了。这个方法可以解决问题,但是也隐藏了一些安全隐患。

删除用户是一个非常慎重的操作,我们只希望通过后台管理系统来执行,所以这个接口只限于给后台管理系统使用。如果我们把它放到 UserService 中,那所有使用到 UserService 的系统,都可以调用这个接口。不加限制地被其他业务系统调用,就有可能导致误删用户。

当然,最好的解决方案是从架构设计的层面,通过接口鉴权的方式来限制接口的调用。不过,如果暂时没有鉴权框架来支持,我们还可以从代码设计的层面,尽量避免接口被误用。我们参照接口隔离原则,调用者不应该强迫依赖它不需要的接口,将删除接口单独放到另外一个接口 RestrictedUserService 中,然后将 RestrictedUserService 只打包提供给后台管理系统来使用。具体的代码实现如下所示:

public interface UserService {
    boolean register(String cellphone, String password);
    boolean login(String cellphone, String password);
    UserInfo getUserInfoById(long id);
    UserInfo getUserInfoByCellphone(String cellphone);
}

public interface RestrictedUserService {
    boolean deleteUserByCellphone(String cellphone);
    boolean deleteUserById(long id);
}

public class UserServiceImpl implements UserService, RestrictedUserService {
    // ... 省略实现代码...

}

在刚刚的这个例子中,我们把接口隔离原则中的接口,理解为一组接口集合,它可以是某个微服务的接口,也可以是某个类库的接口等等。在设计微服务或者类库接口的时候,如果部分接口只被部分调用者使用,那我们就需要将这部分接口隔离出来,单独给对应的调用者使用,而不是强迫其他调用者也依赖这部分不会被用到的接口。

里氏替换原则

子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

虽然从定义描述和代码实现上来看,多态和里式替换有点类似,但它们关注的角度是不一样的。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。

依赖倒置原则

关于 SOLID 原则,我们已经学过单一职责、开闭、里式替换、接口隔离这四个原则。我们再来学习最后一个原则:依赖倒置原则。

1、原理

依赖倒置原则(Dependency Inversion Principle,简称 DIP)是面向对象设计的五大原则(SOLID)之一。这个原则强调要依赖于抽象而不是具体实现。遵循这个原则可以使系统的设计更加灵活、可扩展和可维护。

依赖倒置原则有两个关键点:

  1. 高层模块不应该依赖于低层模块,它们都应该依赖于抽象。

  2. 抽象不应该依赖于具体实现,具体实现应该依赖于抽象。

倒置(Inversion)在这里的确是指“反过来”的意思。在依赖倒置原则(Dependency Inversion Principle, DIP)中,我们需要改变依赖关系的方向,使得高层模块和低层模块都依赖于抽象,而不是高层模块直接依赖于低层模块。这样一来,依赖关系就从直接依赖具体实现“反过来”依赖抽象了。

理解“KISS 原则”

之前,我们学习了经典的 SOLID 原则。现在,我们开始学习KISS 原则和 YAGNI 原则。

KISS 原则(Keep It Simple, Stupid):KISS 原则强调保持代码简单,易于理解和维护。在编写代码时,应避免使用复杂的逻辑、算法和技术,尽量保持代码简洁明了。这样可以提高代码的可读性、可维护性和可测试性。

DRY 原则

DRY 原则(Don’t Repeat Yourself):DRY 原则强调避免代码重复,尽量将相似的代码和逻辑提取到共享的方法、类或模块中。遵循 DRY 原则可以减少代码的冗余和重复,提高代码的复用性和可维护性。当需要修改某个功能时,只需修改对应的共享代码,而无需在多处进行相同的修改。这有助于降低维护成本,提高开发效率。

迪米特法则(Law of Demeter, LoD)

又称最少知识原则(Least Knowledge Principle, LKP),是一种面向对象编程设计原则。它的核心思想是:一个对象应该尽量少地了解其他对象,降低对象之间的耦合度,从而提高代码的可维护性和可扩展性。

我们之前讲过,大部分设计原则和思想都非常抽象,有各种各样的解读,要想灵活地应用到实际的开发中,需要有实战经验的积累。迪米特法则也不例外。所以,我结合我自己的理解和经验,我们可以说的更加直白一点:两个类之间尽量不要直接依赖,如果必须依赖,最好只依赖必要的接口

迪米特法则的主要指导原则如下:

  • 类和类之间尽量不直接依赖。
  • 有依赖关系的类之间,尽量只依赖必要的接口。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值