七种设计原则(二)单一职责原则

1.定义html

单一职责原则概念::规定一个类应该只有一个发生变化的缘由。编程

There should never be more than one reason for a class to change.
 spa

2.单一职责理解设计

从单一职责原则概念,咱们将职责定义为一个变化的因素,如可这个类有多个变化因素,这个类就违背了单一职责原则。code

在设计类的时候须要将不一样的职责分离到单独的类中。一个类只须要专注实现自身的职责,Do one thing and do it well。专一能保证对象的高内聚和细粒度,有利于对象的重用。htm

 

3.问题和解决方案:对象

例1blog

类T负责两个不一样的职责:职责P1、职责P2。当因为职责P1需求发生改变而须要修改类T时,有可能会致使原来运行的职责P2功能发生故障。解决方法:分别创建两个类完成对应的功能。接口

只要作过项目,确定要接触到用户、机构、角色管理这些模块,基本上使用的都是RBAC模型,确实是很好的一个解决办法。咱们今天要讲的是用户管理、修改用户的信息、增长机构(一我的属于多个机构)、增长角色等,用户有这么的信息和行为要维护,咱们就把这些写到一个接口中,都是用户管理类嘛,咱们先来看它的类图:游戏

clip_image002

太Easy的类图了,    这个接口设计得有问题,用户的属性(Property)和用户的行为(Behavior)没有分开,这是一个严重的错误!很是正确,这个接口确实设计得一团糟,应该把用户的信息抽取成一个BO(Bussiness Object,业务对象),把行为抽取成一个BIZ(Business Logic,业务逻辑),按照这个思路对类图进行修正,以下图:

clip_image004

  从新拆封成两个接口,IUserBO负责用户的属性,简单地说,IUserBO的职责就是收集和反馈用户的属性信息;IUserBiz负责用户的行为,完成用户信息的维护和变动。各位可能要说了,这个与我实际工做中用到的User类仍是有差异的呀!别着急,咱们先来看一看分拆成两个接口怎么使用。OK,咱们如今是面向接口编程,因此产生了这个UserInfo对象以后,固然能够把它当IUserBO接口使用。固然,也能够当IUserBiz接口使用,这要看你在什么地方使用了。要得到用户信息,就当是IUserBO的实现类;要是但愿维护用户的信息,就把它看成IUserBiz的实现类就成了,代码清单1-1所示。

IUserBiz userInfo = new UserInfo();
 
//我要赋值了,我就认为它是一个纯粹的BO
 
IUserBO userBO = (IUserBO)userInfo;
 
userBO.setPassword("abc");
 
//我要执行动做了,我就认为是一个业务逻辑类
 
IUserBiz userBiz = (IUserBiz)userInfo;
 
userBiz.deleteUser();
 
.......

  确实能够如此,问题也解决了,可是咱们来回想一下咱们刚才的动做,为何要把一个接口拆分红两个呢?其实,在实际的使用中,咱们更倾向于使用两个不一样的类或接口:一个是IUserBO, 一个是IUserBiz,类图应该以下图所示。

clip_image006

上图就是项目中经常使用的SPR(There should never be more than one reason for a class to change)类图;

  以上咱们把一个接口拆分红两个接口的动做,就是依赖了单一职责原则,那什么是单一职责原则呢?单一职责原则的定义是:应该有且仅有一个缘由引发类的变动。

  实现类也比较简单,我就再也不写了,你们看看这个接口有没有问题?我相信大部分的读者都会说这个没有问题呀,之前我就是这么作的呀,某某书上也是这么写的呀,还有什么什么的源码也是这么写的!是的,这个接口接近于完美,看清楚了,是“接近”!单一职责原则要求一个接口或类只有一个缘由引发变化,也就是一个接口或类只有一个职责,它就负责一件事情,看看上面的接口只负责一件事情吗?是只有一个缘由引发变化吗?好像不是!

     IPhone这个接口可不是只有一个职责,它包含了两个职责:一个是协议管理,一个是数据传送。diag()和huangup()两个方法实现的是协议管理,分别负责拨号接通和挂机;chat()和answer()是数据的传送,把咱们说的话转换成模拟信号或数字信号传递到对方,而后再把对方传递过来的信号还原成咱们听得懂语言。咱们能够这样考虑这个问题,协议接通的变化会引发这个接口或实现类的变化吗?会的!那数据传送(想一想看,电话不只仅能够通话,还能够上网)的变化会引发这个接口或实现类的变化吗?会的!那就很简单了,这里有两个缘由都引发了类的变化,并且这两个职责会相互影响吗?电话拨号,我只要能接通就成,甭管是电信的仍是网通的协议;电话链接后还关心传递的是什么数据吗?不关心,你要是乐意使用56K的小猫传递一个高清的片子,那也没有问题(顶多有人说你13了)。经过这样的分析,咱们发现类图上的IPhone接口包含了两个职责,并且这两个职责的变化不相互影响,那就考虑拆开成两个接口,其类图以下图所示。

