译:面向对象设计原则(Object Oriented Design Principles)

面向对象设计原则
Object Oriented Design Principles

原文:http://www.codeproject.com/Articles/567768/Object-Oriented-Design-Principles java

做者:Marla Sukesh 程序员

 ——文记:本文翻译纯属兴趣,若有雷同,不胜荣幸 数据库


谁适合对这篇文章? 编程

这篇文章适合对面向对象编程有个基本的概念的程序员。至少能区分类和对象,以及能谈论一些面向对象编程的基本主题,如:封装,抽象,多态和继承。  设计模式

简介 架构

在面向对象的世界里咱们只看对象,对象相互之间交互。类,对象,继承,多态,抽象等术语是咱们平常工做中耳熟能详的词汇。 编程语言

在当今软件开发行业每个软件开发者都使用某种类别的面向对象编程语言,可是问题是,是否每个人都真正理解了面向对象编程的的含义?是否知道本身正在使用面向对象编程?若是答案是YES,那么你真正发挥了面向对象编程的做用吗? ide

这篇文章咱们除了讨论基本的面向对象编程,还将讨论面向对象设计。 函数

面向对象设计 测试

在软件系统中一系列计划经过对象之间的相互交互来解决特定的问题。俗话说:适当的使用面向对象设计会使得开发者很轻松,然而糟糕的设计会带来灾难性的损失。

从何开始

当开发者开始建立一个软件架构时,他们的目标每每是很好的。他们用已有的经验创造出优雅而干净的设计。 

随着时间的推移,软件开始变得糟糕。当每个新特性要求被注入或改变软件设计来改变其形态,最终一些简单的变动却须要更多的努力,更糟糕的是为Bug创造了更多的机会。

这是谁的错?

软件解决现实生活中的业务问题,因为业务一直在发展,因此软件也一直在改变。

变动是软件行业必不可少的一部分。显然客户花了钱是为了能知足他们所指望的需求。因此咱们不能因为变动而抱怨退化的软件设计。而是因为咱们的设计还不够强大。

对破坏软件设计最大的因素是在系统中引入了未考虑到的依赖关系。每个系统模块依赖其余的模块,当修改一个模块时会影响到其它模块。若是咱们能很好的管理这些依赖关系,那维护这个软件系统将变得很容易,而且能保证软件的质量。

例如: 

解决方案:原则,设计模式,软件架构

  • 软件架构 好比MVC模式,三层架构,MVP,这些告诉咱们如何结构化整个项目
  • 设计模式 让咱们能够重用之前的经验,甚至更多,它提供可重用的方案解决常见的问题。好比:对象的建立,实例管理,等等。
  • 原则 他告诉咱们,要作什么你就要现实什么。至于你怎么作取决于你本身。每一个人都有本身的一些原则,好比某些人说:我从不说谎,我从不喝酒,等等。遵循这些原则会使他们的生活更轻松,可是如何去坚持这些原则则取决于你本身。

同时,面向对象设计也须要听从一些原则,它使咱们经过软件设计来管理问题。

Mr Robert Martin (众所周知的Bob大叔)将这些原则归类以下:

  1. 类设计原则—也叫SOLID
  2. 包类聚原则
  3. 包耦合原则

本文将经过一些实用的例子来介绍类设计原则:SOLID。

SOLID

J5原则是Bob大叔总结出的五个原则的简称,单一职责原则,开闭原则,里氏替换原则,接口隔离原则和依赖倒置原则。维基百科上面说当将这五个应用到一块儿使得程序员能创造出一个可维护,可扩展的系统。下面详细介绍每个原则。

I) S - SRP - Single responsibility Principle(单一职责原则) 

现实生活

假设我是一个印度软件公司里面的小组leader,在业余时间我从事写做,新闻编辑,还作着其余不一样的项目。简单的说,我身兼多职。

