单一职责原则 开放封闭原则 依赖倒转原则

​ 单一职责原则:

就一个类而言,应该仅有一个引起它变化的原因。

通俗一点来说,一个类应该只做一类事情;一个类应该只负责一个功能。

单一职责原则是程序设计高内聚、低耦合的引申。

如果一个类承担的职责过多,就等于把这些职责耦合在一起,一个职责的变化可能会削弱或者抑制在这个类完成其他职责的能力。这种耦合会导致脆弱的设计,当变化发生时,设计会遭受到意想不到的破坏。

当然,软件设计真正要做的许多内容,就是发现职责并把那些职责相互分离。其实要去判断是否应该分离出类来,也不难,那就是如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多于一个的职责,就应该考虑类的职责分离。

所以,这里要强调一点的是,单一职责原则是一个类处理一类事情,也只有一类事情影响到这个类。并不是一个类处理一个方法。

开始理解

我们来举个例子。作者眼前有一支黑色笔,拿这支笔来做例子吧。

笔有它属性、被动的行为。属性有:黑色、手感好、笔尖0.5。行为有:写字、画画、扎人。

按照一般的做法,我们都把笔的属性与行为都放在一个类来做。单一职责现在就起作用了,我们要分开成属性与行为。

首先按照一般的设计:

一般一个接口、一个类来处理笔的各种事情,上面的图充分解析了这种说法,也是平常人设计的类。

下面,我们按照单一职责原则,来设计笔的属性、行为来隔离开,如下:

从上图看出,我们把行为以及属性动作分离开。

举了这个例子,我们看出一件事情,可以分离开多件事情的处理,从而提高了软件设计的高内聚、低耦合。

但是,我们从上面的例子,可以在分一下笔的行为。因为画画、写字是笔在纸上做的动作,而扎人是笔在人上做的动作。

更通俗地说,就是画画这个动作要发生改变的话,扎人这个动作应该不会受影响,所以扎人应该放到另外一个类中做处理。

下面我们可以更深一层地理解一下这个笔的动作可以分解成2个。

4)更深一层地理解

按照上面说的,我们可以把笔的动作分成两种,一种在纸上做的,一种是在人上做的。

为什么可以分开这两种,因为上面说了,一种在纸上做,一种在人上做。在纸上做只影响在纸张的类,在人上做只影响在人生的类。互不干扰。

开闭原则

1)概念

官方说法是 软件实体(模块、类、函数等)应该可以扩展,但是不可以修改。也就是说软件对扩展开放,对修改关闭。

需要说明的是,对修改关闭不是说软件设计不能做修改,只是尽量不要做不必要的修改。怎么才能做到呢?那就是有相应的扩展性。

其实,软件有相应的扩展性是好处,但是不能说每个地方都有扩展。反而造成了代码的臃肿。所以这里的扩展与修改关闭是有限制的。

2)深入理解

比如我们平常喝水用的一次性纸杯。平常人只是用来装水。喝完水就扔了。这就是这个纸杯的生命周期。纸杯这一生只完成了它的一个功能:装水。纸杯此时就很封闭了,没有什么扩展性。

此时,我看到身边有一支花苗,我想要拿回家种。但是没有容器呀? 啊?旁边不是有一个纸杯吗,可以用此纸杯来种这朵花苗。

纸杯有了它的另外一个扩展性,就是种花苗。

纸杯不仅有装水、种花苗的用途,以后还可以有装小垃圾、冲茶、回收等功能。对于以后这些功能,我们要想到他们的扩张性。

在纸杯只有一个装水的功能的时候,我们只写一个纸杯功能类,说纸杯能装水。但是以后有扩展呢?这一方面我们要预先判断。预先判断它以后可能会根据需求的变动而扩展。对于纸杯本来的装水功能,不能说不能修改,此功能只能在此函数、类中修改。这就是开闭原则的核心。

所以,纸杯在开闭原则所体现的是:尽量少修改,未来可能扩展的模块、类做好预算的判断。如果要修改,只能在此函数此类修改,不能牵涉到其他地方。

