SOLID原则是一种编码的标准,为了避免不良设计,所有的软件开发人员都应该清楚这些原则。SOLID原则是由Robert C Martin推广并被广泛引用于面向对象编程中。正确使用这些规范将提升你的代码的可扩展性、逻辑性和可读性。

当开发人员按照不好的设计来开发软件时,代码将失去灵活性和健壮性。任何一点点小的修改都非常容易引起bug。因此,我们应该遵循SOLID原则。

首先我们需要花一些时间来了解SOLID原则,当你能够理解这些原则并正确使用时,你的代码质量将会得到大幅的提高。同时,它可以帮助你更好的理解一些优秀软件的设计。

为了理解SOLID原则,你必须清楚接口的用法,如果你还不理解接口的概念,建议你先读一读这篇文章

下面我将用简单易懂的方式为你描述SOLID原则,希望能帮助你对这些原则有个初步的理解。

单一责任原则

一个类只能因为一个理由被修改。

A class should have one, and only one, reason to change.

一个类应该只为一个目标服务。并不是说每个类都只能有一个方法,但它们都应该与类的责任有直接关系。所有的方法和属性都应该努力做好同一类事情。当一个类具有多个目标或职责时,就应该创建一个新的类出来。

我们来看一下这段代码:

public class OrdersReportService {

    public List<OrderVO> getOrdersInfo(Date startDate, Date endDate) {
List<OrderDO> orders = queryDBForOrders(startDate, endDate); return transform(orders);
} private List<OrderDO> queryDBForOrders(Date startDate, Date endDate) {
// select * from order where date >= startDate and date < endDate;
} private List<OrderVO> transform(List<OrderDO> orderDOList) {
//transform DO to VO
}
}

这段代码就违反了单一责任原则。为什么会在这个类中执行sql语句?这样的操作应该放到持久化层,持久化层负责处理数据的持久化的相关操作,包括从数据库中存储或查询数据。所以这个职责不应该属于这个类。

transform方法同样不应该属于这个类,因为我们可能需要很多种类型的转换。

因此我们需要对代码进行重构,重构之后的代码如下(为了节省篇幅):

public class OrdersReportService {

    @Autowired
private OrdersReportDao ordersReportDao;
@Autowired
private Formatter formatter;
public List<OrderVO> getOrdersInfo(Date startDate, Date endDate) {
List<OrderDO> orders = ordersReportDao.queryDBForOrders(startDate, endDate); return formatter.transform(orders);
}
} public class OrdersReportDao { public List<OrderDO> queryDBForOrders(Date startDate, Date endDate) {}
} public class Formatter { private List<OrderVO> transform(List<OrderDO> orderDOList) {}
}

开闭原则

对扩展开放,对修改关闭。

Entities should be open for extension, but closed for modification.

软件实体(包括类、模块、函数等)都应该可扩展,而不用因为扩展而修改实体的内容。如果我们严格遵循这个原则,就可以做到修改代码行为时,不需要改动任何原始代码。

我们还是以一段代码为例:

class Rectangle extends Shape {
private int width;
private int height; public Rectangle(int width, int height) {
this.width = width;
this.height = height;
}
}
class Circle extends Shape {
private int radius; public Circle(int radius) {
this.radius = radius;
}
}
class CostManager {
public double calculate(Shape shape) {
double costPerUnit = 1.5;
double area;
if (shape instanceof Rectangle) {
area = shape.getWidth() * shape.getHeight();
} else {
area = shape.getRadius() * shape.getRadius() * pi();
} return costPerUnit * area;
}
}

如果你想要计算正方形的面积,那么我们就需要修改calculate方法的代码。这就破坏了开闭原则。根据这个原则,我们不能修改原有代码,但是我们可以进行扩展。

所以我们可以把计算面积的方法放到Shape类中,再由每个继承它的子类自己去实现自己的计算方法。这样就不用修改原有的代码了。

里氏替换原则

里氏替换原则是由Barbara Liskov在1987年的“数据抽象“大会上提出的。Barbara Liskov和Jeannette Wing在1994年发表了论文对这一原则进行阐述:

如果φ(x)是类型T的属性,并且S是T的子类型,那么φ(y)就是S的属性。

Let φ(x) be a property provable about objects x of type T. Then φ(y) should be true for objects y of type S where S is a subtype of T.

Barbara Liskov给出了易于理解的版本,但是这一版本更依赖于类型系统:

1. Preconditions cannot be strengthened in a subtype.

2. Postconditions cannot be weakened in a subtype.

3. Invariants of the supertype must be preserved in a subtype.

Robert Martin在1996年提出了更加简洁、通顺的定义:

使用指向基类指针的函数也可以使用子类。

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

更简单一点讲就是子类可以替代父类。

根据里氏替换原则,我们可以在接受抽象类(接口)的任何地方用它的子类(实现类)来替代它们。基本上,我们应该注意在编程时不能只关注接口的输入参数,还需要保证接口实现类的返回值都是同一类型的。

下面这段代码就违反了里氏替换原则:

<?php
interface LessonRepositoryInterface
{
/**
* Fetch all records.
*
* @return array
*/
public function getAll();
}
class FileLessonRepository implements LessonRepositoryInterface
{
public function getAll()
{
// return through file system
return [];
}
}
class DbLessonRepository implements LessonRepositoryInterface
{
public function getAll()
{
/*
Violates LSP because:
- the return type is different
- the consumer of this subclass and FileLessonRepository won't work identically
*/
// return Lesson::all();
// to fix this
return Lesson::all()->toArray();
}
}

译者注:这里没想到Java应该怎么实现,因此直接用了作者的代码,大家理解就好

接口隔离原则

不能强制客户端实现它不使用的接口。

A client should not be forced to implement an interface that it doesn’t use.

这个规则告诉我们,应该把接口拆的尽可能小。这样才能更好的满足客户的确切需求。

与单一责任原则类似,接口隔离原则也是通过将软件拆分为多个独立的部分来最大程度的减少副作用和重复代码。

我们来看一个例子:

public interface WorkerInterface {

    void work();
void sleep();
} public class HumanWorker implements WorkerInterface { public void work() {
System.out.println("work");
}
public void sleep() {
System.out.println("sleep");
}
} public class RobotWorker implements WorkerInterface { public void work() {
System.out.println("work");
}
public void sleep() {
// No need
}
}

在上面这段代码中,我们很容易发现问题所在,机器人不需要睡觉,但是由于实现了WorkerInterface接口,它不得不实现sleep方法。这就违背了接口隔离的原则,下面我们一起修复一下这段代码:

public interface WorkAbleInterface {

    void work();
} public interface SleepAbleInterface { void sleep();
} public class HumanWorker implements WorkAbleInterface, SleepAbleInterface { public void work() {
System.out.println("work");
}
public void sleep() {
System.out.println("sleep");
}
} public class RobotWorker implements WorkerInterface { public void work() {
System.out.println("work");
}
}

依赖倒置原则

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

抽象不应该依赖于细节,细节应该依赖于抽象。

High-level modules should not depend on low-level modules. Both should depend on abstractions.

Abstractions should not depend on details. Details should depend on abstractions.

简单来讲就是:抽象不依赖于细节,而细节依赖于抽象。

通过应用依赖倒置模块,只需要修改依赖模块,其他模块就可以轻松得到修改。同时,低层模块的修改是不会影响到高层模块修改的。

我们来看这段代码:

public class MySQLConnection {

    public void connect() {
System.out.println("MYSQL Connection");
}
} public class PasswordReminder { private MySQLConnection mySQLConnection; public PasswordReminder(MySQLConnection mySQLConnection) {
this.mySQLConnection = mySQLConnection;
}
}

有一种常见的误解是,依赖倒置只是依赖注入的另一种表达方式,实际上两者并不相同。

在上面这段代码中,尽管将MySQLConnection类注入了PasswordReminder类,但它依赖于MySQLConnection。而高层模块PasswordReminder是不应该依赖于低层模块MySQLConnection的。因此这不符合依赖倒置原则。

如果你想要把MySQLConnection改成MongoConnection,那就要在PasswordReminder中更改硬编码的构造函数注入。

要想符合依赖倒置原则,PasswordReminder就要依赖于抽象类(接口)而不是细节。那么应该怎么改这段代码呢?我们一起来看一下:

public interface ConnectionInterface {

    void connect();
} public class MySQLConnection implements ConnectionInterface { public void connect() {
System.out.println("MYSQL Connection");
}
} public class PasswordReminder { private ConnectionInterface connection; public PasswordReminder(ConnectionInterface connection) {
this.connection = connection;
}
}

修改后的代码中,如果我们想要将MySQLConnection改成MongoConnection,就不需要修改PasswordReminder类的构造函数注入,因为这里PasswordReminder类依赖于抽象而非细节。

感谢阅读!

原文地址

https://medium.com/better-programming/solid-principles-simple-and-easy-explanation-f57d86c47a7f

译者点评

作者对于SOLID原则介绍的还是比较清楚的,但是里氏原则那里我认为说得还不是很明白,举的例子似乎也不是很明确。我理解的里氏替换原则是:子类可以扩展父类的功能,但不能修改父类方法。因此里氏替换原则可以说是开闭原则的一种实现。当然,这篇文章也只是大概介绍了SOLID的每个原则,大家可以通过查资料来进行更详细的了解。我相信理解了这些设计原则之后,你对程序设计就会有更加深入的认识。后面我也会继续推送一些关于设计原则的文章,欢迎关注。

