前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >设计原则

设计原则

作者头像
甜橙很酸
发布2018-03-30 10:15:37
7220
发布2018-03-30 10:15:37
举报
文章被收录于专栏:DOTNETDOTNET

一、面向对象应用程序开发原则(SOLID)

1单一职责原则(SRP)

定义:

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

为什么要遵守这条原则?

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

运用与辨析

例1:记录日志

代码语言:javascript
复制
public class Logger
{
        public void LogToFile<T>(T msg);
        public void LogToDB<T>(T msg);
        public void LogToWindows<T>(T msg);
}

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

例2:一个大的业务层类

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

代码语言:javascript
复制
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的代码段极有可能违反了开放封闭原则。

运用的方式方法

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

运用与辨析

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

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

定义日志接口

代码语言:javascript
复制
public interface ILogger
{
        void Log<T>(T msg);
}

实现接口

代码语言:javascript
复制
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>接口:

代码语言:javascript
复制
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方法,所以在方法体中抛出了异常

代码语言:javascript
复制
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>拆分成两个以上的更小的接口,按需实现接口,修改如下:

代码语言:javascript
复制
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创建企业级应用的过程中,定义了如下的控制器:

代码语言:javascript
复制
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存储到数据库中,实现如下:

代码语言:javascript
复制
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原则。所以将其更改如下:

代码语言:javascript
复制
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);
        }

        //其他代码

}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档