面向对象设计的SOLID原则

本文详细阐述了SOLID五个面向对象设计原则:单一责任原则、开放封闭原则、里氏替换原则、接口分离原则及依赖倒置原则。这些原则有助于开发者构建更加灵活、可维护的软件系统。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在21世纪的前几年里,"Uncle Bob" Robert Martin引入了用OOP开发软件的五条原则,其目的是设计出更易于维护的高质量系统。无论是设计新应用程序,还是重构现有基本代码,这些SOLID原则都成为开发人员的地图。

缩写全称翻译
SRPThe Single Responsibility Principle单一责任原则
OCPThe Open Closed Principle开放封闭原则
LSPThe Liskov Substitution Principle里氏替换原则
ISPThe Interface Segregation Principle接口分离原则
DIPThe Dependency Inversion Principle依赖倒置原则

单一责任原则/单一职责原则(SRP): 

当需要修改某个类的时候原因有且只有一个(THERE SHOULD NEVER BE MORE THAN ONE REASON FOR A CLASS TO CHANGE)。换句话说就是 让一个类只做一种类型责任,当这个类需要承担其他类型的责任的时候,就需要分解这个类。在所有的SOLID原则中,这是大多数开发人员感到最能完全理解的一条。严格来说,这也可能是违反最频繁的一条原则了。

单一责任原则可以看作是低耦合、高内聚在面向对象原则上的引申,将责任定义为引起变化的原因,以提高内聚性来减少引起变化的原因。责任过多,可能引起它变化的原因就越多,这将导致责任依赖,相互之间就产生影响,从而极大的损伤其内聚性和耦合度。单一责任,通常意味着单一的功能,因此不要为一个模块实现过多的功能点,以保证实体只有一个引起它变化的原因。

本原则的应用:

  • Unix/Linux是这一原则的完美体现者。各个程序都独立负责一个单一的事。
  • Windows是这一原则的反面示例。几乎所有的程序都交织耦合在一起。

另外举个例子:报表的内容和报表的格式都会变化改变,但是这两种变化的性质不同,一个是实质内在,一个是表面上的,SRP认为这是问题的两个方面,其实代表不同的责任,应该将它们分离放入不同的类或模块中,而不应该放在一起,否则的话,因为不同原因发生变化,导致对方变动,比如报表格式变新的样式,这个变化是不应该涉及到内容的。


开放封闭原则/开闭原则(OCP): 

软件实体应该是可扩展,而不可修改的。也就是说,对扩展是开放的,而对修改是封闭的。这个原则是诸多面向对象编程原则中最抽象、最难理解的一个。

  • 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
  • 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对类进行任何修改。

可以使用变化和不变来说明:封装不变部分,开放变化部分,一般使用接口继承实现方式来实现“开放”应对变化,说大白话就是:你不是要变化吗?,那么我就让你继承实现一个对象,用一个接口来抽象你的职责,你变化越多,继承实现的子类就越多。

OCP的要点在于:作为开发人员,别人偶尔会向我们提供基类,偶尔也会为其他开发人员生成基类框架,供其使用。这些使用者应当仅能使用这些基类,但不能对其进行修改。这一点是必要的,因为其他使用者也可能依赖于由基类提供的功能。如果允许使用者修改这些基类,可能会导致连锁反应,不仅会影响到应用程序中的各方面,还会影响到企业内的应用程序。还有一个问题,使用者有时可能会收到基类的升级版本。使用者在升级之前,必须找出一种方法用来处理其对该基类先前版本中所做的自定义。

于是,问题变为:"那么,如果我需要修改这个基类的工作方式,那应当怎么做呢?"OCP的另一部分中给出这一答案;基类应当开放,可进行扩充。在这里,扩充是指创建一个由此基类继承而来的派生类,它可以扩充或重载基类功能,以提供使用者所需要的特定功能。这样,使用者就能使用类的修改版本,而不会影响到类的其他使用者。使用者还可以在将来更轻松地使用基类的升级版本,因为他们不用担心丢失自己的修改内容。

本原则的应用:

对于面向对象来说,需要你依赖抽象,而不是实现,23个经典设计模式中的“策略模式”就是这个实现。对于非面向对象编程,一些API需要你传入一个你可以扩展的函数,比如我们的C语言的qsort()允许你提供一个“比较器”,STL中的容器类的内存分配,ACE中的多线程的各种锁。对于软件方面,浏览器的各种插件属于这个原则的实践。


里氏替换原则(LSP):

软件工程大师Robert C. Martin把里氏替换原则最终简化为一句话:“Subtypes must be substitutable for their base types”。也就是,子类必须能够替换成它们的基类。即:子类应该可以替换任何基类能够出现的地方,并且经过替换以后,代码还能正常工作。另外,不应该在代码中出现if/else之类对子类类型进行判断的条件。里氏替换原则LSP是使代码符合开闭原则的一个重要保证。正是由于子类型的可替换性才使得父类型的模块在无需修改的情况下就可以扩展。在很多情况下,在设计初期我们类之间的关系不是很明确,LSP则给了我们一个判断和设计类之间关系的基准:需不需要继承,以及怎样设计继承关系。