当纸杯只有一个功能,装水时。有一个纸杯操作接口,有一个纸杯操作实现类。

当我们要添加一个功能 种花苗时,我们不也是加一个方法吗?如下:

当添加N个方法时,不也是在纸杯的操作接口上面添加N个方法吗?

我们想一想,此时已经背离了我们的开闭原则。因为每添加一个方法,都要在操作类上面做修改。所以,我们按照开闭原则,开做了以下合理的设计:

从上面可以看出,我们把纸杯的操作类,统一写成一个接口,每个不同的操作继承此接口来完成各自操作。我们还开到多了一个类,叫客户端类,其实此类也不难理解。也就是要最终操作纸杯的类。

3)其他例子

开闭原则其实在大话设计模式中说得非常好,让人通俗易懂。

它举了一个例子,我觉得说得非常好。是加减乘除法的例子。

开始需求是做一个加法的操作。后来继续加入减法、乘法、除法。

开始我们想加法以后可能会做一个需求变更:加入其它的算法法则。所以我们要有一个预判性,这个预判性会导致我们项目以后的扩展性,也会导致如果需求发生变更,程序修改的难易程度。

所以,我们要做一个算法法则的操作类,加减乘除法都继承此操作接口。再加一个算法法则的客户端类类操作此算法。

我们来上一下大话设计模式中的图:

依赖倒置原则

1)概念

a.高层模块不应该依赖于底层模块,两者应该依赖于其抽象。

b.抽象不应该依赖具体实现,具体实现应该依赖抽象。

上面2点是依赖倒置原则的概念,也是核心。主要是说模块之间不要依赖具体实现,依赖接口或抽象。

其实依赖倒置原则的核心思想是面向接口编程。

举例:

比如,学生看语文书。我们就实现这两个类,一个是学生,一个是语文书。学生依赖于语文书,所以语文书是学生的一个属性。

new一个语文的对象,然后new一个学生对象,最后把语文对象赋值到学生的book中。完成了学生看书。

但是,这是不好扩展的,抛离了面向对象的低耦合,高内聚的思想。因为,如果,我们要添加学生看英语书,那怎么添加呢?

首先,在Student类中添加方法DoSomething(EnglishBook book); 然后再实现EnghlishBook类的LookEnglishBook方法。

最后调用,变成:

// JAVA
static void Main(string[] args)
        {
            YuWenBook book = new YuWenBook();
            Student student = new Student();
            student.DoSomething(book);

            //学生看英语书
            EnglishBook englishBook = new EnglishBook();
            student.DoSomething(englishBook);

        }

一看,就知道不好扩展。因为已经修改了Student类中代码(增加了方法)。怎么才算好扩展呢,那么要聊起与开闭原则有关系了。

对修改关闭,对扩展开放。所以我们尽量的不要修改Student类中的代码。所以,我们增加一个接口,叫IBook:

public interface IBook
    {
        void LookBook();
    }

然后,英语、语文各自继承IBook:

    public class EnglishBook: IBook
    {
        public void LookBook()
        {
            Console.WriteLine("看英语书");
        }
    }

    public class YuWenBook:IBook
    {
        public void LookBook()
        {
            Console.WriteLine("看语文书");
        }
    }

再修改一下Student的方法,接收IBook参数,变成:

public class Student
    {
        public void DoSomething(IBook book)
        {
            book.LookBook();
        }

    }

调用

static void Main(string[] args)
        {
            IBook book = new YuWenBook();
            Student student = new Student();
            student.DoSomething(book);

            //学生看英语书
            book = new EnglishBook();
            student.DoSomething(book);

        }

看看,是不是可以很方面的扩展? 为什么这样说呢?

因为当我们再添加一个学生看数学书的时候,我们只需要添加一个数学书类来继承IBook,然后实现LookBook方法。最后我们用IBook对象来调用数学书类就可以了。可以看出,Student不用做任何修改,IBook也是。只需做相应的实现与调用即可。

来源整理自:vue3js.cn 面试官系列