【译】浅谈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的每个原则,大家可以通过查资料来进行更详细的了解。我相信理解了这些设计原则之后,你对程序设计就会有更加深入的认识。后面我也会继续推送一些关于设计原则的文章,欢迎关注。

原文地址:https://www.cnblogs.com/Jackeyzhe/p/11992711.html

时间: 2024-07-30 11:11:23

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

浅谈设计原则和设计模式

文章结构: 1.前言 2.设计原则       3.设计模式 3.1 创建型模式 3.2 结构型模式 3.3 行为型模式 前言 设计原则和设计模式旨在帮助我们设计出一个可复用.可扩展.可维护的应用. 设计原则:设计OR重构系统的指导方针. 设计模式:解决某类问题性质有效的方法. 设计原则和设计模式要实现的目标是:在需求变动或者系统升级时,尽可能少的改变代码,尽可能多的实现新的功能. 设计原则是设计模式的"背后的故事",要深入理解设计模式必先深入理解设计原则. 设计原则 1.开闭原则(O

蚂蚁变大象:浅谈常规网站是如何从小变大的(六)(转)

原文:http://blog.sina.com.cn/s/blog_6203dcd60100xvky.html          [第十阶段 : 数据存储优化]   在前面的阶段中,我们都使用数据库作为默认的存储引擎,很少谈论关于关于数据存储的话题.但是,数据的存储却是我们现在众多大型网站面临的最核心的问题.现在众多网络巨头纷纷推出自己的"高端"存储引擎,也吸引了众多的眼球.比如:google的BigTable.facebook的cassandra.以及开源的Hadoop等等.国内众多

浅谈CSS优先级机制(一)

初次写随笔,如果有哪个地方不足还望大神指点改正,下面我来谈谈我对于CSS优先级的了解吧. CSS优先级,通俗的理解就是你给元素等一堆属性描述,然后最后到底是哪个描述作为最终显示的效果的规则或机制(个人理解).以下我将分为几个点来谈谈优先级的确定. 1.引入方式: CSS引入的方式,我目前只知道四种:内联式.内嵌式.导入式.链接式(当然网上的说法名称不一,理解就好). 各种引入方式的用法我在这里就不再多说了.以上我所按顺序罗列的四个方式是理论上的优先级顺序,也就是说,我使用内联式引入的css代码作

浅谈Android多屏幕的事

浅谈Android多屏幕的事 一部手机可以同时看片.聊天,还可以腾出一支手来撸!这么吊的功能(非N版本,非第三方也能实现,你不知道吧)摆在你面前,你不享用?不关注它是怎样实现的?你来,我就满足你的欲望! 一部手机可以同时看片.聊天,还可以腾出一支手来撸==!就像这样: 是时候告别来回切换应用屏幕的酸爽了,还可以在分屏模式下两Activity间直接拖放数据! 好高大上的样子!这是怎么实现的?别急,我们一一道来: kitkat(4.4)版本对多任务分屏的实现 由于相关的代码和功能被封装及隐藏起来,所

浅谈知识管理

工欲善其事,必先利其器 推荐使用为知笔记(WizNote),它是电脑.手机.平板上都能用的云笔记软件,还可以分类管理和共享资料! 使用我的邀请码注册 前言 在做项目,解决某些需求的时候,总会用到自己不熟悉的模块和技术,这时候就会各种谷歌百度查手册,查询完之后,实现功能需求,过一段时间之后,就又忘记当时是如何实现的了. 这时你会怎么做?是又去网上查找一遍?还是说通过之前的个人知识管理,即时抓取.快速检索该知识? 浅谈知识管理(以自己为例) 熟话说:“好记性不如烂笔头”,但是在这个信息爆棚的时代,充

浅谈Android五大布局

Android的界面是有布局和组件协同完成的,布局好比是建筑里的框架,而组件则相当于建筑里的砖瓦.组件按照布局的要求依次排列,就组成了用户所看见的界面.Android的五大布局分别是LinearLayout(线性布局).FrameLayout(单帧布局).RelativeLayout(相对布局).AbsoluteLayout(绝对布局)和TableLayout(表格布局). LinearLayout: LinearLayout按照垂直或者水平的顺序依次排列子元素,每一个子元素都位于前一个元素之后

浅谈asp.net通过本机cookie仿百度(google)实现搜索input框自动弹出搜索提示

对于通过用户输入关键词实现自动弹出相关搜索结果,这里本人给两种解决方案,用于两种不同的情形. 常见方法是在数据库里建一个用户搜索关系表,然后通过用户搜索框输入的关键字异步调用数据表中的相关数据,显示在一个隐藏div中. 第二种方式也就是我现在着重讨论的方式,适用于单个用户,基于此用户以往的搜索数据来实现搜索提示功能.技术关键是记录下用户的以往搜索数据,写入cookie,然后页面从用户本机cookie调用数据. ok,下面进入正题.本文主要讲实现步骤,代码可根据自己实际需要更改. 一,如何写入co

管理从砖瓦进化为人——浅谈传统软件工程到敏捷软件开发之变革

管理从砖瓦进化为人 --浅谈传统软件工程到敏捷软件开发之变革 前言 如果把软件开发过程比作修筑一座建筑的话,传统的软件工程方法对人的管理就像是把人化作一砖一瓦,秩序地堆砌,一层一层构建起摩天大厦. 显然地,人是不同于砖瓦那样的死物的.人作为一种复杂的动物,软件开发者会有喜怒哀乐,枯燥重复的工作内容会使他们提不起兴趣而缺乏激情:客户想法会随变动的现实而一天天有所转变,软件需求很难保持一成不变:开发者与测试者对于项目的认识会存在差异,而差异将导致效率的降低--因而传统的有些"反人类天性"的

浅谈设计模式的学习(中)

在<浅谈设计模式的学习(上)>中我说到了设计模式的基石-----抽象思维.为什么需要抽象思维呢?因为越抽象就越不容易出错,就像有些领导人说话:坚持改革开放.但怎么算坚持改革开放呢,没有具体的标准,因事而异,所以就不容易违背这个坚持改革开放的原则了. 3.学习设计模式,要保持抽象的思维     什么是抽象思维呢?真的不好说,抽象的东西往往难以说明白,听了也难以搞明白,还是通过具体的例子来说吧 有这么一个学生请假的场景,如果请假时间一天以内则有班长批准就可以了,三天以内则需要老师批准,超过三天就得