当某些很差的事情发生在个人工做场所里时,好比老板由于某些错误责骂我,或者我被其余工做干扰。基本上,当一件事情变得糟糕时,全部事情都陷入困境。

发现问题

在咱们讨论这个原则以前先看看下面的代码 


  • 每次变动插入逻辑时都要修改此类
  • 每次变动报告格式时也要修改此类





问题出在哪里?

每次当一个发生改变时其余的也会跟随着改变,缘由是他们都住在同一个屋子里,拥有相同的父母。咱们没法控制一切。因此一个改变致使双倍的测试,甚至更多。

什么是单一职责原则?(SRP)

SRP:每个软件模块只有一个能引发其改变的缘由

  • 软件模块:类,方法,等等。
  • 变化的缘由:职责

不违反SRP原则的方法

如今看咱们怎样去实现他。咱们将建立3个不一样的类

  1. Employee — 包含了数据属性
  2. EmployeeDB — 数据库操做
  3. EmployeeReport — 报表相关的操做 
public class Employee
{
    public string EmployeeName { get; set; }
    public int EmployeeNo { get; set; }
}
public class EmployeeDB
{
    public void Insert(Employee e) 
    {
        //Database Logic written here
    }
 public Employee Select() 
    {
        //Database Logic written here
    }
}
public class EmployeeReport
{
    public void GenerateReport(Employee e)
    {
        //Set report formatting
    }
}

NOTE:单一职责原则一样适用于方法,每个方法应该只干一件事。

类能够有多个方法吗?

答案是确定的。可是你可能会问:

  1. 一个类只有一个职责
  2. 一个方法只有一个职责
  3. 一个类有多个方法

固然这个问题的答案很是简单。那就是从总体上下文看待问题。这里,职责是跟咱们所说的上下文是相关的。说到类的职责时每每是从更高层次上看待的。对于实例,好比EmployeeDB类主要的职责是对employee数据库相关的操做,然而EmployeeReport则是对员工报表相关的操做。

当咱们说到方法的职责时那就是较低层次上。看下面的例子: 

//Method with multiple responsibilities – violating SRP
public void Insert(Employee e)
{     
    string StrConnectionString = "";       
    SqlConnection objCon = new SqlConnection(StrConnectionString); 
    SqlParameter[] SomeParameters=null;//Create Parameter array from values
    SqlCommand objCommand = new SqlCommand("InertQuery", objCon);
    objCommand.Parameters.AddRange(SomeParameters);
    ObjCommand.ExecuteNonQuery();
}

//Method with single responsibility – follow SRP
public void Insert(Employee e)
{            
    SqlConnection objCon = GetConnection();
    SqlParameter[] SomeParameters=GetParameters();
    SqlCommand ObjCommand = GetCommand(objCon,"InertQuery",SomeParameters);
    ObjCommand.ExecuteNonQuery();
}

private SqlCommand GetCommand(SqlConnection objCon, string InsertQuery, SqlParameter[] SomeParameters)
{
    SqlCommand objCommand = new SqlCommand(InsertQuery, objCon);
    objCommand.Parameters.AddRange(SomeParameters);
    return objCommand;
}

private SqlParameter[] GetParaeters()
{
    //Create Paramter array from values
}

private SqlConnection GetConnection()
{
    string StrConnectionString = "";
    return new SqlConnection(StrConnectionString);
}

测试自己是有利的,使得代码可读性强是另外一个好处。代码可读性越强就越简单易懂。


II) O - OCP – Open Close Principle(开闭原则)

现实生活 

假设你想要在图中第一层和第二层之间在加一层楼。你以为这个可能吗?固然可能,可是可行吗?

这里有一些方法:

  • 第一种方法是事先在盖这栋房子时将他盖成3层。第二层空着,而后在你想要的时候利用第二层。我不知道这是否可行,但的确是一个方法。
  • 第二种方法是你把如今存在的第二层拆掉,而后再盖两层新的。但这个方法貌似很不明智。

发现问题

