一、面向对象应用程序开发原则(SOLID)
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)提取公共部分而不使用继承
如果一组类都支持一个公共的职责,将这个职责提取出来,放到一个父类中,然后让这组类继承此父类。
运用与辨析
见接口隔离原则的例子。
4接口隔离原则(ISP)
定义
不应该强迫客户程序依赖并未使用的方法。接口不应包含所有的对象行为,接口应尽可能的小。这个原则用来处理“胖”接口所存在的缺点。
为什么要遵循此原则?
如果程序中的一部分更改会影响到程序中完全和它无关的其他部分,那么更改的代价和影响就变得不可预测。
违反原则的情形
接口污染,即接口被一个它不总是需要的方法污染,也就是说不是此接口的每一个派生类都需要那个方法。但由于接口已经定义了这个方法,那么不需要它的派生类也要实现这个方法。
运用的方式方法
1)使用委托分离接口
对象的客户端不通过该对象的接口去访问它,而是通过委托去访问他。此方案的缺点:委托处理会导致一些很小但仍然存在的运行时间和内存的开销。
2)使用多重继承分离接口:通常这种做法是首选的。
运用与辨析
在web应用开发中使用仓储模式来封装对底层数据库的访问,为此创建IRepository<T>接口:
public interface IRepository<T>
{
T GetById(int id);
bool Delete(T entity);
bool Save(T entity);
void Update(T entity);
IList<T> Get(string condition);
......
}
这是一个典型的胖接口,并不是每一个子类都会实现这么多的方法。对于继承了这个接口却不需要实现其中某些方法的接口,只能将方法体设置为空实现或抛出异常。例如下面的类中不需要实现Get方法,所以在方法体中抛出了异常
public class MRepository<T> : IRepository<T>
{
public T GetById(int id)
{
//具体实现
}
public bool Delete(T entity)
{
//具体实现
}
public bool Save(T entity)
{
//具体实现
}
public void Update(T entity)
{
//具体实现
}
//不需要实现此方法
public IList<T> Get(string condition)
{
throw new NotImplementedException();
}
}
在接口的实现里抛出异常,这样做显然违背了里氏替换原则(LSP),解决的办法是将IRepository<T>拆分成两个以上的更小的接口,按需实现接口,修改如下:
public interface IRepository<T>
{
T GetById(int id);
bool Delete(T entity);
bool Save(T entity);
void Update(T entity);
}
public interface IRepositoryAL<T>
{
IList<T> Get(string condition);
}
public class MRepository<T> : IRepository<T>
{
public T GetById(int id)
{
//具体实现
}
public bool Delete(T entity)
{
//具体实现
}
public bool Save(T entity)
{
//具体实现
}
public void Update(T entity)
{
//具体实现
}
}
5 依赖倒置原则(DIP)
定义
高层模块不应依赖于低层模块。二者都应依赖于抽象。抽象不应该依赖于细节,细节应该依赖于抽象。这样高层组件与低层组件之间通过抽象的接口来交换而不是具体类。该原则是框架设计的核心。
为什么要遵守此原则?
如果高层模块依赖于低层模块,那么对低层模块的改动会直接影响到高层模块,从而迫使他们一次做出改动。
违反原则的情形
高低层组件通过具体类来实现交互。
运用的方式方法
“倒置”不仅仅是依赖关系的倒置,也是接口所有权的倒置。当使用DIP原则时,往往客户拥有抽象接口,而他们的服务者则从这些抽象接口派生。
启发式的方法:
1)找到那些指向具体类的引用的变量。
2)找到任何派生自具体类的类。
3)找到那些重写方法,而基类方法已经实现过了。
运用与辨析
依赖倒置式控制反转的精髓,通过控制反转可以深刻的体会到依赖倒置的作用。
6 迪米特法则(LoD,又名最少知道原则)
定义
一个对象应当对其他对象有尽可能少的了解,只和自己关系最密切对象直接作用。
关系最密切的对象含义是:
当前对象本身,通过该对象方法参数传入的对象,此类的其他实例化对象,以及其所在聚集类的其他成员。
为什么要遵守此原则?
降低耦合,减少依赖。
违反原则的情形
和除了上述关系最密切的对象之间通信。
运用的方式方法
1)限制类及其成员的访问权限。
2)引入门面模式和中介者模式。
7 组合/聚合复用原则(CARP)
定义
将已有的多个对象组成一个新对象,达到复用的目的。
为什么要遵守此原则?
在建模的过程中,我们会发现,某些实体之间不具有继承关系,但是他们之间却有一些像是的操作,为了实现这种无法用继承表达的关系,我们遵照CARP原则。
二、DRY原则(不要重复自己)
避免重复相同或相似的代码。
运用与辨析
定义拦截器或过滤器充分体现了DRY原则。
例如使用ASP.NET MVC创建企业级应用的过程中,定义了如下的控制器:
public class ExcludedDataController : BaseController
{
[HttpPost]
public ActionResult Add(ExcludedDataInfo info)
{
if (Request.IsAjaxRequest())
{
//其他代码
}
return new EmptyResult();
}
public ActionResult Del(ExcludedDataInfo info)
{
if (Request.IsAjaxRequest())
{
//其他代码
}
return new EmptyResult();
}
public ActionResult BatchAdd(string itemCodes, int currentNavId, int library_DataBase_ID)
{
if (Request.IsAjaxRequest())
{
//其他代码
}
return new EmptyResult();
}
}
其中三个方法中都调用了Request.IsAjaxRequest()方法,明显违反了DRY原则,解决的办法是可以在控制器上添加拦截器。但是或许此控制器的操作中还有不被Ajax调用的操作,那么可以将这些操作移除,放入一个新的控制器中。
三、控制反转(IoC)
控制反转是基于面向对象的原则,提倡松耦合理念的设计原则,允许独立开发应用程序的各个组件。
实现方式
实现方式有两种:依赖注入,服务定位。
依赖注入:引用其他的dll,组件之间的引用,一个类持有另一个类,这些都可以被看做是依赖。最常遇到的是一个类持有另一个类的问题。
依赖注入有三种方式:构造函数注入,属性注入,方法注入。最常使用的是构造函数的注入。
服务定位:通过IoC容器获取依赖的具体类型,并将其赋给接口。
运用与辨析
记录Entity Framework执行SQL语句对优化系统有极大的帮助。为记录SQL定扩展命令拦截器IDbCommandInterceptor,在实现的方法中记录SQL。可以将SQL记录到本地文本文件中,也可以将SQL存储到数据库中,实现如下:
public class CommandInterceptor : IDbCommandInterceptor
{
private Logger logger = new Logger();
public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
{
this.logger.Log<int>(command, interceptionContext);
}
//其他方法......
}
上面的实现包含了一个依赖项,即Logger,如果后续改变存储SQL的媒介,那么就要修改Logger.Log这个方法,明显违反了OCP原则,也没有遵循DIP原则。所以将其更改如下:
public class CommandInterceptor : IDbCommandInterceptor
{
private ICommandLogger logger;
public CommandInterceptor(ICommandLogger logger)
{
this.logger = logger;
}
public void NonQueryExecuted(DbCommand command, DbCommandInterceptionContext<int> interceptionContext)
{
this.logger.Log<int>(command, interceptionContext);
}
//其他代码
}