65.9K
CodeProject 正在变化。 阅读更多。
Home

面向对象设计原则

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (197投票s)

2013年3月28日

CPOL

13分钟阅读

viewsIcon

586936

本文档旨在为至少对面向对象编程有基本了解的人员提供指导。

目标受众是谁?

本文档旨在为至少对面向对象编程有基本了解的人员提供指导。他们知道类和对象之间的区别,并且可以谈论面向对象编程的基本支柱,即封装、抽象、多态和继承。

引言

在面向对象的世界里,我们只看到对象。对象之间相互交互。类、对象、继承、多态、抽象是我们日常职业生涯中经常听到的词汇。

在现代软件世界中,每个软件开发者都在使用某种面向对象的语言,但问题是,他是否真的知道面向对象编程的含义?他是否知道自己正在作为一名面向对象的程序员工作?如果答案是肯定的,他是否真的在使用面向对象编程的力量?

在本文中,我们将超越面向对象编程的基本支柱,探讨面向对象设计。

面向对象设计

这是一个规划软件系统的过程,其中对象将相互交互以解决特定问题。俗话说:“良好的面向对象设计能让开发者生活轻松,而糟糕的设计则会造成灾难。”

如何开始?

当任何人开始创建软件架构时,他们的意图都是好的。他们会尝试利用自己现有的经验来创建优雅且干净的设计。

随着时间的推移,软件开始腐烂。随着每一个功能请求或变更,软件设计都会改变其形状,最终,对应用程序最简单的更改都需要付出巨大的努力,更重要的是,会增加出现更多错误的几率。

谁应负责?

软件是为了解决现实生活中的业务问题,而业务流程在不断演变,软件也在不断变化。

变化是软件世界不可或缺的一部分。客户付费,他们理所当然会要求他们期望的东西。所以我们不能将软件设计的退化归咎于“变化”。是我们的设计本身存在问题。

软件设计恶化的最大原因之一是将非计划的依赖引入系统。系统的每个部分都依赖于其他某个部分,因此改变一个部分会影响到另一个部分。如果我们能够管理这些依赖关系,我们将能够轻松维护软件系统和软件质量。

示例

解决方案 - 原则、设计模式和软件架构

  • 软件架构,如 MVC、3-Tier、MVP,告诉我们整个项目将如何构建。
  • 设计模式允许我们重用经验,或者说,为经常出现的问题提供可重用的解决方案。例如——对象创建问题、实例管理问题等。
  • 原则告诉我们,做这些事情,你就会达到这个目标。如何去做,取决于你自己。每个人在生活中都会给自己设定一些原则,例如,“我从不说谎”或“我从不喝酒”等。他/她遵循这些原则来让自己的生活更轻松,但如何坚持这些原则则取决于个人。

同样,面向对象设计充满了许多原则,这些原则使我们能够管理软件设计中的问题。

Robert Martin 先生(俗称 Bob 大叔)将其分为:

  1. 类设计原则 – 也称为 SOLID
  2. 包内聚原则
  3. 包耦合原则

在本文中,我们将通过实际示例讨论 SOLID 原则。

SOLID

这是由Robert Martin 先生(俗称 Bob 大叔)提出的五个原则的首字母缩写:单一职责、开闭、里氏替换、接口隔离和依赖倒置。据称(维基百科),当所有五个原则一起应用时,程序员更有可能创建一个易于维护和扩展的系统。让我们逐一详细讨论每个原则。

一) S - SRP - 单一职责原则

现实生活中的比较

我在印度一家软件公司担任团队负责人。业余时间我做一些写作、报纸编辑和其他各种项目。基本上,我在生活中承担多重职责。

当工作场所发生不好的事情时,比如老板因为我犯错而责骂我,我就会分心于其他工作。基本上,一件事出了问题,所有事情都会一团糟。

识别编程中的问题

在讨论这个原则之前,我想让您看一看下面的类。

  • 每次插入逻辑发生变化时,这个类都会改变。
  • 每次报告格式发生变化时,这个类都会改变。

问题是什么?

每次改变其中一个时,另一个也有可能被改变,因为它们住在同一个地方,并且有相同的父级。我们无法控制一切。因此,一次更改会导致双重测试(甚至可能更多)。

什么是 SRP?

SRP 说道:“每个软件模块应该只有一个改变的理由。”

  • 软件模块——类、函数等。
  • 改变的理由——职责

不违反 SRP 的解决方案

现在,这取决于我们如何实现。我们可以做的一件事是创建三个不同的类:

  1. Employee – 包含属性(数据)
  2. EmployeeDB – 执行数据库操作
  3. EmplyeeReport – 执行报告相关任务
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
    }
}

注意:此原则也适用于方法。每个方法都应该只有一个职责。

一个类可以有多个方法吗?

答案是肯定的。现在您可能会问,为什么

  1. 一个类将具有单一职责。
  2. 一个方法将具有单一职责。
  3. 一个类可能有一个以上的方法。