咱们假设EmployeeDB的Select方法会被两个不一样的场景调用。一个是普通的员工调用,一个是管理员调用,可是管理员调用的Select方法须要修改。 

若是咱们修改现有Select方法来知足新的需求,原来老的代码逻辑将会受到影响。同时还要改变现有的测试方案,这可能会致使意想不到的Bug。

什么是OCP?

OCP:软件模块应该对修改关闭,对扩展开放。一个正交的声明。

不违反OCP的解决方案

1)使用继承

建立一个EmployeeManagerDB继承EmployeeDB类,而且根据新的需求重写select方法 

public class EmployeeDB
{      
    public virtual Employee Select()
    {
        //Old Select Method
    }
}
public class EmployeeManagerDB : EmployeeDB
{
    public override Employee Select()
    {
        //Select method as per Manager
        //UI requirement
    }
}

NOTE:若是这是预期的设计而且提供了可扩展的虚方法,那么能够称得上是良好的面向对象设计。下面是调用代码:

//Normal Screen EmployeeDB objEmpDb = new EmployeeDB(); Employee objEmp = objEmpDb.Select(); //Manager Screen EmployeeDB objEmpDb = new EmployeeManagerDB(); Employee objEmp = objEmpDb.Select();

2)扩展方法

若是你使用.NET 3.5或者更高的版本,他提供了另一种解决方案叫作扩展方法,它可让咱们在现有类型中添加新的方法而不用干涉它。

NOTE:固然有更多的方法来实现咱们想要结果。就像咱们说这些原则不是戒律同样。

III) L – LSP – Liskov substitution principle(里氏替换原则)

什么是LSP?

你可能会疑问为何咱们先定义好例子和要讨论的问题。简而言之,我以为这样会更好。

LSP: 在任什么时候候父类能够被派生类替换。不要以为奇怪?若是咱们老是能够这样编码:

BaseClass b = new DerivedClass(),怎么还会有这条原则?

现实生活 



父亲是房地产商人,而他的儿子却想成为一名板球运动员。

儿子没法取代父亲的位置,尽管他们是两爷儿。



发现问题

下面咱们讲个很是通用的例子

一般会讨论几何形状的问题,就是正方形继承矩形的问题。看下面代码片断: 

public class Rectangle
{
    public int Width { get; set; }
    public int Height { get; set; }
}

public class Square:Rectangle
{
    //codes specific to
    //square will be added
}

用法以下:

Rectangle o = new Rectangle();
o.Width = 5;
o.Height = 6;

完美,可是遵照LSP原则咱们将用正方形替换矩形 ,咱们来试试:

Rectangle o = new Square();
o.Width = 5;
o.Height = 6;

出啥问题了?正方形的边长不能不相等

这意味着什么?这意味着咱们不能用子类替换父类,这就违反了LSP原则

那么咱们为何不在矩形中虚拟长和宽,而后在正方形中重写它们?

代码以下: 

public class Square : Rectangle 
{
    public override int Width
    {
        get{return base.Width;}
        set
        {
            base.Height = value;
            base.Width = value;
        }
    }
    public override int Height
    {
        get{return base.Height;}
        set
        {
            base.Height = value;
            base.Width = value;
        }
    }        
}

这样作不行,仍是违反了LSP原则,因为在子类改变了宽和高的属性(对于矩形来讲宽和高不能相等,相等就不是矩形了)。(看来这个方法行不通)

解决方法

这里应该抽象一个名为Shape的基类,Shape代码以下: 

public abstract class Shape
{
    public virtual int Width { get; set; }
    public virtual int Height { get; set; }
}

如今这里应该有两个具体的且相互独立的子类。一个是正方形,一个是矩形。他们都将继承自Shape类。

如今程序员可使用以下代码: 

Shape o = new Rectangle();
o.Width = 5;
o.Height = 6;

Shape o = new Square();
o.Width = 5; //both height and width become 5
o.Height = 6; //both height and width become 6

