设计原则

一、面向对象应用程序开发原则(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 条评论
登录 后参与评论

相关文章

来自专栏程序员与猫

CLR和.Net对象生存周期

1356
来自专栏Jimoer

经历的面试题,先做下部分总结。

1653
来自专栏编程

PLC编程优化方法,让程序运行提速!

PLC、DCS、仪器仪表、电气技术资料,一网打尽 通过本方法优化可以极大的减少程序语句数,使PLC程序更简洁、可读性更好,由于不需要做耗时的类型转换,程序运行效...

2149
来自专栏编程

C语言嵌入式系统编程修炼之内存操作

这是我13年前创作和发表在互联网上的文章,这么多年过去了,这篇文章仍然在到处传播。现在贴回Linuxer公众号。 全文目录: C语言嵌入式系统编程修炼之道——背...

3005
来自专栏NetCore

[原创]Fluent NHibernate之旅(三)-- 继承

经过了“开篇”和“简单映射”两篇文章,相信大家对Fluent NHibernate 有了一定的了解了,FluentNHibernate实际就是对 NHibern...

2008
来自专栏一“技”之长

iOS有关内存管理的二三事 原

随着移动设备的内存越来越大,程序员也已经度过了为了那一两M的内存在系统的抽丝剥茧的年代,对于JAVA的开发者,对内存更是伸手即取,并且从不关心什么时候还回去。但...

692
来自专栏美团技术团队

Android热更新方案Robust开源,新增自动化补丁工具

我们在之前的博客文章中介绍了高兼容性、高稳定性的实时热更新解决方案Robust之后,业内反响强烈,不断有读者咨询我们什么时候开源。今天我们非常高兴地宣布,Rob...

4635
来自专栏海说

[转]我的编码习惯 - 接口定义

工作中,少不了要定义各种接口,系统集成要定义接口,前后台掉调用也要定义接口。接口定义一定程度上能反应程序员的编程功底。列举一下工作中我发现大家容易出现的问题:

953
来自专栏zhisheng

面向对象设计原则之依赖倒转原则

前两天的算法排序文章,《Java常用排序算法/程序员必须掌握的8大排序算法(上)》、《Java常用排序算法/程序员必须掌握的8大排序算法(下)》,没看的可以点上...

3708
来自专栏CSDN技术头条

C#秒杀Java的五个不可替代的特性,完美的编程语言并不存在

如果我们可以同时拥有 C# 和 Java 世界的最好特性,那会是什么样呢? 完美的编程语言并不存在,我希望我们可以在这一点上达成一致。开发新语言往往是为了克服...

24110

扫码关注云+社区

领取腾讯云代金券