当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。继承对于OCP,就相当于多态性对于里氏替换原则。子类可以代替基类,客户使用基类,他们不需要知道派生类所做的事情。这是一个针对行为职责可替代的原则,如果S是T的子类型,那么S对象就应该在不改变任何抽象属性情况下替换所有T对象。

我们使用接口时经常碰到一个问题,需要使用接口子类中的方法,而接口中没有这个方法,那么只能要么修改接口,要么将接口向下转型(down cast)为具体子类。为什么会出现这个尴尬现象?有几种情况导致,其中一种情况是是将当前的类重构到接口时,没有将类中所有方法提取(extract)到接口中,可能因为这些被漏掉的方法不属于当前接口,那么,它又违背了单一职责原理,说明你当前这个类的方法设计得又不合理。

所以,如果单一职责设计的足够好,那么LSP原则则是检验的方法。LSP原则是对对象职责和协作的一种检验约束方法,此外还有契约式设计DBC(design by contract)原则,为了保证实现接口的子类职责行为的约束,DBC三要素都必须被重视满足:

  1. 前置条件(Preconditions)不能在子类中被强化。
  2. 后置条件(Postconditions)不能在子类中被弱化。
  3. 子类自身不变性(Invariants)必须在子类自己中封装满足。这也是前面“不改变任何抽象属性”的意思。

本原则的应用:

这个原则有两个最经典的案例——“正方形不是长方形”和“鸵鸟不是鸟”。通过这两个案例,你会明白《墨子 小取》中说的 ——“娣,美人也,爱娣,非爱美人也….盗,人也;恶盗,非恶人也。”——妹妹虽然是美人,但喜欢妹妹并不代表喜欢美人。盗贼是人,但讨厌盗贼也并不代表就讨厌人类。这个原则让你考虑的不是语义上对象的间的关系,而是实际需求的环境。


接口分离原则(ISP):

不能强迫用户去依赖那些他们不使用的接口。换句话说,使用多个专门的接口比使用单一的总接口总要好。注意:在代码中应用ISP并不一定意味着服务就是绝对安全的。仍然需要采用良好的编码实践,以确保正确的验证与授权。

这个原则起源于施乐公司,他们需要建立了一个新的打印机系统,可以执行诸如装订的印刷品一套,传真多种任务。此系统软件创建从底层开始编制,并实现了这些任务功能,但是不断增长的软件功能却使软件本身越来越难适应变化和维护。每一次改变,即使是最小的变化,有人可能需要近一个小时的重新编译和重新部署。这是几乎不可能再继续发展,所以他们聘请罗伯特Robert帮助他们。他们首先设计了一个主要类Job,几乎能够用于实现所有任务功能。只要调用Job类的一个方法就可以实现一个功能,Job类就变动非常大,是一个胖模型啊,对于客户端如果只需要一个打印功能,但是其他无关打印的方法功能也和其耦合,ISP原则建议在客户端和Job类之间增加一个接口层,对于不同功能有不同接口,比如打印功能就是Print接口,然后将大的Job类切分为继承不同接口的子类,这样有一个Print Job类,等等。

本原则的应用:

这个原则可以提升我们“搭积木式”的软件开发。对于设计来说,Java中的各种Event Listener和Adapter,对于软件开发来说,不同的用户权限有不同的功能,不同的版本有不同的功能,都是这个原则的应用。


依赖倒置原则(DIP):

依赖倒置原则(Dependency Inversion Principle,DIP)规定:代码应当取决于抽象概念,而不是具体实现

  1. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象 
  2. 抽象不应该依赖于细节,细节应该依赖于抽象 

类可能依赖于其他类来执行其工作。但是,它们不应当依赖于该类的特定具体实现,而应当是它的抽象。这个原则实在是太重要了,社会的分工化,标准化都是这个设计原则的体现。显然,这一概念会大大提高系统的灵活性。如果类只关心它们用于支持特定契约而不是特定类型的组件,就可以快速而轻松地修改这些低级服务的功能,同时最大限度地降低对系统其余部分的影响。

本原则的应用:

举个例子,墙面的开关不应该依赖于电灯的开关实现,而是应该依赖于一个抽象的开关的标准接口,这样,当我们扩展程序的时候,我们的开关同样可以控制其它不同的灯,甚至不同的电器。也就是说,电灯和其它电器继承并实现我们的标准开关接口,而我们的开关产商就可不需要关于其要控制什么样的设备,只需要关心那个标准的开关标准。这就是依赖倒置原则。这就好像浏览器并不依赖于后面的web服务器,其只依赖于HTTP协议。

Dependency Injection 依赖注入模式也是属于这种类型变种,GoF设计模式中适配器模式中,高层次类只依赖于adapter接口,而被适配者低层次也只依赖adapter接口。


参考资料:

1. 面向对象设计的SOLID原则(此文中有形象的图片予以说明)

2. 【整理】面向对象设计的SOLID原则

3. SOLID原则

4. 3.2.2 SOLID原则

5. 面向对象的S.O.L.I.D 原则

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值