即便咱们在派生类中不改变宽和高的行为,当咱们讨论Shape对象时,宽和高没有具体的限制,它们能够相等也能够不相等。

IV) I – ISP– Interface Segregation principle

(接口隔离原则)

现实生活

假设你购买了一台新电脑。你会看到一系列的USB接口,串行接口,VGA接口等等。若是你打开机箱,你会看到不少插槽在主板上用于链接各个部件,主要由硬件工程师装配的时候使用。

这些插槽都是看不见的除非你打开机箱。简单说,它只暴露出你须要的接口。想象这么一种状况,全部接口都在外部或内部,这样会使得硬件故障概率变大。 

假设咱们要去商店买东西(好比要买个一板球棒)。想象一下商店老板开始向你介绍球和球桩。咱们可能会迷惑,最终可能买了咱们并不须要的东西。甚至可能忘了来这里的最初目的。

发现问题

假设咱们想要开发一个报告管理系统。如今,第一首要任务是建立一个业务层,它将被用在三个不一样角色的UI层级上。

  1. EmployeeUI — 显示当前登陆employee的相关报告
  2. ManagerUI — 显示manager本身以及他管理的小组报告
  3. AdminUI — 显示相关employee,小组,公司相关的报告。
public interface IReportBAL
{
    void GeneratePFReport();
    void GenerateESICReport();

    void GenerateResourcePerformanceReport();
    void GenerateProjectSchedule();

    void GenerateProfitReport();
}
public class ReportBAL : IReportBAL
{    
    public void GeneratePFReport()
    {/*...............*/}

    public void GenerateESICReport()
    {/*...............*/}

    public void GenerateResourcePerformanceReport()
    {/*...............*/}

    public void GenerateProjectSchedule()
    {/*...............*/}

    public void GenerateProfitReport()
    {/*...............*/}
}
public class EmployeeUI
{
    public void DisplayUI()
    {
        IReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
    }
}
public class ManagerUI
{
    public void DisplayUI()
    {
        IReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport ();
        objBal.GenerateProjectSchedule ();
    }
}
public class AdminUI
{
    public void DisplayUI()
    {
        IReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport();
        objBal.GenerateProjectSchedule();
        objBal.GenerateProfitReport();
    }
}

如今在每个UI层当开发者输入”objBal”下面的提示将会出现:

有什么问题?

开发者在使用EmployeeUI时却能够调用其余方法,这可能会致使开发者困惑。

什么是ISP?

ISP:客户不该该被迫实现他不使用的借口。 还能够说成:多个特定的客户接口比一个通用接口更好。 简单的说,若是你的接口太臃肿,应该把它拆分红多个接口。

根据ISP原则更新后的代码

public interface IEmployeeReportBAL
{
    void GeneratePFReport();
    void GenerateESICReport();
}
public interface IManagerReportBAL : IEmployeeReportBAL
{
    void GenerateResourcePerformanceReport();
    void GenerateProjectSchedule();
}
public interface IAdminReportBAL : IManagerReportBAL
{
    void GenerateProfitReport();
}
public class ReportBAL : IAdminReportBAL 
{    
    public void GeneratePFReport()
    {/*...............*/}

    public void GenerateESICReport()
    {/*...............*/}

    public void GenerateResourcePerformanceReport()
    {/*...............*/}

    public void GenerateProjectSchedule()
    {/*...............*/}

    public void GenerateProfitReport()
    {/*...............*/}
}
public class EmployeeUI {
    public void DisplayUI() {
        IEmployeeReportBAL objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
    }
}

代码提示:


public class ManagerUI
{
    public void DisplayUI()
    {
        IManagerReportBAL  objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport ();
        objBal.GenerateProjectSchedule ();
    }
}

代码提示:


