专栏首页DOTNET设计原则

设计原则

一、面向对象应用程序开发原则(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);
        }

        //其他代码

}

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • asp.net web api 下载之断点续传

    一、基本思想 利用 HTTP 请求的Range标头值,来向服务端传递请求数据的开始位置和结束位置。服务端获得这两个参数后,将指定范围内的数据传递给客户端。当客户...

    甜橙很酸
  • Entity Framework——建模建库

    1数据库初始化策略选择 三种初始化策略: 1)CreateDatabaseIfNotExists:默认的策略。如果数据库不存在,那么就创建数据库。但是如果数据库...

    甜橙很酸
  • ASP.NET Web API编程——序列化与内容协商

    1 多媒体格式化器 多媒体类型又叫MIME类型,指示了数据的格式。在HTTP协议中多媒体类型描述了消息体的格式。一个多媒体类型包括两个字符串:类型和子类型。 例...

    甜橙很酸
  • Entity Framework Core 实现全局查询过滤

    微软在 Entity Framework Core 2+ 中引入了全局查询过滤器,简化了构建多租户应用程序和实体软删除的复杂度。这篇文章我将通过代码的形式对全局...

    喵叔
  • 依赖注入容器-- Autofac

    Autofac---Autofac是一款IOC框架,比较于其他的IOC框架,如Spring.NET,Unity,Castle等等所包含的,它很轻量级性能上非常高...

    小世界的野孩子
  • Java面向对象之抽象类,接口

    抽象类: 含有抽象方法的类被声明为抽象类 抽象方法由子类去实现 含有抽象方法的类必须被声明为抽象类 抽象类被子类继承,子类(如果不是抽象类)必须重写抽象类中...

    二十三年蝉
  • 在C#中使用依赖注入-生命周期控制

    在使用依赖注入的过程当中,除了应用设计模式注意代码的变化隔离之外,另外一个重要的内容就是生命周期控制。

    newbe36524
  • Java 接口

    但如果三角形类不需要draw()方法,只能去掉图形类中的draw()或把draw()变成图形类私有

    用户2965768
  • 转载 Java设计模式

    设计模式; 一个程序员对设计模式的理解: “不懂”为什么要把很简单的东西搞得那么复杂。后来随着软件开发经验的增加才开始明白我所看到的“复杂”恰恰就是设计模式的精...

    用户1518699
  • APK安装流程详解1——有关"安装ing"的实体类概述

    该类包含了从AndroidManifest.xml文件中收集的所有信息。 PackageInfo.java源码地址 通过源码我们知道PackageInfo是...

    隔壁老李头

扫码关注云+社区

领取腾讯云代金券