内容简介:在面向对象编程中,接口是一个非常重要的武器。接口所表达的是客户端代码需求和需求具体实现之间的边界。接口分离原则主张接口应该足够小,大而全的契约(接口)是毫无意义的。将大型接口分割为多个小型接口的原因有:①需要单独修饰接口
在面向对象编程中,接口是一个非常重要的武器。接口所表达的是客户端代码需求和需求具体实现之间的边界。接口分离原则主张接口应该足够小,大而全的契约(接口)是毫无意义的。
接口分离的原因
将大型接口分割为多个小型接口的原因有:
①需要单独修饰接口
②客户端需要
③架构需要
需要单独修饰接口
我们通过拆解一个单个巨型接口到多个小型接口的示例,分离过程中创建了各种各样的修饰器,来讲解大量应用接口分离原则带来的主要好处。
下面这个接口包含了5个方法,用于用户对实体对象的持久化存储进行CRUD操作。
public interface ICreateReadUpdateDelete<TEntity> { void Create(TEntity entity); TEntity ReadOne(Guid identity); IEnumerable<TEntity> ReadAll(); void Update(TEntity entity); void Delete(TEntity entity); } 复制代码
ICreateReadUpdateDelete是一个泛型接口,可以接受不同的实体类型。客户端需要首先声明自己要依赖的TEntity。CRUD中的每个操作都是由对应的ICreateReadUpdateDelete接口实现来执行,也包括修饰器实现。
有些修饰器作用于所有方法,比如日志修饰器。当然,日志修饰器属于横切关注点,为了避免在多个接口中重复实现,也可以使用面向切面编程(AOP)来修饰接口的所有实现。
public class CrudLogging<TEntity> : ICreateReadUpdateDelete<TEntity> { private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud; private readonly ILog log; public CrudLogging(ICreateReadUpdateDelete<TEntity> decoratedCrud, ILog log) { this.decoratedCrud = decoratedCrud; this.log = log; } public void Create(TEntity entity) { log.InfoFormat("Create entity of type {0}", typeof(TEntity).Name); decoratedCrud.Create(entity); } public void Delete(TEntity entity) { log.InfoFormat("Delete entity of type {0}", typeof(TEntity).Name); decoratedCrud.Delete(entity); } public IEnumerable<TEntity> ReadAll() { log.InfoFormat("Reading all entities of type {0}", typeof(TEntity).Name); return decoratedCrud.ReadAll(); } public TEntity ReadOne(Guid identity) { log.InfoFormat("Reading entity of type {0}", typeof(TEntity).Name); return decoratedCrud.ReadOne(identity); } public void Update(TEntity entity) { log.InfoFormat("Update entity of type {0}", typeof(TEntity).Name); decoratedCrud.Update(entity); } } 复制代码
但是有些修饰器只应用于接口的部分方法上,而不是所有的方法。假设现在有这么一个需求,在持久化存储中删除某个实体前提示用户。切记不要直接去修改现有的类实现,因为这会违背开放与封闭原则。相反,应该创建一个客户端用来删除实体的新实现。
public class DeleteConfirm<TEntity> : ICreateReadUpdateDelete<TEntity> { private readonly ICreateReadUpdateDelete<TEntity> decoratedCrud; public DeleteConfirm(ICreateReadUpdateDelete<TEntity> decoratedCrud) { this.decoratedCrud = decoratedCrud; } public void Create(TEntity entity) { decoratedCrud.Create(entity); } public IEnumerable<TEntity> ReadAll() { return decoratedCrud.ReadAll(); } public TEntity ReadOne(Guid identity) { return decoratedCrud.ReadOne(identity); } public void Update(TEntity entity) { decoratedCrud.Update(entity); } public void Delete(TEntity entity) { Console.WriteLine("Are you sure you want to delete the entity ? [y/n]"); var keyInfo = Console.ReadKey(); if(keyInfo.Key == ConsoleKey.Y) { decoratedCrud.Delete(entity); } } } 复制代码
如上代码,DeleteConfirm只修饰了Delete方法,其余方法都是 直托方法 (没有任何修饰,就像直接调用被修饰的接口方法一样)。尽管这些直托方法什么都没有做,你还是需要一一实现,并且还需要编写测试方法验证方法行为是否正确,这样做与接口分离的方式比较起来麻烦的多。
我们可以将Delete方法从ICreateReadUpdateDelete接口分离,这样会得到两个接口:
public interface ICreateReadUpdate<TEntity> { void Create(TEntity entity); TEntity ReadOne(Guid identity); IEnumerable<TEntity> ReadAll(); void Update(TEntity entity); } public interface IDelete<TEntity> { void Delete(TEntity entity); } 复制代码
然后只对IDelete接口提供确认修饰器的实现:
public class DeleteConfirm<TEntity> : IDelete<TEntity> { private readonly IDelete<TEntity> decoratedDelete; public DeleteConfirm(IDelete<TEntity> decoratedDelete) { this.decoratedDelete = decoratedDelete; } public void Delete(TEntity entity) { Console.WriteLine("Are you sure you want to delete the entity ? [y/n]"); var keyInfo = Console.ReadKey(); if(keyInfo.Key == ConsoleKey.Y) { decoratedDelete.Delete(entity); } } } 复制代码
这样一来,代码意图更清晰,代码量减少了,也没有那么多的直托方法,相应的测试工作量也变少了。
客户端需要
客户端只需要它们需要的东西。那些巨型接口倾向于给用户提供更多的控制能力,带有大量成员的接口允许客户端做很多操作,甚至包括它们不应该做的。更好的办法是尽早采用防御方式进行编程,以此阻止其他开发人员(包括将来的自己)无意中使用你的接口做出一些不该做的事情。
现在有一个场景是通过用户配置接口访问程序当前的主题,实现如下:
public interface IUserSettings { string Theme { get; set; } } 复制代码
public class UserSettingsConfig : IUserSettings { private const string ThemeSetting = "Theme"; private readonly Configuration config; public UserSettingsConfig() { config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None); } public string Theme { get { return config.AppSettingd[ThemeSetting].value; } set { config.AppSettingd[ThemeSetting].value = value; config.Save(); ConfigurationManager.RefreshSection("appSettings"); } } } 复制代码
接口不同的客户端以不同的目的使用同一个属性:
public class ReadingController { private readonly IUserSettings userSettings; public ReadingController(IUserSettings userSettings) { this.userSettings = userSettings; } public string GetTheme() { return userSettings.Theme; } } public class WritingController { private readonly IUserSettings userSettings; public WritingController(IUserSettings userSettings) { this.userSettings = userSettings; } public void SetTheme(string theme) { userSettings.Theme = theme; } } 复制代码
虽然现在ReadingController类只是用了Theme属性的读取器,WritingController类只使用了Theme属性的设置器。但是由于缺乏接口分离,我们无法阻止WritingController类获取主题数据,也无法阻止ReadingController类修改主题数据,这可是个大问题,尤其是后者。
为了防止和消除错用接口的可能性,可以将原有接口一分为二:一个负责读取主题数据,一个负责修改主题数据。
public interface IUserSettingsReader { string Theme { get; } } public interface IUserSettingsWriter { string Theme { set; } } 复制代码
UserSettingsConfig实现类现在分别实现IUserSettingsReader和IUserSettingsWriter接口
public class UserSettingsConfig : IUserSettings
=>
public class UserSettingsConfig:IUserSettingsReader,IUserSettingsWriter
客户端现在分别只依赖它们真正需要的接口:
public class ReadingController { private readonly IUserSettingsReader userSettings; public ReadingController(IUserSettingsReader userSettings) { this.userSettings = userSettings; } public string GetTheme() { return userSettings.Theme; } } public class WritingController { private readonly IUserSettingsWriter userSettings; public WritingController(IUserSettingsWriter userSettings) { this.userSettings = userSettings; } public void SetTheme(string theme) { userSettings.Theme = theme; } } 复制代码
架构需要
另一种接口分离的驱动力来自于架构设计。在非对称架构中,例如 命令查询责任分离模式 (读写分离),意图就是指导你去做一些接口分离的动作。
数据库(表)的设计本身是面向数据,面向集合的;而现在的主流编程语言都有面向对象的一面。面向数据(集合)和面向对象本身就是冲突的,但是在现代系统中数据库又是必不可少的一环。为了解决这种 阻抗失衡 ,ORM(对象关系映射)应运而生。完全隔离掉数据库,允许我们像操作对象一样操作数据库。现在一般的做法是,增删改操作使用ORM,查询使用原生SQL。对于查询而言,越简单,越有效率(开发效率和执行效率)最好。
示意图如下:
客户端构建
接口的设计(无论是分离或是其他方式产生的)会影响实现接口的类型以及使用该接口的客户端。如果客户端要使用接口,就必须先以某种方式获得接口实例。为客户端提供接口实例的方式一定程度上取决于接口实现的数目。如果每个接口都有自己特有的实现,那么就需要构造所有的实现的实例并提供给客户端。如果所有接口的实现都包含在单个类中,那么只需要构建该类的实例就能满足客户端的所有依赖。
多实现、多实例
假设IRead、ISave和IDelete接口都有自己的实现类,客户端就需要同时引入这三个接口。这也是我们平常开发中最常用的一种方式,基于组合实现,需要哪个接口就引入对应的接口,类似于一种可插拔的组件式开发。
public class OrderController { private readonly IRead<Order> reader; private readonly ISave<Order> saver; private readonly IDelete<Order> deleter; public OrderController(IRead<Order> reader, ISave<Order> saver, IDelete<Order> deleter) { this.reader = reader; this.saver = saver; this.deleter = deleter; } public void CreateOrder(Order order) { saver.Save(order); } public Order GetOrder(Guid orderID) { return reader.ReadOne(orderID); } public void UpdateOrder(Order order) { saver.Save(order); } public void DeleteOrder(Order order) { deleter.Delete(order); } } 复制代码
单实现、单实例
此种方式是在 单个类 中继承并实现多个分离的接口,看上去也许有些反常(接口的分离的目的不是再次把它们统一在单个实现中)。常用于接口的叶子实现类,也就是说,既不是修饰器也不是适配器的实现类,而是完成工作的实现类。在叶子实现类上应用这种方式,是因为 叶子类中所有实现的上下文是一致的 。这种方式经常应用在和Entity Framework等持久化框架直接打交道的类。
public class CreateReadUpdateDelete<TEntity>: IRead<TEntity>,ISave<TEntity>,IDelete<TEntity> { public void Save(TEntity entity) { } public IEnumerable<TEntity> ReadAll() { return new List<TEntity>(); } public void Delete(TEntity entity) { } } public OrderController CreateSingleService() { var crud = new CreateReadUpdateDelete<Order>(); return new OrderController(crud,crud,crud); } 复制代码
超级接口反模式
把所有接口分离得来的接口又聚合在同一个接口下是一个 常见的错误 ,这些接口一起聚合构成了一个“超级接口”,这破坏了接口分离带来的好处。
public interface CreateReadUpdateDelete<TEntity>: IRead<TEntity>,ISave<TEntity>,IDelete<TEntity> { } 复制代码
总结
接口分离,无论是用来辅助修饰,还是为客户端隐藏它们不应该看到的功能,还是作为架构设计的产物。我们都应该在创建任何接口时牢记接口分离这个技术原则,而且最好是从一开始就应用接口分离原则。
以上所述就是小编给大家介绍的《编码最佳实践——接口分离原则》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 用 Go 开发接口服务--Go 语言编码规范
- 编码、摘要和加密(一)——字节编码
- 新媒体编码时代的技术:编码与传输
- MySQL数据库字符编码总结--数据传输编码
- PHP 开发者学 Golang 之 URL 编码 (Urlencode)、解编码 (Urldecode)
- ios平台实现视频H264硬编码及软编码(附完整demo)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
WebWork in Action
Jason Carreira、Patrick Lightbody / Manning / 01 September, 2005 / $44.95
WebWork helps developers build well-designed applications quickly by creating re-usable, modular, web-based applications. "WebWork in Action" is the first book to focus entirely on WebWork. Like a tru......一起来看看 《WebWork in Action》 这本书的介绍吧!