单一职责原则(SRP)--深度剖析--面向对象设计(OOD)

单一职责原则(SRP)--深度剖析--面向对象设计(OOD)单一职责就是指一个类应该专注于做一件事。现实生活中也存在诸如此类的问题:“一个人可能身兼数职,甚至于这些职责彼此关系不大,那么他可能无法做好所有职责内的事情,所以,还是专人专管比较好。”我们在设计类的时候,就应该遵循这个单一职责原则。

    记得有人比喻过软件开发、设计原则、设计模式之间的关系就是战争、战略和战术的关系,关于设计模式实际上是设计原则的具体应用,以后我们还会讲到这一点。另外,大家都很熟悉计算器的例子,很多的人都愿意以此为例,我们也以计算器编程为例说明单一职责原则: 
在有些人眼里,计算器就是一件东西,是一个整体,所以它把这个需求进行了抽象,最终设计为一个Calculator类,代码如下:

class Calculator{ 
public String calculate() {

Console.Write("Please input the first number:"); 
String strNum1 = Console.ReadLine(); 
Console.Write(Please input the operator:"); 
String strOpr= Console.ReadLine();

Console.Write("Please input the second number:"); 
String strNum2 = Console.ReadLine();

String strResult = ""; 
if (strOpr == "+"){ 
strResult = Convert.ToString(Convert.ToDouble(strNum1) + Convert.ToDouble(strNum2)); 

else if (strOpr == "-"){ 
strResult = Convert.ToString(Convert.ToDouble(strNum1) – Convert.ToDouble(strNum2)); 

else if (strOpr == "*"){ 
strResult = Convert.ToString(Convert.ToDouble(strNum1) * Convert.ToDouble(strNum2)); 

else if (strOpr == "/"){ 
strResult = Convert.ToString(Convert.ToDouble(strNum1) / Convert.ToDouble(strNum2)); 
}

Console.WriteLine("The result is " + strResult); 

}

另外,还有一部分人认为:计算器是一个外壳和一个处理器的组合。

class Appearance{ 
public int displayInput(String &strNum1,String &strOpr, String &strNum2) { 
Console.Write("Please input the first number:"); 
strNum1 = Console.ReadLine(); 
Console.Write(Please input the operator:"); 
strOpr= Console.ReadLine();

Console.Write("Please input the second number:"); 
strNum2 = Console.ReadLine();

return 0; 
}

public String displayOutput(String strResult) { 
Console.WriteLine("The result is " + strResult); 

}

class Processor{ 
public String calculate(String strNum1,String strOpr, String strNum2){ 
String strResult = ""; 
if (strOpr == "+"){ 
strResult = Convert.ToString(Convert.ToDouble(strNum1) + Convert.ToDouble(strNum2)); 

else if (strOpr == "-"){ 
strResult = Convert.ToString(Convert.ToDouble(strNum1) – Convert.ToDouble(strNum2)); 

else if (strOpr == "*"){ 
strResult = Convert.ToString(Convert.ToDouble(strNum1) * Convert.ToDouble(strNum2)); 

else if (strOpr == "/"){ 
strResult = Convert.ToString(Convert.ToDouble(strNum1) / Convert.ToDouble(strNum2)); 

return strResult; 

}

为什么这么做呢?因为外壳和处理器是两个职责,是两件事情,而且都是很容易发生需求变动的因素,所以把它们放到一个类中,违背了单一职责原则。 
比如,用户可能对计算器提出以下要求: 
第一,目前已经实现了“加法”、“减法”、“乘法”和“除法”,以后还可能出现“乘方”、“开方”等很多运算。 
第二,现在人机界面太简单了,还可能做个Windows计算器风格的界面或者Mac计算器风格的界面。 
所以,把一个类Calculator 拆分为两个类Appearance和Processor,一个类做一件事情,这样更容易应对需求变化。如果界面需要修改,那么就去修改Appearance类;如果处理器需要修改,那么就去修改Processor类。

我们再举一个邮件的例子。我们平常收到的邮件内容,看起来是一封信,实际上内部有两部分组成:邮件头和邮件体。电子邮件的编码要求符合RFC822标准。 
第一种设计方式是这样: 
interface IEmail { 
public void setSender(String sender); 
public void setReceiver(String receiver); 
public void setContent(String content); 
}

class Email implements IEmail { 
public void setSender(String sender) {// set sender; } 
public void setReceiver(String receiver) {// set receiver; } 
public void setContent(String content) {// set content; } 
}

这个设计是有问题的,因为邮件头和邮件体都有变化的可能性。 
1、邮件头的每一个域的编码,可能是BASE64,也可能是QP,而且域的数量也不固定。 
2、邮件体中封装的邮件内容可能是PlainText类型,也可能是HTML类型,甚至于流媒体。 
所谓第一种设计方式违背了单一职责原则,里面封装了两种可能引起变化的原因。 
我们依照单一职责原则,对其进行改进后,变为第二种设计方式: 
interface IEmail { 
public void setSender(String sender); 
public void setReceiver(String receiver); 
public void setContent(IContent content); 
}

interface IContent { 
public String getAsString(); 
}

class Email implements IEmail { 
public void setSender(String sender) {// set sender; } 
public void setReceiver(String receiver) {// set receiver; } 
public void setContent(IContent content) {// set content; } 
}

有的资料把单一职责解释为:“仅有一个引起它变化的原因”。这个解释跟“专注于做一件事”是等价的。如果一个类同时做两件事情,那么这两件事情都有可能引起它的变化。同样的道理,如果仅有一个引起它变化的原因,那么这个类也就只能做一件事情。

单一职责原则的使用

    单一职责原则的尺度如何掌握?我们怎么能知道该拆分还是不应该拆分呢?原则很简单:需求决定。如果你所需要的计算器,永远都没有外观和处理器变动的可能性,那么就应该把它抽象为一个整体的计算器;如果你所需要的计算器,外壳和处理器都有可能发生变动,那么就必须把它拆离为外壳和处理器。

    单一职责原则实际上是把相同的职责进行了聚合,避免把相同的职责分散到不同的类之中,这样就可以控制变化,把变化限制在一个地方,防止因为一个地方的变动,引起更多地方的变动的“涟漪效应”,单一职责原则避免一个类承担过多的职责。单一职责原则不是说一个类就只有一个方法,而是具有单一功能

    我们在使用单一职责原则的时候,牢记以下几点

A、一个设计合理的类,应该仅有一个可以引起它变化的原因,即单一职责,如果有多个原因可以引起它的变化,就必须进行分离; 
B、在没有需求变化征兆的情况下,应用单一职责原则或其他原则是不明智的,因为这样会使系统变得很复杂,系统将会变成一堆细小的颗粒组成,纯属于没事找抽; 
C、在需求能够预计或实际发生变化时,就应该使用单一职责原则来重构代码,有经验的设计师、架构师对可能出现的需求变化很敏感,设计上就会具有一定的前瞻性。单一职责原则(SRP)--深度剖析--面向对象设计(OOD)

定义:

  单一职责(SRP,Single Responsibility Principle)强调的是职责的分离,在某种程度上对职责的理解,构成了不同类之间耦合关系的设计关键,因此单一职责原则或多或少成为设计过程中一个必须考虑的基础性原则。

核心思想:

      一个类,最好只做一件事情,只有一个引起它变化的原因。

  单一职责原则可以看做是低耦合,高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。职责过多,可能引起它变化的原因就越多,这将导致依赖,相互之间就产生影响,从而极大的损伤其内聚性和耦合性。单一职责,通常意味着单一的功能,因此不要为类实现过多的功能点,以保证实体只有一个引起它变化的原因。

重构方法:

  对于违反这一个原则的类应该进行重构,例如以Façade模式或Proxy模式分离职责,通过基本方法Extract Interface ,Extract Class 和Extract Method进行梳理。

案例分析:

  MIS 系统中根据不同的权限进行数据增删改查的系统比比皆是,然而这正是一个比较常见的一种需求场景,先看设计图:

图2-2职责分离的设计

  以上设计,DBManager类将对数据库的操作和用户权限的判断封装在一个类中去实现,看小段代码:

        public void Add()        {              if(GetPermission(id) == "CanAdd")            {                 Insert();// 可以添加数据 ,数据库操作逻辑代码            }        }

  现在GetPermission(string  Id)权限设置规则发生变化, 那么Insert() 数据库操作逻辑全部要修改,必须首先将这种代码扫地清理掉。

  按照单一职责原则,一个类只有一个引起它变化的原因,下面我们选择合适的方法进行重构有缺陷的设计,在此我们使用Proxy模式来解决类太累的问题,重构之后的设计图:

图2-2职责分离的设计

  以Proxy模式重构之后,有效的实现了职责分离,DBManager专注于执行数据库的操作,下面来看修改之后的代码:

public class DBManager:IDAO    {        public void Add()        {            throw new NotImplementedException();        }         public void View()        {            throw new NotImplementedException();        }         public void Delete()        {            throw new NotImplementedException();        }    }

  而DBManagerProxy只需关注于数据库逻辑的操作:

public class DBManagerProxy:IDAO    {        private IDAO dbdaoInstance;         public DBManagerProxy(IDAO dbdao)        {            dbdaoInstance = dbdao;        }         string GetParmession(string id)        {            // 判断逻辑             return "Add";        }          public void Add()        {            if (GetParmession("Add")=="ADD")            {               // 权限操作            }        }         public void View()        {            throw new NotImplementedException();        }         public void Delete()        {            throw new NotImplementedException();        }    }

  小结:

  在实例中IDAO 体现了设计模式中的重要原则:依赖倒置原则(DIP),通过DBManagerProxy代理类实现了职责分离,DBManager仅有一个引起变化的原因,就是数据操作的需求变更,而权限的变更和修改不对DBManager 造成任何影响,体现了单一职责原则。

  适用场景:

  一个类只有一个引起它变化的原因,否则就应当考虑重构。

  SRP由引起变化的原因决定,而不由功能决定。虽然职责常常引起变化的轴线,但有时就未必,应该适当考虑。

  可以通过Facade模式或Proxy模式进行职责分离。

  个人心得和大家共勉:

  在实际的项目过程中间要不断的去重构自己的代码,平时多看好书,不过现在好书不多(市场上也就那么几本),要多去思考书中作者写的每一个示例,举一反三,会逐步的提高自己的设计能力和分析能力。


    单一职责原则(Single Responsibility Principle)

定义:不要存在多于一个导致类变更的原因。通俗的说,即一个类只负责一项职责。
问题由来:类T负责两个不同的职责:职责P1,职责P2。当由于职责P1需求发生改变而需要修改类T时,有可能会导致原本运行正常的职责P2功能发生故障。
解决方案:遵循单一职责原则。分别建立两个类T1、T2,使T1完成职责P1功能,T2完成职责P2功能。这样,当修改类T1时,不会使职责P2发生故障风险;同理,当修改T2时,也不会使职责P1发生故障风险。
         说到单一职责原则,很多人都会不屑一顾。因为它太简单了。稍有经验的程序员即使从来没有读过设计模式、从来没有听说过单一职责原则,在设计软件时也会自觉的遵守这一重要原则,因为这是常识。在软件编程中,谁也不希望因为修改了一个功能导致其他的功能发生故障。而避免出现这一问题的方法便是遵循单一职责原则。虽然单一职责原则如此简单,并且被认为是常识,但是即便是经验丰富的程序员写出的程序,也会有违背这一原则的代码存在。为什么会出现这种现象呢?因为有职责扩散。所谓职责扩散,就是因为某种原因,职责P被分化为粒度更细的职责P1和P2。
         比如:类T只负责一个职责P,这样设计是符合单一职责原则的。后来由于某种原因,也许是需求变更了,也许是程序的设计者境界提高了,需要将职责P细分为粒度更细的职责P1,P2,这时如果要使程序遵循单一职责原则,需要将类T也分解为两个类T1和T2,分别负责P1、P2两个职责。但是在程序已经写好的情况下,这样做简直太费时间了。所以,简单的修改类T,用它来负责两个职责是一个比较不错的选择,虽然这样做有悖于单一职责原则。
举例说明,用一个类描述动物呼吸这个场景:
class Animal{
    public void breathe(String animal){
       System.out.println(animal+"呼吸空气");
    }
}

public class Client{
    public static void main(String[] args){
       Animal animal = new Animal();
       animal.breathe("牛");
       animal.breathe("羊");
       animal.breathe("猪");
    }
}
运行结果:
牛呼吸空气
羊呼吸空气
猪呼吸空气

程序上线后,发现问题了,并不是所有的动物都呼吸空气的,比如鱼就是呼吸水的。修改时如果遵循单一职责原则,需要将Animal类细分为陆生动物类Terrestrial,水生动物Aquatic,代码如下:
class Terrestrial{
    public void breathe(String animal){
       System.out.println(animal+"呼吸空气");
    }
}
class Aquatic{
    public void breathe(String animal){
       System.out.println(animal+"呼吸水");
    }
}

public class Client{
    public static void main(String[] args){
       Terrestrial terrestrial = new Terrestrial();
       terrestrial.breathe("牛");
       terrestrial.breathe("羊");
       terrestrial.breathe("猪");
     
       Aquatic aquatic = new Aquatic();
       aquatic.breathe("鱼");
    }
}
运行结果:
牛呼吸空气
羊呼吸空气
猪呼吸空气
鱼呼吸水

我们会发现如果这样修改花销是很大的,除了将原来的类分解之外,还需要修改客户端。而直接修改类Animal来达成目的虽然违背了单一职责原则,但花销却小的多,代码如下:
class Animal{
    public void breathe(String animal){
       if("鱼".equals(animal)){
           System.out.println(animal+"呼吸水");
       }else{
           System.out.println(animal+"呼吸空气");
       }
    }
}

public class Client{
    public static void main(String[] args){
       Animal animal = new Animal();
       animal.breathe("牛");
       animal.breathe("羊");
       animal.breathe("猪");
       animal.breathe("鱼");
    }
}
可以看到,这种修改方式要简单的多。但是却存在着隐患:有一天需要将鱼分为呼吸淡水的鱼和呼吸海水的鱼,则又需要修改Animal类的breathe方法,而对原有代码的修改会对调用“猪”“牛”“羊”等相关功能带来风险,也许某一天你会发现程序运行的结果变为“牛呼吸水”了。这种修改方式直接在代码级别上违背了单一职责原则,虽然修改起来最简单,但隐患却是最大的。还有一种修改方式:
class Animal{
    public void breathe(String animal){
       System.out.println(animal+"呼吸空气");
    }

    public void breathe2(String animal){
       System.out.println(animal+"呼吸水");
    }
}

public class Client{
    public static void main(String[] args){
       Animal animal = new Animal();
       animal.breathe("牛");
       animal.breathe("羊");
       animal.breathe("猪");
       animal.breathe2("鱼");
    }
}
可以看到,这种修改方式没有改动原来的方法,而是在类中新加了一个方法,这样虽然也违背了单一职责原则,但在方法级别上却是符合单一职责原则的,因为它并没有动原来方法的代码。这三种方式各有优缺点,那么在实际编程中,采用哪一中呢?其实这真的比较难说,需要根据实际情况来确定。我的原则是:只有逻辑足够简单,才可以在代码级别上违反单一职责原则;只有类中方法数量足够少,才可以在方法级别上违反单一职责原则;
    例如本文所举的这个例子,它太简单了,它只有一个方法,所以,无论是在代码级别上违反单一职责原则,还是在方法级别上违反,都不会造成太大的影响。实际应用中的类都要复杂的多,一旦发生职责扩散而需要修改类时,除非这个类本身非常简单,否则还是遵循单一职责原则的好。
         遵循单一职责原的优点有:
l   可以降低类的复杂度,一个类只负责一项职责,其逻辑肯定要比负责多项职责简单的多;
l   提高类的可读性,提高系统的可维护性;
l   变更引起的风险降低,变更是必然的,如果单一职责原则遵守的好,当修改一个功能时,可以显著降低对其他功能的影响。
需要说明的一点是单一职责原则不只是面向对象编程思想所特有的,只要是模块化的程序设计,都需要遵循这一重要原则。

 


我们知道,在面向对象设计中要做到高内聚低耦合。而单一职责原则就是实现高内聚低耦合的最好办法。面向对象设计中单一职责原则是指:
    一个类只负责一个功能领域中的相应职责。
    如果一个类承担的职责过多,就等于把这些职责耦合在了一起。当其中一个职责变化时,可能影响其他职责的运作。
    下面我们用C++的例子来具体说明。
    比如我们有如下的设计:

class  CShape
{
public :
    
virtual   ~ CShape();
    
virtual   void  Draw()  =   0 ;
    
virtual   double  GetArea()  =   0 ;
};

class  CSquare :  public  CShape
{
public :
    
void  Draw();
    
double  GetArea();
        
    
void  SetWidth( double  dWidth);
    
double  GetWidth();

private :
    
double  m_dWidth;
};

    现在有两个不同的应用程序用到了类CSquare,一个是有关几何计算方面的,另一个是有关图形方面的。对于前者而说,程序从来不需要绘制图形;而对于后者来说,程序也从来不需要计算图形的面积。
    在上面这种情况下,我们的设计就违反了单一职责原则。它即提供了几何计算方面的功能,又提供了图形绘制方面的功能。这样,在有关几何计算方面的应用程序中就要链接图形显示方面的库文件;而在有关图形方面的应用程序中却链接了数学计算方面的库文件。而这些多余的链接其实是不必要的。它们不但会使编译、链接的时间变长,而且会使应用程序占用的内存增加。如果我们对图形的显示代码做了修改,那么有关几何计算方面的应用程序就要重新链接。我们为什么要为自己不需要的功能重新链接自己的程序呢?因此,上面的设计是不正确的。
    下面是一个符合单一职责原则的设计。在这个设计中,把原来的类CShape分为两个类:CGeometricShape和CGraphicalShape,来分别承担几何和图形两方面的职责。同样,分别派生出GGeometricSquare和CGraphicalSquare。

class  CGeometricShape
{
public :
    
virtual   ~ CGeometricShape();
    
virtual   double  GetArea()  =   0 ;
};

class  GGeometricSquare :  public  CGeometricShape
{
public :
    
double  GetArea();

    
void  SetWidth( double  dWidth);
    
double  GetWidth();

private :
    
double  m_dWidth;
};

class  CGraphicalShape
{
public :
    
virtual   ~ CGraphicalShape();
    
virtual   void  Draw()  =   0 ;
};

class  CGraphicalSquare :  public  CGraphicalShape
{
public :
    
void  Draw();

    
void  SetWidth( double  dWidth);
    
double  GetWidth();

private :
    
double  m_dWidth;
};

    这样,有关几何计算方面的应用程序和有关图形方面的应用程序之间就没有一点儿耦合了。不仅应用程序中不会链接多余的代码,而且对其中的任意一个类进行修改都不会影响到另外一个应用程序。





单一职责原则(Single Responsibility Principle),简称SRP。
定义:
There should never be more than one reason for a class to change.
应该有且仅有一个原因引起类的变更。
 
有时候,开发人员设计接口的时候会有些问题,比如用户的属性和用户的行为被放在一个接口中声明。这就造成了业务对象和业务逻辑被放在了一起,这样就造成了这个接口有两种职责,接口职责不明确,按照SRP的定义就违背了接口的单一职责原则了。
下面是个例子:

package com.loulijun.chapter1;
 
public interface Itutu {
    //身高
    void setShengao(double height);
    double getShengao();
    //体重
    void setTizhong(double weight);
    double getTizhong();
    //吃饭
    boolean chiFan(boolean hungry);
    //上网
    boolean shangWang(boolean silly);
}
  上面的例子就存在这个问题,身高、体重属于业务对象,与之相应的方法主要负责用户的属性。而吃饭、上网是相应的业务逻辑,主要负责用户的行为。但是这就会给人一种不知道这个接口到底是做什么的感觉,职责不清晰,后期维护的时候也会造成各种各样的问题。
解决办法:单一职责原则,将这个接口分解成两个职责不同的接口即可
ItutuBO.java:负责tutu(涂涂,假如是个人名)的属性

package com.loulijun.chapter1;
 

public interface ItutuBO {
    //身高
    void setShengao(double height);
    double getShengao();
    //体重
    void setTizhong(double weight);
    double getTizhong();
}
ItutuBL.java:负责涂涂的行为

package com.loulijun.chapter1;

public interface ItutuBL {
    //吃饭
    boolean chiFan(boolean hungry);
    //上网
    boolean shangWang(boolean silly);
}
这样就实现了接口的单一职责。那么实现接口的时候,就需要有两个不同的类
TutuBO.java

package com.loulijun.chapter1;
 
public class TutuBO implements ItutuBO {
    private double height;
    private double weight;
    @Override
    public double getShengao() {        
        return height;
    }
 
    @Override
    public double getTizhong() {
        return weight;
    }
 
    @Override
    public void setShengao(double height) {
        this.height = height;
    }
 
    @Override
    public void setTizhong(double weight) {
        this.weight = weight;
    }
 
}
TutuBL.java

package com.loulijun.chapter1;
 
public class TutuBL implements ItutuBL {
 
    @Override
    public boolean chiFan(boolean hungry) {
        if(hungry)
        {
            System.out.println("去吃火锅...");
            return true;
        }
        return false;
    }
 
    @Override
    public boolean shangWang(boolean silly) {
        if(silly)
        {
            System.out.println("好无聊啊,上会网...");
            return true;
        }
        return false;
    }
 
}
这样就清晰了,当需要修改用户属性的时候只需要对ItutuBO这个接口来修改,只会影响到TutuBO这个类,不会影响其他类。
那么单一职责原则的意义何在呢?
1、降低类的复杂性,实现什么样的职责都有清晰的定义
2、提高可读性
3、提高可维护性
4、降低变更引起的风险,对系统扩展性和维护性很有帮助
 
但是、使用单一职责原则有一个问题,“职责”没有一个明确的划分标准,如果把职责划分的太细的话会导致接口和实现类的数量剧增,反而提高了复杂度,降低了代码的可维护性。所以使用这个职责的时候还要具体情况具体分析。建议就是接口一定要采用单一职责原则,实现类的设计上尽可能做到单一职责原则,最好是一个原因引起一个类的变化。



 

认识单一职责原则

         单一职责原则是面向对象设计中最重要的原则之一,而面向对象最基础的东西就是类和对象的使用,而单一职责可以说是对类和对象的一种要求,也就是要求类应该有且仅有一个引起它变化的原因。

      开闭原则是指一个类,只有一个引起它变化的原因。有且只有一个职责。每一个职责都是变化的一个轴线,如果一个类有一个以上的职责,这些职责就耦合在了一起。这会导致脆弱的设计。当一个职责发生变化时,可能会影响其它的职责。另外,多个职责耦合在一起,会影响复用性。

 

单一职责原则由来

         谈到单一职责原则的由来不得不说面向对象,也不得不说需求是在不断变化,因为软件需求唯一的不变的真理就是软件需求一定会变化,因为需求变化,所以我们就要用到面向对象的设计思想,而面向对象的要求就是复用、能用最小的代价应对变化、不用改变现有代码就能满足扩展(其实这就上一篇博客中说的《开闭原则》)。在面向对象的要求下,许多人在这些问题上思考了很多,也花费了很大努力来实现这几点,所以单一职责原则就在这样的背景下诞生了。

 

单一职责原则的好处

      ① 单一职责原则提供了一个编写程序的标准,也就是让类的复杂性降低,实现什么职责都有清晰明确的定义

      ② 在可读性和可维护性上得到了提高,因为类的职责单一,对类的阅读,类之间的调用关系都是清晰明确的。

 

项目中分析单一职责原则

         在这里引用一个比较经典的例子,就是图形计算程序和图形绘制程序调用Area()方法和Draw()方法的例子。    

      例子出处《开篇-模式和原则》:

     

      图形计算程序只使用了正方形的Area()方法,永远没有使用Draw()方法,而它却和draw方法关联起来,这就违反了单一指责原则,如果将来有一天图形绘制程序导致draw发生变化,那就影响到了本来毫无关系的图形计算程序。

      所以我们应该将不同的职责分配给不同的类,让每个类的职责单一,隔离变化

常言道

      常言道:该你管的你管,不该你管的你别插手。

你可能感兴趣的:(单一职责原则(SRP)--深度剖析--面向对象设计(OOD))