嗯,这个问题的答案很简单。这是上下文。在这里,职责与我们所讨论的上下文相关。当我们说类的职责时,它将处于较高的级别。例如,EmployeeDB 类将负责与数据库相关的员工操作,而 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);
}

测试本身就很有益,但代码变得可读也是一个额外的优点。代码越可读,它就越显而易见。

二) O - OCP – 开闭原则

现实生活中的比较

假设您想在您的两层楼房的第一层和第二层之间再加一层。您认为这可能吗?是的,可能,但可行吗?以下是一些选项:

  • 您在第一次盖房子时可以做的一件事是,在建造时就建三层,把第二层留空。然后随时利用第二层。我不知道这有多可行,但这是一个解决方案。
  • 拆掉现在的二楼,再建两层新楼,这不合理。

识别编程中的问题

假设 EmployeeDB 类中的 Select 方法被两个客户端/屏幕使用。一个用于普通员工,一个用于经理,而经理屏幕需要对该方法进行更改。

如果我更改 Select 方法以满足新需求,其他 UI 也会受到影响。而且,对现有经过测试的解决方案进行更改可能会导致意外的错误。

什么是 OCP?

它说道:“软件模块应该对修改关闭,对扩展开放。”这是一个正交的陈述。

不违反 OCP 的解决方案

1)使用继承

我们将从 EmployeeDB 派生一个名为 EmployeeManagerDB 的新类,并根据新需求重写 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
    }
}

注意:如果这项变更在设计时就已预见,并且已经为扩展提供了某种机制(例如,将方法设为虚拟),那么现在该设计就被认为是良好的面向对象设计。现在 UI 代码将看起来像

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

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

2)扩展方法

如果您使用的是 .NET 3.5 或更高版本,则有一种第二种方法称为扩展方法,它允许我们在不更改现有类型的情况下为其添加新方法。

注意:可能还有其他实现所需结果的方法。正如我所说的,这些是原则而不是命令。

三) L – 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。

为什么我们不在 Rectangle 中将 Width 和 Height 设为虚拟,并在 Square 中重写它们?

代码片段

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,因为我们在派生类中改变了 Width 和 Height 属性的行为(对于 Rectangle,高度和宽度不能相等,如果它们相等,它就不是 Rectangle)。

(这不会是一种替换。)

不违反 LSP 的解决方案

应该有一个抽象类 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

即使在派生类中重写后,我们也没有改变宽度和高度的行为,因为当我们谈论形状时,宽度和高度没有固定的规则。它们可以相等,也可以不相等。

四) I – ISP– 接口隔离原则

现实生活中的比较

假设您购买了一台新的台式电脑。您会发现几个 USB 端口、一些串行端口、一个 VGA 端口等。如果您打开机箱,您会看到主板上有许多插槽,用于连接各种部件,这些插槽主要由硬件工程师在组装时使用。

这些内部插槽在您打开机箱之前是看不见的。总之,只有必需的接口才对您可用/可见。想象一下,如果一切都是外部的或内部的。那么硬件发生故障的可能性更大(就像计算机用户的日子已经够难过一样)。

假设我们去商店买东西(比如,买一个板球拍)。

现在想象一下,店主开始向您展示球和球门柱。我们可能会感到困惑,并最终购买了我们不需要的东西。我们甚至可能忘记我们当初为什么来这里。

识别编程中的问题

假设我们要开发一个报表管理系统。现在,第一项任务是创建一个业务层,该业务层将由三个不同的 UI 使用。

  1. EmployeeUI – 显示与当前登录员工相关的报表
  2. ManagerUI – 显示与他自己和所属团队相关的报表。
  3. AdminUI – 显示与个人员工、团队以及公司相关的报表,例如利润报表。
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

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,我们可以让客户看到他们需要看到的内容。

五) D – DIP– 依赖倒置原则

现实生活中的比较

让我们谈谈我们的台式电脑。RAM、硬盘、CD-ROM(等)等不同部件松散地连接到主板上。这意味着,如果将来任何部件停止工作,都可以轻松更换。想象一下,如果所有部件都紧密耦合在一起,那么任何部件都无法从主板上移除。那样的话,如果 RAM 停止工作,我们就必须购买新的主板,这将非常昂贵。

识别编程中的问题

看下面的代码。

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 类,该类会将异常记录在物理文件中。现在,假设明天管理层决定将异常记录在事件查看器中。那怎么办?更改现有代码。哦,天哪!这可能会导致新的错误!

什么是 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。

希望大家喜欢阅读这篇文章。感谢您的耐心。

如需有关 ASP.NET、设计模式、WCF 和 MVC 等各种主题的技术培训,请联系 SukeshMarla[at]Gmail.com 或访问 www.sukesh-marla.com

如需更多此类内容,请点击 此处。订阅 文章更新 或在 Twitter 上关注 @SukeshMarla

.NET, C#, ASP.NET, SQL, WCF, WPF, WWF, SharePoint, 设计模式, UML 等领域查看 400 多个常见问题解答。

© . All rights reserved.