软件工程师需要理解的面向对象程序开发原则(SRP、OCP、LSP)-lsp文件

软件工程师需要理解的面向对象程序开发原则(SRP、OCP、LSP)-lsp文件

导读

本文示例使用C#语言写的,但是原理适用于一切面向对象语言。

1单一职责原则(SRP)

定义:

一个类应该只有一个发生变化的原因。这条原则曾被称为内聚性,即一个模块的组成元素之间的功能相关性。

为什么要遵守这条原则?

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

运用与辨析

例1:记录日志

 public class Logger
{
public void LogToFile<T>(T msg);
public void LogToDB<T>(T msg);
public void LogToWindows<T>(T msg);
}

这个例子定义了一个日志类,包含三种方法:将日志写入本地文件、数据库或windows系统日志。一般会人为日志类记录日志这个动作算做一个职责,然而事实并非如此,将日志记录到不同的存储介质算作不同的职责。基于这种认识,断定这个类包含了太多的职责,应该将职责分离出来。

例2:一个大的业务层类

一个用户履历操作相关的类,包括:用户的教育背景,社会兼职职务,工作经历个人简历,获得的荣誉等,示例如下:

public class UserResumeService
{
#region 社会兼职
//添加社会兼职
public bool AddParttime(int userId, Parttime item)
{
//具体实现
}
//删除社会兼职信息
public bool DelParttime(int userId, string parttimeId)
{
//具体实现
}
//更新社会兼职
public bool UpdateParttime(int userId, Parttime item)
{
//具体实现
}
#endregion

#region 教育背景
//添加教育背景
public bool AddEducation(int userId, EducationInfo item)
{
//具体实现
}
//删除教育背景
public bool DelEducation(int userId, string educationId)
{
//具体实现
}
//更新教育背景
public bool UpdateEducation(int userId, EducationInfo item)
{
//具体实现
}
#endregion

#region 工作经历
//添加工作经历
public bool AddWork(int userId, WorkInfo item)
{
//具体实现
}
//删除工作经历
public bool DelWork(int userId, string workId)
{
//具体实现
}
//更新工作经历
public bool UpdateWork(int userId, WorkInfo item)
{
//具体实现
}
#endregion

#region 科研项目
//添加科研项目
public bool AddProject(int userId, Project item)
{
//具体实现
}
//删除科研项目
public bool DelProject(int userId, string projectId)
{
//具体实现
}
//更新科研项目
public bool UpdateProject(int userId, Project item)
{
//具体实现
}
#endregion
}

这个类实在太大了,以至于不等不用#region将每块功能收起来。虽然这些操作都是针对一个用户的,但这不是一个职责,也不是俩个职责,这个类包含了太多职责,然而这不是一个工具类!如果是工具类还说得过去。解决的办法就是,将这个大类拆为几个小类,每个类表达一个职责,譬如教育背景相关操作归为一个小类,社会兼职相关操作也归为一个小类,其他依次类推。

2 开放封闭原则(OCP)

定义

软件实体(类、模块、函数等)应该是可以扩展的,但不可修改。

为什么要遵守此原则?

任何系统在其生命周期都极有可能发生变化,如果不遵循此原则,那么系统将难以应对发生的变化,这很可能迫使我们抛弃现有版本,这会给我们带来极大的损失。

违反原则的情形

那些包含switch、if/else的代码段极有可能违反了开放封闭原则。

运用的方式方法

创建出固定的、能够描述一组任意个可能行为的抽象基类或接口,然后针对每一个可能的行为创建一个派生自抽象基类或接口的子类。

运用与辨析

这种做法的缺点是有可能会产生很多类,这样就增加了代码量。

据此修改上面日志记录的例子:

定义日志接口

public interface ILogger
{
void Log<T>(T msg);
}

实现接口

public class LoggerToFile : ILogger
{
public void Log<T>(T msg)
{
//具体实现
}
}
public class LoggerToDB : ILogger
{
public void Log<T>(T msg)
{
//具体实现
}
}
public class LoggerToWindows : ILogger
{
public void Log<T>(T msg)
{
//具体实现
}
}

3里氏替换原则(LSP)

定义

子类型能够替换掉它们的基类型,而不影响对象的行为和规则。

为什么要遵循此原则?

我们要遵循OCP原则,OCP背后的机制是抽象和多态,支持抽象和多态的关键机制是继承(比如C#是这样),那么是什么设计规则支配着这种继承用法?最佳的继承层次特征是什么?如何使我们创建的类层次结构符合OCP?这是本原则要解答的问题。

违反原则的情形

1)显示的使用if语句或if/else语句去确定一个对象的类型,以便可以选择针对不同对象实现不同操作。

2)对于继承是IS-A(是一个)关系,即如果一个新类型的对象被认为和一个已知类型的对象之间满足IS-A关系,那么这个新对象的类应该从这个已有对象的类派生。

3)完成的功能少于其基类的派生类通常是不能替换其基类的,因此违反LSP。

4)当派生类中抛出基类没有的异常时,违反LSP。

运用的方式方法

1)基于契约编程

契约是通过为每一个方法声明前置条件和后置条件来指定的。要使一个方法得以执行,前置条件必须要为真;执行完毕后,该方法要保证后置条件为真。

派生类的前置条件和后置条件规则为:在重新声明派生类中的例程时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。

2)提取公共部分而不使用继承

如果一组类都支持一个公共的职责,将这个职责提取出来,放到一个父类中,然后让这组类继承此父类。

运用与辨析

见本系列文章:《软件工程师需要理解的面向对象程序开发原则(ISP、DIP、LoD、CARP、DRY、IoC)》中接口隔离原则的例子。

推荐阅读