public class AdminUI
{
    public void DisplayUI()
    {
        IAdminReportBAL  objBal = new ReportBAL();
        objBal.GenerateESICReport();
        objBal.GeneratePFReport();
        objBal.GenerateResourcePerformanceReport();
        objBal.GenerateProjectSchedule();
        objBal.GenerateProfitReport();
    }
}

代码提示:


这样经过遵照ISP原则,可让客户看到他们指望的内容。

V) D–DIP– Dependency Inversion principle (依赖倒置原则)

现实生活

咱们拿PC机来讲。不一样的部件,好比内存,硬盘,光驱等等,全都松散的链接在主板上。这意味着,假如某一天某些部件坏了能够很容易更换。想像一下全部部件都紧密的耦合在一块儿,这意味着它不能从主板上拆下来。在这种状况下若是内存坏了,咱们不得不更换新的主板,这将很是浪费money,若是你是土豪那就无所谓了。

发现问题

看下面代码:

public class CustomerBAL
{
    public void Insert(Customer c)
    {
        try
        {
            //Insert logic
        }
        catch (Exception e)
        {
            FileLogger f = new FileLogger();
            f.LogError(e);
        }
    }
}

public class FileLogger
{
    public void LogError(Exception e)
    {
        //Log Error in a physical file
    }
}

在上面代码中CustomerBAL直接依赖于FileLogger,FileLogger会将异常日志写入文件。如今咱们假设明天管理决定将异常日志输出到事件视图上。那将会怎么样?改变现有的代码。Oh no!My God,那将会创造出新的错误。 

什么是DIP?

DIP:高层模块不该该依赖于底层模块,相反,二者都应该依赖于抽象。

DIP解决方案

public interface ILogger
{
    void LogError(Exception e);
}

public class FileLogger:ILogger
{
    public void LogError(Exception e)
    {
        //Log Error in a physical file
    }
}
public class EventViewerLogger : ILogger
{
    public void LogError(Exception e)
    {
        //Log Error in a physical file
    }
}
public class CustomerBAL
{
    private ILogger _objLogger;
    public CustomerBAL(ILogger objLogger)
    {
        _objLogger = objLogger;
    }

    public void Insert(Customer c)
    {
        try
        {
            //Insert logic
        }
        catch (Exception e)
        {            
            _objLogger.LogError(e);
        }
    }
}

跟你所看到的同样客户依赖于抽象,ILogger能够做为实例替代任何它的派生类。

到目前为止咱们讨论了SOLID得五个原则。感谢Bob大叔。

结束了吗?

除了Bob大叔总结的这些原则,还有其余原则吗?答案是确定的,可是这里就不在一一讲述其细节了。下面大概列一下:

  • 面向接口编程,而非实现
  • 不要重复本身
  • 封装变化
  • 依赖于抽象,而非具体的类
  • 最少知识原则/迪米特法则
  • 组合优先于继承
  • 好莱坞原则
  • 尽量使用设计模式
  • 争取松散耦合的系统
  • 保持简单和直接

总结

咱们没法阻止需求变动,惟一可作的是开发设计出能够适应变动的软件。

  • 当你建立类,方法,或者其余模块时(这甚至适用于SQL的储存过程和函数)SRP原则应该牢记在心中。保持代码的可读性,健壮,可测试性
  • 从我我的经验上来看并非每次咱们都要遵照DIP原则,有时咱们须要依赖于具体的实现类。但须要确定的是理解系统,需求和环境属性,找出哪些地方应该遵照DIP原则。
  • 遵照DIP和SRP将为OCP的实现提供便利。
  • 确保建立特定接口,以便让复杂和困惑远离开发人员。同时ISP原则也将获得遵照。
  • 在使用继承时确保LSP原则

但愿这篇文章能给你带来收获。多谢你的耐心阅读。

许可

本文及相关代码和文件都遵循CPOL协议(Code Project Open License

关于做者 


Marla Sukesh

Technical Lead ShawMan Softwares 

India 

关于译者 

此处省略若干字

相关文章
相关标签/搜索