【译】浅谈SOLID原则
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原则的更多相关文章
- 浅谈 SOLID 原则的具体使用
SOLID 是面向对象设计5大重要原则的首字母缩写,当我们设计类和模块时,遵守 SOLID 原则可以让软件更加健壮和稳定.那么,什么是 SOLID 原则呢?本篇文章我将谈谈 SOLID 原则在软件开发 ...
- 浅谈 SOLID 原则
单一职责原则(SRP) 单一职责原则(SRP)表明一个类有且只有一个职责.一个类就像容器一样,它能添加任意数量的属性.方法等.然而,如果你试图让一个类实现太多,很快这个类就会变得笨重.任意小的改变都将 ...
- 在net中json序列化与反序列化 面向对象六大原则 (第一篇) 一步一步带你了解linq to Object 10分钟浅谈泛型协变与逆变
在net中json序列化与反序列化 准备好饮料,我们一起来玩玩JSON,什么是Json:一种数据表示形式,JSON:JavaScript Object Notation对象表示法 Json语法规则 ...
- c#Winform程序调用app.config文件配置数据库连接字符串 SQL Server文章目录 浅谈SQL Server中统计对于查询的影响 有关索引的DMV SQL Server中的执行引擎入门 【译】表变量和临时表的比较 对于表列数据类型选择的一点思考 SQL Server复制入门(一)----复制简介 操作系统中的进程与线程
c#Winform程序调用app.config文件配置数据库连接字符串 你新建winform项目的时候,会有一个app.config的配置文件,写在里面的<connectionStrings n ...
- [译]开发者须知的SOLID原则
原文:SOLID Principles every Developer Should Know – Bits and Pieces SOLID Principles every devloper sh ...
- 浅谈开源项目Android-Universal-Image-Loader(Part 3.1)
本文转载于:http://www.cnblogs.com/osmondy/p/3266023.html 浅谈开源项目Android-Universal-Image-Loader(Part 3.1) 最 ...
- 【ASP.NET MVC系列】浅谈表单和HTML辅助方法
[01]浅谈Google Chrome浏览器(理论篇) [02]浅谈Google Chrome浏览器(操作篇)(上) [03]浅谈Google Chrome浏览器(操作篇)(下) [04]浅谈ASP. ...
- 浅谈RESTful
浅谈RESTful 什么是RESTful? REST全称是Representational State Transfer,中文意思是表述(编者注:通常译为表征)性状态转移. 它首次出现在2000年Ro ...
- 浅谈SQL注入风险 - 一个Login拿下Server
前两天,带着学生们学习了简单的ASP.NET MVC,通过ADO.NET方式连接数据库,实现增删改查. 可能有一部分学生提前预习过,在我写登录SQL的时候,他们鄙视我说:“老师你这SQL有注入,随便都 ...
随机推荐
- 第三章 学习Shader所需的数学基础(2)
目录 1.坐标空间 1.2 坐标空间的变换 @ 1.坐标空间 我们在以前渲染流水线中就接触了坐标空间的变换.例如,在学习顶点着色器流水线阶段时,我们说过,顶点着色器的最基本功能就是把模型的顶点坐标从模 ...
- 在Linux下生成crypt加密密码
[摘要]当我们用红帽Kickstart脚本或useradd或其他方式写东西的时候,经常会需要用到crypt命令加密生成的密码格式.那么,有没有其他方式可以生成这种格式的密码?事实上,方法有很多 1.我 ...
- iOS开发-Apple Pay-苹果支付
转自:http://www.open-open.com/lib/view/open1471952847228.html Apple Pay,是苹果公司在2014年苹果秋季新品发布会上发布的一种基于NF ...
- MVC效验器
步骤一:导入依赖 <!--数据效验--> <dependency> <groupId>org.hibernate</groupId> <artif ...
- HDU5919 Sequence II(主席树)
Mr. Frog has an integer sequence of length n, which can be denoted as a1,a2,⋯,ana1,a2,⋯,anThere are ...
- CoderForces-913D
You are preparing for an exam on scheduling theory. The exam will last for exactly Tmilliseconds and ...
- cl创建opencv程序
环境 win8 VS2017或VS2013 opencv 3.2.0 配制环境变量 解压opencv到某个目录,比如D:\Program\Uninstall,把设置OPENCV_HOME为D:\Pro ...
- synchronized,ReentrantLock解决锁冲突,脏读的问题
最常见的秒杀系统,解决思路就是从前端.后台服务.数据库层层去掉负载,以达到平衡 锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLo ...
- Python3 函数小练习
函数小练习 第一题 现有文件info.txt, 其内容如下: alpha male 18 1000 bravo male 28 2000 charlie female 38 3000 delta fe ...
- 基于 .NET Core 的简单文件服务器
Netnr.FileServer 基于 .NET Core 的简单文件服务器,数据库为SQLite 源码 https://github.com/netnr/blog https://gitee.com ...