SOLID原则,是面向对象编程的几个重要概念的英文首字母缩写,也是面向对象编程中最基础的几个概念。最早是由《代码清洁之道》的作者Bob Martin和《修改代码的艺术》的作者Michael Feathers 提出来。
但是,基础,并不意味着很多人都掌握,其实,不掌握的还是很多。对于掌握了的人来说,能用好的也不多。为什么会这样呢?也许因为对我们人类来说,它却恰好是反模式。
SOLID 原则是什么
SOLID 不是一个原则,他是一组面向对象设计原则的简称,他代表下面5种设计原则:
S ingle Responsibility Principle 单一职责原则
O pen/Closed Principle 开闭原则
L iskov Substitution Principle 里氏替换原则
I nterface Segregation Principle 接口分离原则
D ependency Inversion Principle 依赖倒置原则
以上就是SOLID中的5种面向对象设计原则,下面分别看看他们具体指的是什么。
单一职责原则,即 Single Responsibility Principle
单一职责原则是修改一个类的理由从来不应超过一个。
这条原则虽然看上去简单,但是实施起来非常难。因为你很难去界定一个类的职责。
这个职责的确定有一些技巧,比如说你在实现这个类的时候,不需要去关注其他的类;还有就是你修改这个类的时候,如果影响到这个类的其他职责的实现,那说明我们需要拆分这个类了。
即为何要将两个职责分离到单独的类?
因为每个职责都是变化的一个轴线,当需求变化,该变化会反映为类的职责变化,如果一个类承担多个职责,那么引起它变化的原因就有多个。
如果一个类承担的职责过多,就等于把职责耦合在一起,一个职责的变化可能会削弱或抑制这个类完成其他职责的能力。
所以,单一职责可以降低耦合性,增强代码的可复用性,降低某个类的复杂度。
//购物车
class Cart
{
// 添加商品
public function addItem(){ /* ... */}
// 移除商品
public function deleteItem(){/* ... */}
// 获取商品
public function getItem(){/* ... */}
// 设定订单信息
public function setOrderInfo($info){/* ... */}
// 取得付款信息
public function getPaymentInfo(){/* ... */}
// 保存订单
public function saveOrder(){/* ... */}
// 发送订单确认邮件
public function sendMail(){/* ... */}
}
$cart = new Cart();
$cart->addItem(); // 添加商品
$cart->getPaymentInfo(); //获取付款信息
重构后:
// 订单
class Order
{
// 设定订单信息
public function setOrderInfo($info){/* ... */}
// 取得付款信息
public function getPaymentInfo(){/* ... */}
// 保存订单
public function saveOrder(){/* ... */}
// 发送订单确认邮件
public function sendMail(){/* ... */}
}
// 重构后的购物车
class Cart
{
// 添加商品
public function addItem(){ /* ... */}
// 移除商品
public function deleteItem(){/* ... */}
// 获取商品
public function getItem(){/* ... */}
}
$cart = new Cart();
$order = new Order();
$cart->addItem(); // 添加商品
$order->getPaymentInfo(); //获取付款信息
开放封闭原则(Open/Closed Principle)
类或者方法,应该对扩展是开放的,对修改是关闭的
这个是面向对象编程原则中最为抽象、最难理解的。「对扩展开放」指的是设计类时要考虑到新需求提出是类可以增加新功能,「对修改关闭」指的是一旦一个类开发完成,除了修正 BUG 就不要再去修改它。这个原则前后两部似乎是冲突的,但是如果正确地设计类和它们的依赖关系,就可以增加功能而不修改已有的源代码。通常可以通过依赖关系抽象实现开闭原则,比如 interface(接口) 或 abstract(抽象类)而不是具体类,通过创建新的类实现它们来增加功能。这个原则能减少改出来的 BUG 出现,而且增加软件本身的灵活性。
详细可见thrift的抽象工厂类的实现。
里氏替换原则(Liskov Substitution Principle)
如果它看上去像一只鸭子,并且像鸭子一样嘎嘎叫,但是需要电池 - 你可能错误的抽象了
当一个子类的实例应该能够替换任何其父类的实例时,它们之间才具有 IS-A 关系
何为LSP
定义:派生类(子类)对象能够替换其基类(超类)对象被使用, is - a
Barbara Liskov对LSP定义是这么说的:若对每个类型S的对象q1,都存在一个类型T的对象q2,使得在所有对T编写的程序P中,用q1替换q2后,程序P行为功能不变,则S是T的子类型。 听着有些绕,我将它画一个类图便于理解:
为何要有LSP
- 首先来分析下:
现在我说天上飞着一只鸟....
子类麻雀替换父类:天上飞着一只麻雀。
子类鸵鸟替换父类:天上飞着一只鸵鸟。
由上因为违反了里氏替代原则,导致整个设计存在严重逻辑错误。
由于违反了里氏替代原则,间接的违反了OCP原则。因为明显可以看出飞翔对于鸵鸟因该是封闭的。
- 现在来看一下代码( LSP的违反导致OCP的违反)
/** * 鸟 */
class Bird
{
public static final int IS_OSTRICH = 1;//是鸵鸟
public static final int IS_SPARROW = 2;//是麻雀
public int isType;
public Bird(int isType) { this.isType = isType; }
}
/** * 鸵鸟 */
class Ostrich extends Alimal
{
public Ostrich() { super(Bird.IS_OSTRICH); }
private void run(){ System.out.print("我跑得飞快的!"); }
public move(){ run() }
}
/** * 麻雀 */
class Sparrow extends Bird
{
public Sparrow() { super(Bird.IS_SPARROW); }
public void fly(){ System.out.print("我会飞啊飞!"); }
}
现在有一个方法toBeijing,统一处理去北京的行为
public void toBeijing(Alimal alimalObj) {
if (bird.isType == Bird.IS_OSTRICH) {
Ostrich ostrich = (Ostrich) bird;
ostrich.run();
} else if (bird.isType == Bird.IS_SPARROW) {
Sparrow sparrow = (Sparrow) bird;
sparrow.fly();
}
}
3. 分析 大家可以看出,birdLetGo方法明显的违反了开闭原则,它必须要知道所有Bird的子类。并且每次创建一个Bird子类就得修改它一次。
破窗效应:一个很漂亮的房子,
接口分离原则(Interface Segregation Principle)
多个专用的接口比一个通用的接口好
这个原则定义了一个类决不要实现不会用到的接口。不遵循这个原则意味着在我们在实现里会依赖很多我们并不需要的方法,但又不得不去定义。 所以,实现多个特定的接口比实现一个通用接口要好。一个接口被需要用到的类所定义,所以这个接口不应该有这个类不需要实现的其它方法。
好处:
系统解耦。
代码易于重构。
违反 ISP 原则
- 我们有一个 Car 的接口
public interface Car {
void startEngine();
void accelerate();
}
- 同时也有一个实现 Car 接口的 Mustang(野马) 类:
public class Mustang implements Car {
@Override
public void startEngine() {
// start engine...
}
@Override public void accelerate() {
// accelerate...
}
}
现在我们有个新的需求,要添加一个新的车型:
一辆 DeloRean(德罗宁), 但这并不是一个普通的 DeLorean,我们的 DeloRean 非常特别,它有穿梭时光的功能。
像以往一样,我们没有时间来做一个好的实现,而且 DeloRean 必须马上回到过去。
- 为我们的 DeloRean 在 Car 接口里增加两个新的方法:
public interface Car {
void startEngine();
void accelerate();
void backToThePast();
void backToTheFuture();
}
- 现在我们的 DeloRean 实现 Car 的方法:
public class DeloRean implements Car {
@Override
public void startEngine() {
// start engine...
}
@Override
public void accelerate() {
// accelerate...
}
@Override
public void backToThePast() {
// back to the past...
}
@Override
public void backToTheFuture() {
// back to the future...
}
}
- 但是现在 Mustang 被迫去实现在 Car 接口里的新方法:
public class Mustang implements Car {
@Override
public void startEngine() {
// 启动引擎
}
@Override public void accelerate() {
// 加速
}
@Override public void backToThePast() {
// 因为 Mustang 不能回到过去!
throw new UnsupportedOperationException();
}
@Override public void backToTheFuture() {
// 因为 Mustang 不能穿越去未来!
throw new UnsupportedOperationException();
}
}
依赖导致原则(Dependency Inversion Principle)
高层次的模块不应该依赖低层次的模块,他们都应该依赖于抽象。
抽象不应该依赖于细节,细节应该依赖于抽象。
依赖倒转原则的意思是一个特定的类不应该直接依赖于另外一个类,但是可以依赖于这个类的抽象(接口)。
当我们应用这个原则的时候我们能减少对特定实现的依赖性,让我们的代码复用性更高。
好处:
减少耦合。
代码更高的复用性。
违反 DIP 原则:
- 我们有一个类叫 DeliveryDriver 代表着一个司机为快递公司工作。
public class DeliveryDriver {
public void deliverProduct(Product product){
// 运送产品
}
}
- DeliveryCompany 类处理货物装运:
public class DeliveryCompany {
public void sendProduct(Product product) {
DeliveryDriver deliveryDriver = new DeliveryDriver();
deliveryDriver.deliverProduct(product);
}
}
我们注意到 DeliveryCompany 创建并使用 DeliveryDriver 实例。所以 DeliveryCompany 是一个依赖于低层次类的高层次的类,这就违背了依赖倒转原则。(译者注:上述代码中 DeliveryCompany 需要运送货物,必须需要一个 DeliveryDriver 参与。但如果以后对司机有更多的要求,那我们既要修改 DeliveryDriver 也要修改上述代码。这样造成的依赖,耦合度高)
A solution:
解决方法:
- 我们创建 DeliveryService 接口,这样我们就有了一个抽象。
public interface DeliveryService {
void deliverProduct(Product product);
}
- 重构 DeliveryDriver 类以实现 DeliveryService 的抽象方法:
public class DeliveryDriver implements DeliveryService {
@Override public void deliverProduct(Product product) {
// 运送产品
}
}
- 重构 DeliveryCompany,使它依赖于一个抽象而不是一个具体的东西。
public class DeliveryCompany {
private DeliveryService deliveryService;
public DeliveryCompany(DeliveryService deliveryService) {
this.deliveryService = deliveryService;
}
public void sendProduct(Product product) {
this.deliveryService.deliverProduct(product);
}
}
现在,依赖在别的地方创建,并且从类构造器中被注入。
千万不要把这个原则与依赖注入混淆。依赖注入是一种设计模式,帮助我们应用这个原则来确保各个类之间的合作不涉及相互依赖。
结论
遵循 SOLID 原则来构建高质量, 易于扩展, 足够健壮并且可复用的软件是非常必要的。同时, 我们也不要忘了从实际和常识出发, 因为有的时候过份设计会使简单的问题复杂化。
欢迎加入一线互联网公司架构交流群,QQ群聊号码:783153655,附赠infoQ上的技术大咖的架构分享资料