代码改变世界

【面向对象设计原则】之接口隔离原则(ISP)

2017-05-27 16:39  蓝之风  阅读(...)  评论(...编辑  收藏

接口隔离原则(Interface  Segregation Principle, ISP):使用多个专门的接口,而不使用单一的总接口,即客户端不应该依赖那些它不需要的接口。

从接口隔离原则的定义可以看出,他似乎跟SRP有许多相似之处。 是的其实ISP和SRP都是强调职责的单一性, 接口隔离原则告诉我们在定义接口的时候要根据职责定义“较小”的接口,不要定义“高大全”的接口。也就是说接口要尽可能的职责单一,这样更容易复用,暴露给客户端的方法更具有“针对性”, 比如定义一个接口包括一堆访问数据库的方法, 有包括一堆访问网络的方法,还包括一些权限认证的方法。 把这么一摊子风牛马不相及的方法封装到一个接口里面,显然是不合适的, 如果客户程序只想用到数据访问的一些功能,但是调用接口的时候你把访问网络的方法和权限认证的方法暴露给客户,这使得客户程序感到“疑惑”,那么这个接口就不ISP,它很显然的构成了接口污染。

注意: 这里所说的接口是广义上的接口,他是一组契约, 是提供给程序交互的一组约定,并非各种语言interface 关键字定义的一组方法的结集合。但是这里所说的接口可以用各种语言的关键字interface 来定义,当然也可以用抽象类,类等等来定义。

假设有个客户提出了软件系统的需求:

1. 用户可以使用第三方QQ,微信,微博登录到系统。

2.系统中包括人员管理人员管理。

3.访问第三方的API获取一些数据。

好了拿到这个需求后首先经过分析,简单的原型设计,数据库设计之后开始编写代码了。 通常第一步定义接口。很快接口就定义出来了如下:

    public interface IObject
    {
        void Connection(string connectionString);
        SqlDataReader ExcuteSql(string sql);
        string LoginWithQQ(string token);
        string LoginWithWeibo(string token);
        string LoginWithWeiXin(string token);
        string GetDataFromAPI(string url, string token);
    }

这个看起来还不错,接口已经定义了,写个具体类继承一下这个接口并实现所有的方法,现在就可以实现业务,写界面了。 等过了几天客户说 在给我加上支付宝登录。那好再加一个支付宝登录接口,代码现在长这样子:

    public interface IObject
    {
        void Connection(string connectionString);
        SqlDataReader ExcuteSql(string sql);
        string LoginWithQQ(string token);
        string LoginWithWeibo(string token);
        string LoginWithWeiXin(string token);
        string GetDataFromAPI(string url, string token);
        string LoginWithAlipay(string token);
    }

再在实现类中实现一下LoginWithAlipay方法 就好了。

时间在推移,一天客户说再给我加个百度登录,好吧套路有了加一个就是了,有啥了不起。 时间依旧。。。 客户说加个 facebook 登录, 。。。加个 Linkedin。。。, 尼玛 没完没了了, 现在接口已经变成这样子了:

    public interface IObject
    {
        void Connection(string connectionString);
        SqlDataReader ExcuteSql(string sql);
        string LoginWithQQ(string token);
        string LoginWithWeibo(string token);
        string LoginWithWeiXin(string token);
        string GetDataFromAPI(string url, string token);
        string LoginWithAlipay(string token);
        string LoginWithTwitter(string token);
        string LoginWithFaceBook(string token);
        string LoginWithRenRen(string token);
        string LoginWithBaidu(string token);
        string LoginWithDropbox(string token);
        string LoginWithGithub(string token);
        //这里省略10000字       
stringLoginWithLinkedin(string token);
    }

有一天这个接口自己都不想看了,太多方法了,更何况实现类中的代码都七八千行了。

于是决定重构, 现在回头看看这个接口早就应该重构了,甚至一开始定义的时候就应该拆分,接口的名字都不知道怎么命名(一般在写代码的时候类,接口,方法的名字不知道怎么命名的时候就是该重构的时候了)竟然起了IObject这么奇葩的名字,这个设计显然是烂到家了, 他几乎违背了我们讲过的所有设计原则, 必须到了要重构的时候了。

来吧,重构吧,经过分析第一步先根据功能来划分将IObject接口拆分成三个“小”接口:

1.数据库操作相关的抽取到一个接口中(IDatabaseProvider)。

2.第三方API调用相关的方法抽取到一个接口中(IThirdpartyAPIProvider)。

3.第三方登陆相关的方法抽取到一个接口中(IThirdpartyAuthenticationProvider)。

现在代码变成这个样子:

    public interface IDatabaseProvider
    {
        SqlDataReader ExcuteSql(string sql);
        string LoginWithQQ(string token);
    }

    public interface IThirdpartyAPIProvider
    {
        string Get(string url, string token);
    }

    public interface IThirdpartyAuthenticationProvider
    {
        string LoginWithQQ(string token);
        string LoginWithWeibo(string token);
        string LoginWithWeiXin(string token);
        string LoginWithAlipay(string token);
        string LoginWithTwitter(string token);
        string LoginWithFaceBook(string token);
        string LoginWithRenRen(string token);
        string LoginWithBaidu(string token);
        string LoginWithDropbox(string token);
        string LoginWithGithub(string token);
        //这里省略10000字
        string LoginWithLinkedin(string token);
    }

这下看起来好多了, 但是IThirdpartyAuthenticationProvider 代码还很多,还很丑陋,有没有办法再进一步重构呢? 答案是肯定。 第二步 我们可以将第三方登录的接口中的LogigWithxxx方法提到一个单独的接口中,其他具体站点的接口再继承这个接口,代码如下:

    public interface IThirdpartyAuthenticationProvider
    {
        string Login(string token);
    }

    public interface IQQAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IWeiboAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IWeiXinAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IAlipayAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface ITwitterAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IFaceBookAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IRenRenAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IBaiduAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    public interface IDropboxAuthenticationProvider : IThirdpartyAuthenticationProvider { }
    public interface IGitHubAuthenticationProvider:IThirdpartyAuthenticationProvider{}
    //这里省略10000字
    public interface ILinkedinAuthenticationProvider : IThirdpartyAuthenticationProvider { }

这这下就好多了。 我们分析一下重构后的代码有什么好处:

1. 接口的职责更单一了,调用目标更清晰了,每一个接口就专门做一件事情。符合SRP了。

2. 在操作数据库的时候不会在IDatabase接口中调到其它的第三方API调用和第三方登录认证相关的方法,每一个几口更专注了。符合ISP了。

3.在添加新的第三方登录的时候不需要在修改原来的实现 类了,核心业务逻辑只需要加一个接口和接口的实现类就可以了。符合OCP了。

4. 提升了代码的稳定性,可维护性和可扩展性。

当然任何事情都具有两面性,如果将一件好事做到极端有可能就会走向反面, 比方说定义一个User实体的接口:

public interface IIdProperty { int Id { get; set; } }
public interface IFirstNameProperty { string FirstName { get; set; } }
public interface ILastNameProperty { string LastName { get; set; } }
public interface IAgeProperty { int Age { get; set; } }
public interface IBirthdayProperty { DateTime Birthday { get; set; } }

public class User:IIdProperty,IFirstNameProperty,ILastNameProperty,IAgeProperty,IBirthdayProperty
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }
    public DateTime Birthday { get; set; }
}

把每个属性都定义成一个接口那就不可取了,也是没有意义的,反而给维护或扩展带来不必要的麻烦,这就是使用接口时要注意的地方:

在使用接口时要注意控制接口的粒度,接口定义的粒度不能太细,也不能太粗。 接口粒度太细,系统中就会出现接口泛滥,接口和实现类急剧膨胀,反而不易维护;接口粒度太粗,就会违背ISP,系统的灵活性就会降低,不易维护和扩展。

关联阅读:

【面向对象设计原则】之原则概述

【面向对象设计原则】之单一职责原则(SRP)

【面向对象设计原则】之开闭原则(OCP)

【面向对象设计原则】之里氏替换原则(LSP)

【面向对象设计原则】之依赖倒置原则(DIP)