【译】浅谈SOLID原则的更多相关文章

  1. 浅谈 SOLID 原则的具体使用

    SOLID 是面向对象设计5大重要原则的首字母缩写,当我们设计类和模块时,遵守 SOLID 原则可以让软件更加健壮和稳定.那么,什么是 SOLID 原则呢?本篇文章我将谈谈 SOLID 原则在软件开发 ...

  2. 浅谈 SOLID 原则

    单一职责原则(SRP) 单一职责原则(SRP)表明一个类有且只有一个职责.一个类就像容器一样,它能添加任意数量的属性.方法等.然而,如果你试图让一个类实现太多,很快这个类就会变得笨重.任意小的改变都将 ...

  3. 在net中json序列化与反序列化 面向对象六大原则 (第一篇) 一步一步带你了解linq to Object 10分钟浅谈泛型协变与逆变

    在net中json序列化与反序列化   准备好饮料,我们一起来玩玩JSON,什么是Json:一种数据表示形式,JSON:JavaScript Object Notation对象表示法 Json语法规则 ...

  4. c#Winform程序调用app.config文件配置数据库连接字符串 SQL Server文章目录 浅谈SQL Server中统计对于查询的影响 有关索引的DMV SQL Server中的执行引擎入门 【译】表变量和临时表的比较 对于表列数据类型选择的一点思考 SQL Server复制入门(一)----复制简介 操作系统中的进程与线程

    c#Winform程序调用app.config文件配置数据库连接字符串 你新建winform项目的时候,会有一个app.config的配置文件,写在里面的<connectionStrings n ...

  5. [译]开发者须知的SOLID原则

    原文:SOLID Principles every Developer Should Know – Bits and Pieces SOLID Principles every devloper sh ...

  6. 浅谈开源项目Android-Universal-Image-Loader(Part 3.1)

    本文转载于:http://www.cnblogs.com/osmondy/p/3266023.html 浅谈开源项目Android-Universal-Image-Loader(Part 3.1) 最 ...

  7. 【ASP.NET MVC系列】浅谈表单和HTML辅助方法

    [01]浅谈Google Chrome浏览器(理论篇) [02]浅谈Google Chrome浏览器(操作篇)(上) [03]浅谈Google Chrome浏览器(操作篇)(下) [04]浅谈ASP. ...

  8. 浅谈RESTful

    浅谈RESTful 什么是RESTful? REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移. 它首次出现在2000年Ro ...

  9. 浅谈SQL注入风险 - 一个Login拿下Server

    前两天,带着学生们学习了简单的ASP.NET MVC,通过ADO.NET方式连接数据库,实现增删改查. 可能有一部分学生提前预习过,在我写登录SQL的时候,他们鄙视我说:“老师你这SQL有注入,随便都 ...

随机推荐

  1. [学习笔记] [数据分析] 01.Python入门

    1.安装Python与环境配置 ① ② 安装pip以及利用pip安装Python库 2.Anaconda安装 conda list 要在root环境下 3.常用数据分析库 ① Numpy 安装:con ...

  2. yarn和npm的对比以及yarn的使用

    0--前言 为什么要使用yarn,如果你从事前端开发有些年头了,那你肯定对npm又爱又恨,爱就不说了,恨嘛,就是NPM经常奇慢和卡顿,这还能忍,经常各种错误就没法忍了,尤其是他人创建的项目,自己在安装 ...

  3. Spring面试题集锦(精选)

    以下来自网络收集,找不到原文出处.此次主要为了面试收集,希望对大家有所帮助~~~~ 1.什么是Spring? Spring是一个开源的Java EE开发框架.Spring框架的核心功能可以应用在任何J ...

  4. 企业DevOps研发模式下CI/CD实践详解指南

    阅读全文大概需要 10分钟. 1. 前言 借着公司今年新组建的中台研发部东风,我作为其中的主要负责人,在研发中心主导推行DevOps研发管理模式转变及质量管理创新建设,本篇文章摘取自今年9月底,笔者在 ...

  5. 微信小程序——e.target与e.currentTarget的区别

    在小程序的点击事件中,我们经常使用这两个属性来传参,看起来效果一样,查了官方文档如下: target:事件源组件对象 currentTarget:当前组件对象 什么意思?我刚开始就有点不懂,那就直接上 ...

  6. luogu CF16E Fish

    题目描述 有n条鱼,编号从1到n,住在湖里.每天有一对鱼相遇, 彼此相遇的概率是一样的.如果两条标号为i和j的鱼见面,第一只吃了第二只的概率为a{i,j},第二只会吃了第一只的概率为a{j,i}=1- ...

  7. HDU2242 考研路茫茫——空调教室 (双联通分+树形DP)

    考研路茫茫——空调教室 Time Limit: 4000/2000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total ...

  8. 2019 AI Bootcamp Guangzhou 参会日记

    2019年的全球AI训练营在北京.上海.广州.杭州.宁波五个地方同时举办! 12月14日,微软全球AI Bootcamp活动再次驾临广州,本次会议结合 ML.NET 和基于 SciSharp 社区介绍 ...

  9. Local Model Poisoning Attacks to Byzantine-Robust Federated Learning

    In federated learning, multiple client devices jointly learn a machine learning model: each client d ...

  10. js(对象,入口函数,函数)

    对象: 创建: var myObject = {};/* 声明对象字面变量*/ 添加值: myObject.name="Jener"; myObject.age=25; 代码格式: ...