clip_image010

这个类图看着有点复杂了,彻底知足了单一职责原则的要求,每一个接口职责分明,结构清晰,可是我相信你在设计的时候确定不会采用这种方式,一个手机类要把ConnectionManager和DataTransfer组合在一块才能使用。组合是一种强耦合关系,你和我都有共同的生命期,这样的强耦合关系还不如使用接口实现的方式呢,并且还增长了类的复杂性,多了两个类。通过这样的思考后,咱们再修改一下类图,以下图所示。

clip_image012

简洁清晰、职责分明的电话类图

  这样的设计才是完美的,一个类实现了两个接口,把两个职责融合在一个类中。你会以为这个Phone有两个缘由引发变化了呀,是的是的,可是别忘记了咱们是面向接口编程,咱们对外公布的是接口而不是实现类。并且,若是真要实现类的单一职责,这个就必须使用上面的组合模式了,这会引发类间耦合太重、类的数量增长等问题,人为的增长了设计的复杂性。

     看过电话这个例子后,是否是有点反思了,我之前的设计是否是有点的问题了?不,不是的,不要怀疑本身的技术能力,单一职责原则最难划分的就是职责。一个职责一个接口,但问题是“职责”是一个没有量化的标准,一个类到底要负责那些职责?这些职责该怎么细化?细化后是否都要有一个接口或类?这些都须要从实际的项目去考虑,从功能上来讲,定义一个IPhone接口也没有错,实现了电话的功能,并且设计还很简单,仅仅一个接口一个实现类,实际的项目我想你们都会这么设计。项目要考虑可变因素和不可变因素,以及相关的收益成本比率,所以设计一个IPhone接口也多是没有错的。可是,若是纯从“学究”理论上分析就有问题了,有两个能够变化的缘由放到了一个接口中,这就为之后的变化带来了风险。若是之后模拟电话升级到数字电话,咱们提供的接口IPhone是否是要修改了?接口修改对其余的Invoker类是否是有很大影响?!

     注意 单一职责原则提出了一个编写程序的标准,用“职责”或“变化缘由”来衡量接口或类设计得是否有优良,可是“职责”和“变化缘由”都是不可度量的,因项目而异,因环境而异。

例2

再好比:生产手机

假定如今有以下场景:国际手机运营商那里定义了生产手机必需要实现的接口,接口里面定义了一些手机的属性和行为,手机生产商若是要生产手机,必需要实现这些接口。

咱们首先以手机做为单一职责去设计接口,方案以下。

  /// <summary>
    /// 充电电源类
    /// </summary>
    public class ElectricSource
    {    }
public interface IMobilePhone
    {
        //运行内存
        string RAM { get; set; }

        //手机存储内存
        string ROM { get; set; }

        //CPU主频
        string CPU { get; set; }

        //屏幕大小
        int Size { get; set; }

        //手机充电接口
        void Charging(ElectricSource oElectricsource);

        //打电话
        void RingUp();

        //接电话
        void ReceiveUp();

        //上网
        void SurfInternet();
    }

而后咱们的手机生产商去实现这些接口

//具体的手机示例
    public class MobilePhone:IMobilePhone
    {
        public string RAM
        {
            get {throw new NotImplementedException();}
            set{ throw new NotImplementedException();}
        }

        public string ROM
        {
            get{throw new NotImplementedException();}
            set{ throw new NotImplementedException();}
        }

        public string CPU
        {
            get{ throw new NotImplementedException();}
            set{ throw new NotImplementedException();}
        }

        public int Size
        {
            get{throw new NotImplementedException();}
            set{throw new NotImplementedException();}
        }

        public void Charging(ElectricSource oElectricsource)
       {
            throw new NotImplementedException();
        }

        public void RingUp()
        {
            throw new NotImplementedException();
        }

        public void ReceiveUp()
        {
            throw new NotImplementedException();
        }

        public void SurfInternet()
        {
            throw new NotImplementedException();
        }
    }

