Qizhen's profileLabVIEWPhotosBlogListsMore Tools Help

Blog


    模块接口 API 的两种设计方案

        假如你要设计一个程序模块,它的功能是读写 INI 文件。用户调用这个模块,就可以方便的把信息写入 INI 文件,或从其中读出信息。 
        你将如何设计这个模块的接口呢?LabVIEW 中常见的方式有两种,第一,为模块的每个方法都做一个子 VI,比如写数值型数据的方法做一个 VI,写字符串的做一个 VI,读字符串的一个 VI 等等;另一种方案:把所有的方法都放到一个子 VI 里去,用户通过一个变量来选择运行哪个方法。

        这两种方案各有优缺点。第一种方案符合一般人的思维模式,更容易让用户理解和学会使用。现在 LabVIEW 中处理 INI 文件的模块采用的就是这种方案。每个用户可能用到的方法(甚至是每一种数据类型),都有一个对应的 VI。维护起来也容易,哪个方法有 bug,到它对应的那个 VI 中去调试就可以了。

        但是打开这些处理 INI 文件的 VI,他们调用了一个更底层的模块,这个模块采用的是第二种接口方案。所有对 INI 文件底层的操作,都被放到了一个子 VI(Config Data Registry.vi)里。用输入参数("function")来控制执行不同的功能。
        这种方案也有它的好处,我看过一本叫做《软件工程方法在LabVIEW中的应用》的书,它的内容用一句话来概括,就是号召大家把模块都写成上述的第二种方案。不过我们先来说一下着第二种方案的弊端。

        首先,给外部用户的感觉就不如第一种方案那么清晰易学。如果把所有方法分开成独立的 VI,用户可以只专注学习自己可能会用到的功能对应的 VI;而第二种方案,所有功能在一个接口 VI 里,那就强迫用户把所有功能都要了解一下。
        其次,每种不同功能所用到的参数都不尽相同。采用第二种方案,就意味着这个唯一的接口 VI 要包含所有方法时用到的控件(参数)。所以这个 VI 上的控件会比较多。并且,有的控件在调用不同功能时,用途(或者说所表达的意思)不同。这样不但会造成用户学习的困难,在使用时,也非常容易出错。
        还有一条,第二种方案的效率在某些情况下非常低下。我们把一个模块提供给用户,但用户不见得会使用这个模块中所有的功能。第一种方案,用户程序是在编译时选择使用模块中的那些方法;而第二种方案是在运行时选择使用什么方法。如果用户只用到一个模块中的一两个功能,采用第二种方案,只用用户用到的方法相关的代码才会被链接到它的程序中;而采用第二个方案,不论用户是否需要,整个模块都会被链接到它的程序中去。
        这是因为这几个缺点,造成现在 LabVIEW 提供给用户的库中,几乎都是采用的第一种接口方案。

        但是,着第二种方案,一度是 LabVIEW 程序设计中一个非常流行的方法,自然也有他的优点。
        其一是更好的解决模块封装的问题。在 LabVIEW 8 之前,LabVIEW 本身不支持面向对象编程,也没有提供对一个模块进行封装的功能。我如果编写一个功能模块给用户,我这个模块中所有的 VI,即便是我只把它当作内部使用,都可以被用户调用。这是很不安全的,因为内部 VI 随时都可能被改变调整,从而引起客户应用程序的错误。如果所有的功能都通过一个 VI 暴露给用户,则用户更容易搞清楚只有这个 VI 他可以用,其它的 VI 都是不能被他直接使用的。并且这样也可以使自己编写的一大堆 VI 看上去也更像是一个模块或组件。
        LabVIEW 的另一个问题是,它作为数据流驱动的编程语言,不像文本语言那样可以方便的使用全局或局部变量。在 LabVIEW 中使用全局或局部变量不但效率查,还会严重影响程序的可维护性。我编写的模块,它所用到的内部数据如何组织呢?全局变量既然不好,那就只能考虑使用移位寄存器了。
        LabVIEW 程序如果设计的不好,数据在不同节点间传递时会产生很多份拷贝,造成效率低下。为了解决这个问题,最好是我内部使用数据,就不要再在 VI 之间传来传去了。打开 Config Data Registry.vi,你会发现这个 VI 的主体框架是一个只运行一次的循环。凡是这种只运行一次的循环,程序真正想利用的都是循环上的移位寄存器。这个 VI 里的多个移位寄存器都是既无输入又无输出的,它们的功能是用来保存模块的私有数据。 

        用移位寄存器保存模块的全部私有数据,模块的所有方法都在移位寄存器之间完成。这样数据始终在一个 VI 内,避免了数据在不同 VI 之间传递可能会引起的复制。这是很长一段时间内都相当流行的 LabVIEW 程序模块设计思路,不过我觉得也许现在可以放弃这个方案了。
        首先,这个实现方法只适合功能简单的小模块,模块的大部分代码都放到一个 VI 中。如果模块数据功能较多,还用这个方法编出来的 VI 就很难读懂,没法维护了。Config Data Registry.vi 虽然功能并不复杂,但代码已经不那么清晰易懂了。
        如果这个模块在程序中只有一个实例还好办,若要支持多个实例,那数据部分就要设计个更为复杂以确保模块不同实例之间的数据不会混乱。
        最重要的是现在 LabVIEW 自身已经开始支持面向对象的功能了。在 LVClass 中,既可以有数据,也可以有方法;方法可以被定义为是私有的或共有的;另外之支持继承、多态等。所有这些都为功能模块的封装和接口提供了更好的解决方案。与其费尽心机的自己想办法把格模块包装的更合理,不如直接利用 LVOOP 已有的功能。把自己的的模块都设计为 LVClass。

    《我和 LabVIEW》

    编辑

    Comments (7)

    Please wait...
    Sorry, the comment you entered is too long. Please shorten it.
    You didn't enter anything. Please try again.
    Sorry, we can't add your comment right now. Please try again later.
    To add a comment, you need permission from your parent. Ask for permission
    Your parent has turned off comments.
    Sorry, we can't delete your comment right now. Please try again later.
    You've exceeded the maximum number of comments that can be left in one day. Please try again in 24 hours.
    Your account has had the ability to leave comments disabled because our systems indicate that you may be spamming other users. If you believe that your account has been disabled in error please contact Windows Live support.
    Complete the security check below to finish leaving your comment.
    The characters you type in the security check must match the characters in the picture or audio.

    To add a comment, sign in with your Windows Live ID (if you use Hotmail, Messenger, or Xbox LIVE, you have a Windows Live ID). Sign in


    Don't have a Windows Live ID? Sign up

    Qizhen Ruanwrote:
    回Mach,现在LabVIEW有个模块是 Internet Toolkit 可以解析 XML 文件。不过将来,LabVIEW 本身也会具备 XML 解析功能的。我自己设计程序中也是都开始使用 XML 文件来保存数据了。
    July 17
    Qizhen Ruanwrote:
    这个不是我,是他们弄错了。
    July 5
    在NI的labview zone里看到你的推荐了。
    Qizhen Ruan - LabVIEW Tips for Chinese Developers                       
    Software Engineer
    LabVIEW
    不过不知道怎么连到一个英文的blog上了,也是你的吗?
    July 5
    杰 高wrote:
    还是觉得第二种比较好吧。
    简单的程序用第一种,比较复杂的,或者继承性好的,还是第二种。
    很明显的一点,如果采用第一种方法,有不少代码是重复的。
    再深入到软件工程的角度的话,有点高深了,俺没有研究。呵呵。至少是没有把这样的编程经验提炼到广泛应用的角度。
    July 4
    Picture of Anonymous
    Mach wrote:
    第二种好,它降低了系统耦合度。驱动程序里就是这样,除了CreateFile和CloseHandle, 中间的操作全都是用一个IOCTRL结构传统参数,这样很方便添加新的功能。
    ps. LabVIEW现在有XML模块了么?我现在做软件用ini的地方全都XML化了,好处很多。。。
    July 2
    Yongqing Yewrote:
    嗯,这两个方法各有优缺点吧,要看实际运用的情况。不多倒是可以讨论一下,应该比较有意思。
    June 29
    arrowpig xuwrote:
    Qizhen,我看过一些API设计以后,觉得这似乎是一种普遍的做法。不单单在LabVIEW中,我去看了一下socket的API设计。报文的接受和发送中,从用户调用的角度说一共有3对库函数,recv()/send(),recvfrom()/sendto()以及recvmsg()/sendmsg(),但是最终都归结于一个统计的系统调用,在内核中的入口为sys_socketcall()。
    recv()/send()是suppose连接已经被建立(connect已经被成功调用),所以这两个函数的借口参数中没有目标地址。而recvfrom()和sendto()就需要填写目标地址。在内核中,sys_recv()实际上通过sys_recvfrom()实现,而sys_send()通过sys_sendto()实现。而最终在内核里,三个用于接收的函数最后全部通过sock_recvmsg()来接受报文,而三个用于发送的函数全都是通过sock_sendmsg()来发送报文的。
    写到这里,我觉得这个还是一个不错的主题呢,在Large-Scale C++ Development和Design Pattern都有对这个的讨论,等我总结一些贴出来.
    June 29

    Trackbacks

    The trackback URL for this entry is:
    http://ruanqizhen.spaces.live.com/blog/cns!5852D4F797C53FB6!2375.trak
    Weblogs that reference this entry
    • None