zoukankan      html  css  js  c++  java
  • 期待的接口

    《编程絮语》之三

    定义接口时需要注意什么?是实现,还是消费?窃以为,接口是抽象了的服务,服务的消费者只会关心服务能够提供什么,而不会考虑服务如何实现。例如在ATM机上取款,取款人只需要考虑怎样插入储蓄卡,怎么选择功能项,然后输入正确的密码和取款金额,再等待正确数额的钞票从机器中吐出,最后取走。至于内部的实现机制,则不在取款人的思考范畴。因此,接口必须符合调用者的期待,不然就会给设计带来障碍。接口的定义是为调用者准备的,接口具备的方法以及方法具备的签名,都必须站在调用者的角度来考虑。当调用者是测试用例时,这样的设计就变成了测试驱动设计。

    例如编写一个银行账务管理系统,存取款服务的接口定义应该是这样:

    public interface IBankService

    {

        bool Withdraw(Money money);

        bool Deposit(Money money);

    }

     

    在IBankService的实现类中,会通过调用一个Account类,实现存款和取款的功能:

    public class Account

    {

        public bool Add(Money money)

        {

            //实现

        }

        public bool Substract(Money money)

        {

            //实现

        }

    }

    public class BankServiceImpl : IBankService

    {

        private Account m_account;

     

        public BankServiceImpl(Account account)

        {

            m_account = account;

        }

     

        public bool Withdraw(Money money)

        {

            try

            {

                m_account.Substract(money);

                return true;

            }

            catch

            {

                return false;

            }

        }

        public bool Deposit(Money money)

        {

            try

            {

                m_account.Add(money);

                return true;

            }

            catch

            {

                return false;

            }

        }

    }

    注意IBankService和Account的方法名,两者均实现了存取款功能,为何名称大相径庭?原因就在于对象的调用者并不相同。IBankService暴露给UI,实际上就代表了它是与存取款业务直接相关的。Withdraw和Deposit的命名正好符合这样的逻辑。对于Account而言,表现出来的是帐户上的余额是增加或减少,它并不知道存取款的业务逻辑。

     

    虽然设计者才是接口定义的主宰,然而调用者作为顾客,他才是真正的上帝。调用者说:“我希望使用这样的接口。”潜在的含义是,当我创建这样的实现类时,当我传递需要的输入实参时,你已经帮我考虑好了。调用者就像是守候在无人售货机前的顾客,选一罐可口可乐,然后按价塞入相应的钱币,就听到叮里咣当,最后滚出的一定是一罐可口可乐,而不是百事可乐。接口的设计者需要考虑调用者的感受,同时却不能对调用者做出任何假设。无人售货机如果只能接收五元和十元的纸币,就必须能够防止顾客放入错误的钱币。

     

    一旦服务提供的接口并不符合调用者的期待,就存在一个“适配”的工作,这正是Adapter模式的意图。Adapter对象是一个高明的调解人,负责将两个不协调的接口统一,既有效地保证了第三方接口对象的重用,又能够很好的支持服务的扩展。

     

    虽然服务的定义者必须要符合调用者的期待,但反过来,定义者也给予了调用者一定的限制。此时,接口代表一种规约,它是对调用者进行了合理的限制。以Java的线程处理为例,就要求执行多线程逻辑的对象必须要实现Runnable接口,否则Thread的start()方法就不能执行。

    class MyThreadStart implements Runnable
    {
        public void run()
        { //执行相关操作 }
    }
    Thread controller = new Thread(new MyThreadStart());
    controller.start();

    如果方法要求传入的参数类型为抽象类型,则表明该方法的实现可能是变化的。这同样属于对接口的期待。好的设计不应该与具体类型耦合在一处,而是应该将创建具体对象的职责交由调用者去决定。“依赖注入”的方式正是基于这一点,例如Order实体对象的定义:

    public interface IOrderRepository { }

     

    public class Order

    {

        public Order(IOrderRepository repository) { }

    }

    Order类的定义对IOrderRepository接口存在一个期待,即我们应该传入实现IOrderRepository接口的具体类对象,而不是其他类型。这种对接口的期待是开放的。例如,我们可以在单元测试的时候,考虑定义MockOrderRepository类去实现IOrderRepository接口,从而完成对真正的资源库对象进行模仿。

    如果实现接口的类包含的公开方法比接口宽,就需要思考这样的设计是否合理。因为,这意味着这些公开方法对扩展是封闭的,违背了开放封闭原则。调用者在调用这样的类时,如果仍然采用多态的方式去调用,则需要对接口类型进行强制转换。例如:

    public interface IConfigReader

    {

        string Read(string section);

    }

     

    public class XmlConfigHandler : IConfigReader

    {

        public string Read(string section) { }

        public void Write(string section,string value) { }

    }

     

    public class ConfigSettingManager

    {

        private string m_section;

        public ConfigSettingManager(string section)

        {

            m_section = section;

        }

        public void Config(IConfigReader reader)

        {

            string value = reader.Read(m_section);

     

            if (value != expectedValue)

            {

                ((XmlConfigHandler)reader).Write(m_section, expectedValue);

            }

        }

    }

    如上的设计是不协调的。Config()方法的实现破坏了程序结构的平衡与和谐。它带来两个问题。其一,方法的实现与期待不符。既然参数类型为IConfigReader,则表明Config方法期待对配置文件的读功能。那么,对写功能的调用就是不合理的。其二,方法引入了与XmlConfigHandler的具体依赖关系,从而让IConfigReader接口对可扩展性做出的努力付诸东流。

    对上述设计的修改基于两种不同的策略,有两种不同的结果。如果对接口方法的期待更加细粒度,即希望分别对读操作和写操作进行区别对待,可以再定义一个IConfigWriter接口。如果XmlConfigHandler类需要实现Write()方法,就可以实现IConfigWriter接口。这就对Write()方法实现了抽象,使得它与Read()方法能够处于相同的抽象层面。如果不需要做这样的区别对待,且读操作和写操作的变化方向与变化粒度是一致的,就可以将其定义为一个统一的接口,例如IConfigHandler:

    public interface IConfigHandler

    {

        string Read(string section) ;

        void Write(string section, string value) ;

    }

    所以,在通常情况下,我们不应该对传入的接口对象进行强制类型转换。接口是设计者对调用者的一种约束和控制,如果进行强制类型转换,说明设计者自己违背了这样的约束,丢失了对调用者的控制力。只有一种例外,即标记接口的使用。Uncle Bob在《敏捷软件开发》中提出的Acyclic Visitor模式即使用了这样的标记接口,如下所示:

    public interface ModemVisitor

    { }

     

    public interface HayesVisitor

    {

        void visit(Hayes modem);

    }

     

    public class Hayes

    {

        public void accept(ModemVisitor v)

        {

            try

            {

                HayesVisitor hv = (HayesVisitor)v;

                hv.visit(this);

            }

            catch (){}

        }

    }

    标记接口通常被定义为空。它不是为了调用者的期待而定义,其意图是抽象,将那些不能抽象在一起的类,利用一个标记绑定起来,为其提供统一的接口。标记接口保证了调用方法的一致性。虽然强制类型转换会引入具体依赖,却不会有任何副作用,因为在方法实现中,设计者的期待本身就是要转换的类型。这里不存在扩展,如Hayes类中accept()方法的实现,它期待的只能是HayesVisitor类型。

  • 相关阅读:
    Asp.net Url Routing
    关于Linq to DataSet
    WebService开发实例(Axis2实现,无需安装,快速实现)
    开启我的博客之旅
    字符串算法专题之:目录
    递归程序转换为非递归程序的方法
    微信与QQ的比较和定位分析
    字符串算法专题之:字符串匹配算法
    递归程序的含义、实现机制以及复杂度计算
    北洋大讲堂之“斯凯网络CEO宋涛我的创业之路”感想
  • 原文地址:https://www.cnblogs.com/wayfarer/p/1683367.html
Copyright © 2011-2022 走看看