这种设计有没有问题呢?这是一个颇有争议的话题。单一职责原则要求一个接口或类只有一个缘由引发变化,也就是一个接口或类只有一个职责,它就负责一件事情,原则上来讲,咱们以手机做为单一职责去设计,也是有必定的道理的,由于咱们接口里面都是定义的手机相关属性和行为,引发接口变化的缘由只多是手机的属性或者行为发生变化,从这方面考虑,这种设计是有它的合理性的,若是你能保证需求不会变化或者变化的可能性比较小,那么这种设计就是合理的。但实际状况咱们知道,现代科技突飞猛进,科技的进步促使着人们不断在手机原有基础上增长新的属性和功能。好比有一天,咱们给手机增长了摄像头,那么须要新增一个像素的属性,咱们的接口和实现就得改吧,又有一天,咱们增长移动办公的功能,那么咱们的接口实现是否是也得改。因为上面的设计没有细化到必定的粒度,致使任何一个细小的改动都会引发从上到下的变化,有一种“牵一发而动全身”的感受。因此须要细化粒度,下面来看看咱们如何变动设计。

二次设计 变动:

咱们将接口细化

 //手机属性接口
    public interface IMobilePhoneProperty
    {
        //运行内存
        string RAM { get; set; }

        //手机存储内存
        string ROM { get; set; }

        //CPU主频
        string CPU { get; set; }

        //屏幕大小
        int Size { get; set; }

        //摄像头像素
        string Pixel { get; set; }
    }

    //手机功能接口
    public interface IMobilePhoneFunction
    {
        //手机充电接口
        void Charging(ElectricSource oElectricsource);

        //打电话
        void RingUp();

        //接电话
        void ReceiveUp();

        //上网
        void SurfInternet();

        //移动办公
        void MobileOA();
    }

实现类

//手机属性实现类
    public class MobileProperty:IMobilePhoneProperty
    {

        public string RAM
        {
            get{ throw new NotImplementedException();}
            set{ throw new NotImplementedException();}
        }

        public string ROM
        {
            get{ throw new NotImplementedException();}
            set{ throw new NotImplementedException();}
        }

        public string CPU
        {
            get{ throw new NotImplementedException();}
            set{throw new NotImplementedException();}
        }

        public int Size
        {
            get{throw new NotImplementedException();}
            set{throw new NotImplementedException();}
        }

        public string Pixel
        {
            get{throw new NotImplementedException();}
            set{throw new NotImplementedException();}
        }
    }

    //手机功能实现类
    public class MobileFunction:IMobilePhoneFunction
    {

        public void Charging(ElectricSource oElectricsource)
        {
            throw new NotImplementedException();
        }

        public void RingUp()
        {
            throw new NotImplementedException();
        }

        public void ReceiveUp()
        {
            throw new NotImplementedException();
        }

        public void SurfInternet()
        {
            throw new NotImplementedException();
        }

        public void MobileOA()
        {
            throw new NotImplementedException();
        }
    }

    //具体的手机实例
    public class HuaweiMobile
    {
        private IMobilePhoneProperty m_Property;
        private IMobilePhoneFunction m_Func;
        public HuaweiMobile(IMobilePhoneProperty oProperty, IMobilePhoneFunction oFunc)
        {
            m_Property = oProperty;
            m_Func = oFunc;
        }
    }

对于上面题的问题,这种设计可以比较方便的解决,若是是增长属性,只须要修改IMobilePhoneProperty和MobileProperty便可;若是是增长功能,只须要修改IMobilePhoneFunction和MobileFunction便可。貌似完胜第一种解决方案。那么是否这种解决方案就完美了呢?答案仍是看状况。原则上,咱们将手机的属性和功能分开了,使得职责更加明确,全部的属性都由IMobilePhoneProperty接口负责,全部的功能都由IMobilePhoneFunction接口负责,若是是需求的粒度仅仅到了属性和功能这一级,这种设计确实是比较好的。反之,若是粒度再细小一些呢,那咱们这种职责划分是否完美呢?好比咱们普通的老人机只须要一些最基础的功能,好比它只须要充电、打电话、接电话的功能,可是按照上面的设计,它也要实现IMobilePhoneFunction接口,某一天,咱们增长了一个新的功能玩游戏,那么咱们就须要在接口上面增长一个方法PlayGame()。但是咱们老人机根本用不着实现这个功能,但是因为它实现了该接口,它的内部实现也得从新去写。从这点来讲,以上的设计仍是存在它的问题。那么,咱们如何继续细化接口粒度呢?

