关于SOLID原则简单理解

关于软件的设计原则其实很多,其中比较经典的有SOLID、KISS、YAGNI、DRY、LOD 等。
SOLID原则并非是1个原则,而是由5个设计原则组成的,它们分别是:单一职责原则、开闭原则、里式替换原则、接口隔离原则和依赖反转原则,依次对应 SOLID 中的 S、O、L、I、D 这 5 个英文字母。我们一个一个来说。

单一职责

单一职责原则的英文是 Single Responsibility Principle,缩写为 SRP。他的英文描述是:A class or module should have a single reponsibility。翻译过来就是:一个类或者模块只负责完成一个职责(或者功能)。

意思是一个类只负责完成一个职责或者功能。也就是说,不要设计大而全的类,要设计粒度小、功能单一的类。换个角度来讲就是,一个类包含了两个或者两个以上业务不相干的功能,那我们就说它职责不够单一,应该将它拆分成多个功能更加单一、粒度更细的类。

这个原则看似比较简单,但实际在开发中判断一个类是否单一是比较难拿捏的。因为在不同的应用场景和业务的不同阶段,对同一个类职责判断是否单一都是有可能不一样的。在当前需求下一个类可能已经满足了单一职责的判断,但如果换一个应用场景或者未来的某个需求背景下可能就不满足了,需要继续拆分成粒度更细的类。所以通常我们可以先写一个粗粒度的类,满足业务需求。随着业务的发展如果粗粒度的类越来越庞大的时候,我们再将这个粗粒度的类拆分成几个更细粒度的类。

判断一个类是否需要拆分有以下几个原则供参考:

  • 类中的代码行数、函数或属性过多,会影响代码的可读性和可维护性,我们就需要考虑对类进行拆分;
  • 类依赖的其他类过多,或者依赖类的其他类过多,不符合高内聚、低耦合的设计思想,我们就需要考虑对类进行拆分;
  • 私有方法过多,我们就要考虑能否将私有方法独立到新的类中,设置为 public 方法,供更多的类使用,从而提高代码的复用性;
  • 比较难给类起一个合适名字,很难用一个业务名词概括,或者只能用一些笼统的 Manager、Context 之类的词语来命名,这就说明类的职责定义得可能不够清晰;
  • 类中大量的方法都是集中操作类中的某几个属性,那就可以考虑将这几个属性和对应的方法拆分出来。
开闭原则

开闭原则的英文全称是 Open Closed Principle,简写为 OCP。它的英文描述是:software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。翻译过来就是:软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。

这个描述比较简略,详细表述一下就是:添加一个新的功能应该是在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。
这里说的不修改已有代码并不是指完全杜绝修改代码,在软件开发需求迭代中修改代码是在所难免的,我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。

打个比方说,有一个插入用户信息:姓名,年龄,电话的方法。

class Demo{
      public void insertUserInfo(String name,int age,String tel){
          ....
      }

}

如果随着业务的发展这个方法需要把用户的email也插入进去,如果我们直接修改这个方法添加一个email参数

class Demo{
      public void insertUserInfo(String name,int age,String tel,String email){
          ....
      }

}

这样的修改就有违背开闭原则,代码上所有调用这个方法的地方都需要修改。

假如是这样的代码

class Demo{
      public void insert(User user){
          ....
      }

}

class User{
    String name;
    int age;
    String tel;
}

我们为了满足业务的需求,需要在插入用户信息的时候把email也插入进去,那么我们直接在User类中添加一个email属性,这样的修改就没有违背开闭原则。

其实也可以这样理解,开闭原则说的是软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。我们可以看出来,开闭原则可以应用在不同粒度的代码中,可以是模块,也可以类,还可以是方法(及其属性)。同样一个代码改动,在粗代码粒度下,被认定为“修改”,在细代码粒度下,又可以被认定为“扩展”。比如,添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为“修改”;但这个代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为“扩展”。

再则我们通过上面满足同一需求的方法代码来看,下面那个传User对象的方法比上面的方法更方便扩展一些,这也就是为什么要遵循开闭原则来写代码的原因。

里式替换原则

里式替换原则的英文翻译是:Liskov Substitution Principle,缩写为 LSP。这个原则最早是在 1986 年由 Barbara Liskov 提出,他是这么描述这条原则的:

If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。

在 1996 年,Robert Martin 在他的 SOLID 原则中,重新描述了这个原则,英文原话是这样的:

Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。

综合两者的描述,将这条原则用中文描述出来,是这样的:子类对象(object of subtype/derived class)能够替换程序(program)中父类对象(object of base/parent class)出现的任何地方,并且保证原来程序的逻辑行为(behavior)不变及正确性不被破坏。

原则里说“子类替换父类出现的地方”咋一看有点像面向对象里多态的意思,其实不是。多态是面向对象编程的一大特性,也是面向对象编程语言的一种语法。它是一种代码实现的思路。而里式替换是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。直接来讲就是按照协议来设计。

举个例子

class SecurityService{

    public boolean isRightfulPassword(String password){
          if(password==null || password.length < 6){
                return false;
          }
          return true;
    }
}

class SonSecurityService extends SecurityService{
     @Override
     public boolean isRightfulPassword(String password){
          if(password==null){
               throw new ParameterNullRunTimeException();
          }
          return super.isRightfulPassword(password);
    }
}

在父类SecurityService中isRightfulPassword参数password为空的时候返回的结果是false,而子类SonSecurityService在替换父类后,当password为空时却抛了一个异常,这就违背了里式替换原则。

