• [Effective C++ --018]让接口容易被正确使用,不易被误用


    □第一节 什么是接口?
    什么是接口?
    百度百科的解释:两个不同系统(或子程序)交接并通过它彼此作用的部分。
    接口的概念贯穿于整个软件开发过程中,本文讨论的接口概念仅局限于以C++实现的class,function等。


    class接口
    以OOP的思想,现实的问题总能抽象成一个个class,这些class包含哪些属性,方法其实就是class接口要考虑的问题。
    function接口
    class接口的操作就是通过一个个function来实现的,function的参数传递,返回值是function接口要考虑的问题。
    本条款“让接口容易被正确使用,不易被误用” 是设计优良接口重要准则之一,并在开始的时候就需要考虑的。

    “定义输入”
    要开发一个“让接口容易被正确使用,不易被误用”的接口,首先要考虑使用该接口可能产生的错误。考虑用来表示时间的Date类。

    class Date {
    private:
        int   year;   //年份
        short month;  //月份
        short day;    //天数
    public:
        Date(int year, short month, short day) {
            this->year = year;
            this->month = month;
            this->day = day;
        }
    };

    今天是2013/02/28,今天的日期可以表示为

    Date d(2013,02,28);

    那明天的日期可以表示为

    Date d(2013,13,01);

    问题来了,刚才手抖一下,把“03”敲成“13”。
    还有一个问题,假如不小心也可能出现情况:

    Date d(2013,28,02); //天数与月份反了

    上面的两个问题系统都不会被系统检测,也就是我们的接口接收到错误输入而没有做出任何阻止。


    “使用新类型”
    导入新类型来覆盖已有的数据类型是一个不错的方案。

    struct Year {
        explicit Year(int y):val(y) { }
        int val;
    };
    struct Month {
        explicit Month(int m):val(m) { }
        int val;
    };
    
    struct Day {
        explicit Day(int d):val(d) { }
        int val;
    };

    Date的构造函数可以是下面的样子:

    Date(const Year& y, const Month& m, const Day& d);
    
    Date d(Year(2013), Month(03), Day(01)); // OK
    Date d(Year(2013), Day(01), Month(03)); // NG,类型错误

    另外一个问题的解决很容易想到的方法是使用枚举:

    关于年份,似乎输入任何一个数值都是合法的,所以不需要枚举。
    关于月份,一年中只有12个月,所以可以枚举1~12的值。
    比如月份的枚举如下:

    enum Month {
        month1= 1,
        month2,
        ...
        month12,
    };

    那么修改后的Date类应该是下面的样子

    Date(Year year, Month month, Day day);

    这样做法可以解决一部分问题,至少不会出现月份越界的情况。
    于是明天的日期可以这么写:

    Date d(Year(2013),month(3) , Day(01));

    但很不幸C++中的枚举只是被拿来当作int值来用,例如下面的情况是可以通过编译的:

    Date d(Year(2013), (Month)13, Day(28));

    幸好,还有更厉害的一招:预定义所有有效的Months

    class Month {
    public:
         static  Month  Jan() { return Month(1); }
         static  Month  Feb() { return Month(2); }
        ...
        static  Month  Dec() { return Month(12); }
    private:
        explicit Month(int m);
    }
    Date d(Year(2013), Month::Feb(),Day(28));

    但这种做法的看起来很诡异,而且当要枚举的集合中有很多元素时,势必影响到代码生产效率和系统性能。

    总结
    在接口设计的时候引入新类型,限定边界值是保证接口的正确性的有效措施之一。
    但过分追求接口的正确性往往会带来系统逻辑复杂度的增加。

    □第二节 让接口更容易些
    “望文生义”
    每个人都有名字,名字的作用就是用来区分个体的。名字往往能给我们带来很多信息。比如我的名字叫“小明”,很容易知道
    我名字的读音“xiaoming”,是个男的。假如有个人叫“李芳”我说他是个男的,你信吗?

    函数也有名字,这个名字是给人读的。所以当给接口定义名称的时候最好通俗易懂,"见名知意"。

    “多重性”
    用过.NET的都知道,Arrays有个属性名为Length用于求数组中元素个数,ArrayLists中有个属性为Count,也是干这事的。
    相信肯定有过这样的经历:当使用元素个数的时候总是先尝试用Length,发现没有再换Count。是不是有点郁闷?

    “自己动手”
    有些事情自己能做,那就不要依赖别人。当接口的某些行为依赖于调用者来完成,那就犯了大忌。
    比如我们的ConductorFactory,创建完Conductor就不管了。比如下面的代码:

    cgi_CallStatus cgi_ConductorFactory::cgi_CreateSetSyslogConductor(cgi_SettingConductor *&conductor,
                                                                        cgi_Status &status)
    {
        status.okNg = CALL_OK;
    
        conductor = MMI_NEW cgi_SettingConductor(
            cgi_Translate807a::Request,
            cgi_Translate807a::Response,
            cgi_Translate807a::GetSize,
            NULL,
            NULL,
            NULL);
    
        if (conductor == NULL) {
            cgi_TranslatorCommon::SetErrorStatus(CGI_ERROR_NEW, CGI_CONDUCTOR_FACTORY,
                                                 CGI_FACTORY_SETSYSLOGCONDUCTOR, 1, status, NULL);
            return CALL_ERROR;
        }
    
        return CALL_OK;
    }

    conductor的销毁显然不是由ConductorFactory来负责,假如使用完没有delete,很显然内存泄漏就产生了。
    我们可以使用智能指针来解决:

    cgi_ConductorFactory::cgi_CreateSetSyslogConductor(std::tr1::shared_ptr<cgi_SettingConductor> conductor,  cgi_Status &status) {
    ...
    }

    什么是接口应该做的,什么是调用者要做的,在设计时一定要多加思考。

    总结
    容易理解的名字,一致的行为表现会让接口更容易使用,并且不会发生错误。接口尽量不要试图让调用者做些事情,因为它可能不听话。

    ■总结■
    1.合理考虑接口的输入,并尽可能的阻止错误数据的输入
    2.阻止错误的方法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任
    3.给接口一个容易理解的名字,告诉别人接口是干什么用的
    4.让接口的功能与其行为保持一致
    5.在系统中如果是做同一件事的,尽可能的保持接口名字的一致
    6.尽可能少的期待调用者要做的事情,因为不能保证调用者会按照要求去做

  • 相关阅读:
    sizeof()和 strlen()的区别 --- 个人笔记
    MySQL 生成自增流水号
    sql 逗号分隔列转行
    【转】.NET中lock的使用方法及注意事项
    【转】在一个SQL Server表中一行的多个列找出最大值
    使用 PIVOT 和 UNPIVOT
    List<T>转DataTable
    C# 分数计算类(加减乘除)
    【转】mysql执行计划介绍
    【转】SQL Server 存储:理解IAM 页
  • 原文地址:https://www.cnblogs.com/hustcser/p/4118864.html
Copyright © 2020-2023  润新知