最终设计

接口细化粒度设计以下

//手机基础属性接口
    public interface IMobilePhoneBaseProperty
    {
        //运行内存
        string RAM { get; set; }

        //手机存储内存
        string ROM { get; set; }

        //CPU主频
        string CPU { get; set; }

        //屏幕大小
        int Size { get; set; }
    }

    //手机扩展属性接口
    public interface IMobilePhoneExtentionProperty
    {
        //摄像头像素
        string Pixel { get; set; }
    }

    //手机基础功能接口
    public interface IMobilePhoneBaseFunc
    {
        //手机充电接口
        void Charging(ElectricSource oElectricsource);

        //打电话
        void RingUp();

        //接电话
        void ReceiveUp();
    }

    //手机扩展功能接口
    public interface IMobilePhoneExtentionFunc
    {
        //上网
        void SurfInternet();

        //移动办公
        void MobileOA();

        //玩游戏
        void PlayGame();
    }

实现类和上面相似    

//手机基础属性实现
    public class MobilePhoneBaseProperty : IMobilePhoneBaseProperty
    {

        public string RAM
        {
            get{throw new NotImplementedException();}
            set{throw new NotImplementedException();}
        }

        public string ROM
        {
            get{throw new NotImplementedException();}
            set {throw new NotImplementedException();}
        }

        public string CPU
        {
            get{throw new NotImplementedException();}
            set{ throw new NotImplementedException();}
        }

        public int Size
        {
            get{ throw new NotImplementedException();}
            set{ throw new NotImplementedException();}
        }
    }

    //手机扩展属性实现
    public class MobilePhoneExtentionProperty : IMobilePhoneExtentionProperty
    {

        public string Pixel
        {
            get{ throw new NotImplementedException();}
            set{ throw new NotImplementedException();}
        }
    }

    //手机基础功能实现
    public class MobilePhoneBaseFunc : IMobilePhoneBaseFunc
    {
        public void Charging(ElectricSource oElectricsource)
        {
            throw new NotImplementedException();
        }

        public void RingUp()
        {
            throw new NotImplementedException();
        }

        public void ReceiveUp()
        {
            throw new NotImplementedException();
        }
    }

    //手机扩展功能实现
    public class MobilePhoneExtentionFunc : IMobilePhoneExtentionFunc
    {

        public void SurfInternet()
        {
            throw new NotImplementedException();
        }

        public void MobileOA()
        {
            throw new NotImplementedException();
        }

        public void PlayGame()
        {
            throw new NotImplementedException();
        }
    }

    此种设计能解决上述问题,细分到此粒度,这种方案基本算比较完善了。能不能算完美?这个得另说。接口的粒度要设计到哪一步,取决于需求的变动程度,或者说取决于需求的复杂度

        由于每个职责都是变化的中心。当需求变动时,这个变化将经过更改职责相关的类来实现。若是一个类拥有多个职责,那么这个类在变动的时候可能会影响到其余的类,产生没法预期的破坏。因此单一职责原则有利于对象的稳定,让多个对象负责各自的职责,而后对象之间进行协做要比一个对象负责多个职责强的多,方法之间也是这样。


4.优势:

类的复杂性下降,实现什么职责都有清晰明确的定义;

可读性提升,复杂性下降,那固然可读性提升了;

可维护性提升,那固然了,可读性提升,那固然更容易维护了;

变动引发的风险下降,变动是必不可少的,若是接口的单一职责作得好,一个接口修改只对相应的实现类有影响,对其余的接口无影响,这对系统的扩展性、维护性都有很是大帮助。

 

5.难点职责的划分

类的设计尽可能作到只有一个缘由引发变化。单一职责原则是一个很是简单的原则,但一般也是最难作的正确的一个原则。职责的联合是在实践中常常碰到的事情。理论是理论,实践是实践。要考虑相关因素和收益等。

 

6.总结 :

以上经过一个应用场景简单介绍了下单一职责原则的使用,类的设计尽可能作到只有一个缘由引发变化。上面三种设计,没有最合理,只有最合适。理解单一职责原则,最重要的就是理解职责的划分,职责划分的粒度取决于需求的粒度,没有最好的设计,只有最适合的设计。

Do one thing,and do it well~.

参考连接:

https://zhuanlan.zhihu.com/p/24198903

https://www.cnblogs.com/cbf4life/archive/2009/12/11/1622166.html

相关文章
相关标签/搜索