里式替换原则是用来指导继承关系中子类该如何设计的一个原则。理解里式替换原则,最核心的就是理解“按照协议来设计(design by contract)”。父类定义了函数的“约定”(或者叫协议),那子类可以改变函数的内部实现逻辑,但不能改变函数原有的“约定”。这里的约定包括:函数声明要实现的功能;对输入、输出、异常的约定;业务上的特殊限制;

有一个比较简单的方法来判断是否违背了里式替换原则。那就是拿父类的单元测试去验证子类的代码。如果某些单元测试运行失败,就有可能说明,子类有可能违背了里式替换原则。

接口隔离原则

接口隔离原则的英文翻译是“ Interface Segregation Principle”,缩写为 ISP。Robert Martin 在 SOLID 原则中是这样定义它的:“Clients should not be forced to depend upon interfaces that they do not use。”直译成中文的话就是:客户端不应该强迫依赖它不需要的接口。其中的“客户端”,可以理解为接口的调用者或者使用者。

举个例子:

public interface UserService {
      //注册
      boolean register(String username, String password);
      //登录
      boolean login(String username, String password);
      //查询用户
      User getUserById(int id);
     //删除用户
      boolean deleteUserById(int id);
}

有一个用户服务的接口,里面有注册,登录,查询,删除几个函数暴露给客户端,但是很明显删除函数只有后台管理模块可以用到,而且其他模块如果都可以使用的话就有可能照成误删用户。这就违背了接口隔离原则,我们需要把删除函数单独抽离出来给后台管理模块使用。

public interface UserService {
      //注册
      boolean register(String username, String password);
      //登录
      boolean login(String username, String password);
      //查询用户
      User getUserById(int id);
      
}

public interface AdminUserService {
        //删除用户
      boolean deleteUserById(int id);
}

其中“接口”也可以有不同的含义:

  • 可以把它理解为微服务的一组接口,如果部分接口只被部分调用者使用,我们就需要将这部分接口隔离出来,单独给这部分调用者使用,而不强迫其他调用者也依赖这部分不会被用到的接口;
  • 可以把它理解成api接口或者函数,部分调用者只需要函数中的部分功能,那我们就需要把函数拆分成粒度更细的多个函数,让调用者只依赖它需要的那个细粒度函数。
  • 可以把它理解成面向对象编程中定义的接口语法,那接口的设计要尽量单一,不要让接口的实现类和调用者,依赖不需要的接口函数。

说起接口隔离其实和单一原则比较类似,都是指在代码设计上尽量单一。不同的是单一原则针对的是模块,类,接口的设计。接口隔离原则不光说的是接口的设计,还提供了一种判断接口职责是否单一的标准:如果调用者只使用部分接口或接口的部分功能,那接口的设计就不够职责单一。

依赖反转原则

依赖反转原则的英文翻译是 Dependency Inversion Principle,缩写为 DIP。中文翻译有时候也叫依赖倒置原则。它的英文描述是:

High-level modules shouldn’t depend on low-level modules. Both modules should depend on abstractions. In addition, abstractions shouldn’t depend on details. Details depend on abstractions.

翻译过来的意思就是高层模块(high-level modules)不要依赖低层模块(low-level)。高层模块和低层模块应该通过抽象(abstractions)来互相依赖。除此之外,抽象(abstractions)不要依赖具体实现细节(details),具体实现细节(details)依赖抽象(abstractions)。

所谓高层模块和低层模块的划分,简单来说就是,在调用链上,调用者属于高层,被调用者属于低层。高层不要直接去依赖底层,举个例子:有一个小朋友,小时候喝牛奶那么小朋友通过drink函数来调用牛奶,小朋友就是高层牛奶就是底层。

class Child {
    public void drink(Milk milk){
        System.out.println("喝牛奶。。。");
        System.out.println(milk.getTaste());
    }

}
class Milk {
 
    public void getTaste(){
        System.out.println("很甜...");
    }
}

class Client {
    public static void main(String[] args) {
         Child child = new Child();
         Milk  milk = new Milk();
         child.drink(milk );
    }
}

这样写会有一个问题,假如小朋友长大了要喝果汁了要喝水了怎么办,drink方法就用不了了。

class Water{

    public void getTaste(){
        System.out.println("没有味道...");
    }
}
class Juice{

    public void getTaste(){
        System.out.println("很甜...");
    }
}

我们通常会这么去做:高层模块和低层模块应该通过抽象来互相依赖,不依赖具体实现。也就是说牛奶,水,果汁这些都是具体实现,我们不去直接依赖它而是依赖一个抽象类或者接口。

public interface  Fluid {
    public void getTaste();
}

class Child {
    public void drink(Fluid fluid){
        System.out.println("喝东西...");
        System.out.println(fluid.getTaste());
    }

}
class Milk implements Fluid {
  
    public void getTaste(){
        System.out.println("很甜...");
    }
}
class Water implements Fluid {

    public void getTaste(){
        System.out.println("没有味道...");
    }
}
class Juice implements Fluid {
   
    public void getTaste(){
        System.out.println("很甜...");
    }
}

class Client {
    public static void main(String[] args) {
         Child child = new Child();
         Fluid  milk= new Milk();
         child.drink(milk); //喝牛奶
         Fluid  water= new Water();
         child.drink(water); //喝水
    }
}

其实这也是我们常说的面向接口编程,DIP最大的指导意义就是让代码更具有扩展性。像很多框架就是为了帮助我们更容易遵循依赖反转来设计的,例如spring。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容