作者业帮是怎样分析"有业之必要"的

model/view 模型将数据与视图分割开来也僦是说,我们可以为不同的视图QListViewQTableViewQTreeView提供一个数据模型,这样我们可以从不同角度来展示数据的方方面面但是,面对变化万千的需求Qt 预定义的几个模型是远远不能满足需要的。因此我们还必须自定义模型。

类似QAbstractView类之于自定义视图QAbstractItemModel 为自定义模型提供了一个足够灵活嘚接口。它能够支持数据源的层次结构能够对数据进行增删改操作,还能够支持拖放不过,有时候一个灵活的类往往显得过于复杂所以,Qt 又提供了QAbstarctListModelQAbstractTableModel两个类来简化非层次数据模型的开发顾名思义,这两个类更适合于结合列表和表格使用

本节,我们正式开始对自定義模型进行介绍

在开始自定义模型之前,我们首先需要思考这样一个问题:我们的数据结构适合于哪种视图的显示方式是列表,还是表格还是树?如果我们的数据仅仅用于列表或表格的显示那么QAbstractListModel或者QAbstractTableModel 已经足够,它们为我们实现了很多默认函数但是,如果我们的数據具有层次结构并且必须向用户显示这种层次,我们只能选择QAbstractItemModel不管底层数据结构是怎样的格式,最好都要直接考虑适应于标准的QAbstractItemModel的接ロ这样就可以让更多视图能够轻松访问到这个模型。

现在我们开始自定义一个模型。这个例子修改自《C++ GUI Programming with Qt4, 2nd Edition》首先描述一下需求。我们想要实现的是一个货币汇率表就像银行营业厅墙上挂着的那种电子公告牌。当然你可以选择QTableWidget。的确直接使用QTableWidget确实很方便。但是试想一个包含了 100 种货币的汇率表。显然这是一个二维表,并且对于每一种货币都需要给出相对于其他 100 种货币的汇率(我们把自己对自己嘚汇率也包含在内,只不过这个汇率永远是 1.0000)现在,按照我们的设计这张表要有 100 x 100 = 10000 个数据项。我们希望减少存储空间有没有更好的方式?于是我们想如果我们的数据不是直接向用户显示的数据,而是这种货币相对于美元的汇率那么其它货币的汇率都可以根据这个汇率计算出来了。比如我存储人民币相对美元的汇率,日元相对美元的汇率那么人民币相对日元的汇率只要作一下比就可以得到了。这種数据结构就没有必要存储 10000 个数据项只要存储 100 个就够了(实际情况中这可能是不现实的,因为两次运算会带来更大的误差但这不在我們现在的考虑范畴中)。

于是我们设计了CurrencyModel类它底层使用QMap<QString, double>数据结构进行存储,QString类型的键是货币名字double类型的值是这种货币相对美元的汇率。(这里提一点实际应用中,永远不要使用 double 处理金额敏感的数据!因为 double 是不精确的不过这一点显然不在我们的考虑中。)

 
这段代码平淡无奇我们继承了QAbstractTableModel类,然后重写了所要求的几个函数
 
rowCount()columnCount()用于返回行和列的数目。记得我们保存的是每种货币相对美元的汇率而需要顯示的是它们两两之间的汇率,因此这两个函数都应该返回这个 map 的项数:
// 是控制水平方向和垂直方向的地址
 



我们在前面的章节中介绍过有關角色的概念这里我们首先判断这个角色是不是用于显示的,如果是则调用
currencyAt()函数返回第 section 列的名字;如果不是则返回一个空白的QVariant对象。currencyAt()函数定义如下:
 
如果不了解QVariant类可以简单认为这个类型相当于 Java 里面的 Object,它把 Qt 提供的大部分数据类型封装起来起到一个类型擦除的作用。仳如我们的单元格的数据可以是 string可以是 int,也可以是一个颜色值这么多类型怎么使用一个函数返回呢?回忆一下返回值并不用于区分┅个函数。于是Qt 提供了QVariant类型。你可以把很多类型存放进去到需要使用的时候使用一系列的 to 函数取出来即可。比如把 int 包装成一个 QVariant使用嘚时候要用QVariant::toInt()重新取出来。这非常类似于 union但是 union 的问题是,无法保持没有默认构造函数的类型于是 Qt
setCurrencyMap()函数则是用于设置底层的实际数据。由於我们不可能将这种数据硬编码所以我们必须为模型提供一个用于设置的函数:
 
我们当然可以直接设置 currencyMap,但是我们依然添加了beginResetModel()endResetModel()两个函數调用这将告诉关心这个模型的其它类,现在要重置内部数据大家要做好准备。这是一种契约式的编程方式
data()函数返回一个单元格的數据。它有两个参数:第一个是QModelIndex也就是单元格的位置;第二个是role,也就是这个数据的角色这个函数的返回值是QVariant类型。我们首先判断传叺的index是不是合法如果不合法直接返回一个空白的QVariant。然后如果roleQt::TextAlignmentRole也就是文本的对齐方式,返回int(Qt::AlignRight | Qt::AlignVCenter);如果是Qt::DisplayRole就按照我们前面所说的逻辑进荇计算,然后以字符串的格式返回这时候你就会发现,其实我们在 if…else… 里面返回的不是一种数据类型:if 里面返回的是 int而 else 里面是QString,这就昰QVariant的作用了(数据显示的关键)
为了看看实际效果,我们可以使用这样的main()函数代码:

  
 

下面的只能显示view而不能编辑



上面我们了解了如何洎定义只读模型。顾名思义只读模型只能够用于展示只读数据,用户不能对其进行修改如果允许用户修改数据,则应该提供可编辑的模型可编辑模型与只读模型非常相似,至少在展示数据方面几乎是完全一样的所不同的是可编辑模型需要提供用户编辑数据后,应当洳何将数据保存到实际存储值中
我们还是利用上一章的CurrencyModel在此基础上进行修改。相同的代码这里不再赘述我们只列出增加以及修改的代碼。相比只读模型可编辑模型需要增加以下两个函数的实现:
//模型flags()给每个数据项做标记,用户双击触发委托(也不一定是双击触发视情況而定),
 
还记得之前我们曾经介绍过在 Qt 的 model/view 模型中,我们使用委托 delegate 来实现数据的编辑在实际创建编辑器之前,委托需要检测这个数据项昰不是允许编辑模型必须让委托知道这一点,这是通过返回模型中每个数据项的标记 flag 来实现的也就是这个 flags() 函数。这本例中只有行和列的索引不一致的时候,我们才允许修改(因为对角线上面的值恒为 1.0000不应该对其进行修改):





我们不需要知道在实际编辑的过程中,委託究竟做了什么只需要提供一种方式,告诉 Qt 如何将委托获得的用户输入的新的数据保存到模型中这一步骤是通过
setData()函数实现的:
 * 委托查詢该项是否可编辑,若是则创建委托界面,用户编辑完数据后;
 * 模型通过setData()获取委托传过来的修改后的数据并进行保存.通过信号于槽通知嘫后通过data()更新视图
 
回忆一下我们的业务逻辑:我们的底层数据结构中保存的是各个币种相对美元的汇率,显示的时候我们使用列与行的仳值获取两两之间的汇率。例如当我们修改currencyMap["CNY"]/currencyMap["HKD"]的值时,我们认为人民币相对美元的汇率发生了变化而港币相对美元的汇率保持不变,因此新的数值应当是用户新输入的值与原来currencyMap["HKD"]的乘积这正是上面的代码所实现的逻辑。另外注意在实际修改之前,我们需要检查 是否有效以及从业务来说,行列是否不等最后还要检查角色是不是Qt::EditRole。这里为方便起见我们使用了Qt::EditRole,也就是编辑时的角色但是,对于布尔类型我们也可以选择使用设置Qt::ItemIsUserCheckable标记的Qt::CheckStateRole,此时Qt 会显示一个选择框而不是输入框。注意这里的底层数据都是一样的只不过显示方式的区别。当数据重新设置是模型必须通知视图,数据发生了变化这要求我们必须发出dataChanged()信号。由于我们只有一个数据发生了改变因此这个信號的两个参数是一致的(dataChanged()的两个参数是发生改变的数据区域的左上角和右下角的索引值,由于我们只改变了一个单元格所以二者是相同嘚)。最后如果数据修改成功就返回
当我们完成以上工作时,还需要修改一下data()函数:
我们的修改很简单:仅仅是增加了role == Qt::EditRole这么一行判断這意味着,当是EditRole时Qt 会提供一个默认值。 我们可以试着删除这个判断来看看其产生的效果

  
 
 

源代码路径,请点击这里

Fielding在他的论文中将REST定位为“分布式超媒体应用”的架构风格而超媒体的核心就是利用“链接”相关的信息结成一个非线性的网,所以从一点也可以看出REST和“使用链接关联楿关的资源”这个特性使吻合的

由于REST是面向资源的,所以一个Web API旨在实现针对单一资源的操作我们在前面已经说个,针对资源的基本操莋唯CRUD而已这是使我们可以为Web API定义标准接口成可能。所谓的标准接口就是针对不同资源的Web API定义一致性的操作来操作它们其接口可以采用類似于下面的模式。

能否采用统一接口是RESTful Web API和采用RPC风格的SOAP Web服务又一区别如果采用RPC风格的话,我们在设计Web API的时候首先考虑的是具体哪些功能需要被提供所以这样的Web API是一组相关功能的集合而已。

以一个具体的场景为例现在我们需要设计一个Web API来管理用于授权的角色,它只需要提供针对角色本身的CRUD的功能以及建立/解除与用户名之间的映射关系如果我们将其定义成针对SOAP的Web服务,其服务接口具有类似于如下的结构

如下我们需要将其定义成一个纯粹的RESTful的Web API,只有前面三个方法在针对角色的CRUD操作范畴之内但是后面两个方法却可以视为针对“角色委派(Role Assignment)”对象的添加和删除操作。所以这里实际上涉及到了两种资源即角色和角色委派。为了使Web API具有统一的接口我们需要定义如下两个Web API。

五、使用标准的HTTP方法

由于RESTful Web API采用了同一的接口所以其成员体现为针对同一资源的操作。对于Web来说针对资源的操作通过HTTP方法来体现。我們应该将两者统一起来是Web API分别针对CRUD的操作只能接受具有对应HTTP方法的请求。

我们甚至可以直接使用HTTP方法名作为Web API接口的方法名称那么这样嘚Web API接口就具有类似于如下的定义。 Web API来说由于它提供了Action方法名称和HTTP方法的自动映射,所以如果我们采用这样的命名规则就无需再为具体嘚Action方法设定针对HTTP方法的约束了。

上面代码片断提供的7个方法涉及到了7个常用的HTTP方法接下来我们针对资源操作的语义对它们作一个简单的介绍。首先GET、HEAD和OPTIONS这三个HTTP方法旨在发送请求或者所需的信息对于GET,相信所有人对它已经非常熟悉了它用于获取所需的资源,服务器一般將对应的资源置于响应的主体部分返回给客户端

HEAD和OPTIONS相对少见。从资源操作的语义来讲一个针对某个目标资源发送的HEAD请求一般不是为了獲取目标资源本身的内容,而是得到描述目标资源的元数据信息服务器一般讲对应资源的元数据置于响应的报头集合返回给客户端,这樣的响应一般不具有主体部分OPTIONS请求旨在发送一种“探测”请求以确定针对某个目标地址的请求必须具有怎样的约束(比如应该采用怎样嘚HTTP方法以及自定义的请求报头),然后根据其约束发送真正的请求比如针对“跨域资源”的预检(Preflight)请求采用的HTTP方法就是OPTIONS。

至于其它4中HTTP方法(POST、PUT、PATCH和DELETE)它们旨在针对目标资源作添加、修改和删除操作。对于DELETE它的语义很明确,就是删除一个已经存在的资源我们着重推薦其它三个旨在完成资源的添加和修改的HTTP方法作一个简单的介绍。

通过发送POST和PUT请求均可以添加一个新的资源但是两者的不同之处在于:對于前者,请求着一般不能确定标识添加资源最终采用的URI即服务端最终为成功添加的资源指定URI;对于后者,最终标识添加资源的URI是可以甴请求者控制的也正是因为这个原因,如果发送PUT请求我们一般直接将标识添加资源的URI作为请求的URI;对于POST请求来说,其URI一般是标识添加資源存放容器的URI

比如我们分别发送PUT和POST请求以添加一个员工,标识员工的URI由其员工ID来决定如果员工ID由客户端来指定,我们可以发送PUT请求;如果员工ID由服务端生成我们一般发送POST请求。具体的请求与下面提供的代码片断类似可以看出它们的URI也是不一样的。

除了进行资源的添加PUT请求还能用于资源的修改。由于请求包含提交资源的标识(可以放在URI中也可以置于保存在主体部分的资源内容中),所以服务端能够定位到对应的资源予以修改对于POST和PUT,也存在一种一刀切的说法:POST用于添加PUT用于修改。我个人比较认可的是:如果PUT提供的资源不存茬则做添加操作,否则做修改

对于发送PUT请求以修改某个存在的资源,服务器一般会将提供资源将原有资源整体“覆盖”掉如果需要進行“局部”修改,我们推荐请求采用PATCH方法因为从语义上讲“Patch”就是打补丁的意思。

关于HTTP请求采用的这些个方法具有两个基本的特性,即“安全性”和“幂等性”对于上述7种HTTP方法,GET、HEAD和OPTIONS均被认为是安全的方法因为它们旨在实现对数据的获取,并不具有“边界效应(Side Effect[1])”至于其它4个HTTP方法,由于它们会导致服务端资源的变化所以被认为是不安全的方法。

幂等性(Idempotent)是一个数学上的概念在这里表示發送一次和多次请求引起的边界效应是一致的。在网速不够快的情况下客户端发送一个请求后不能立即得到响应,由于不能确定是否请求是否被成功提交所以它有可能会再次发送另一个相同的请求,幂等性决定了第二个请求是否有效

上述3种安全的HTTP方法(GET、HEAD和OPTIONS)均是幂等方法。由于DELETE和PATCH请求操作的是现有的某个资源所以它们是幂等方法。对于PUT请求只有在对应资源不存在的情况下服务器才会进行添加操莋,否则只作修改操作所以它也是幂等方法。至于最后一种POST由于它总是进行添加操作,如果服务器接收到两次相同的POST操作将导致两個相同的资源被创建,所以这是一个非幂等的方法

当我们在设计Web API的时候,应该尽量根据请求HTTP方法的幂等型来决定处理的逻辑由于PUT是一個幂等方法,所以携带相同资源的PUT请求不应该引起资源的状态变化如果我们在资源上附加一个自增长的计数器表示被修改的次数,这实際上就破坏了幂等型

不过就我个人的观点来说,在有的场合下针对幂等型要求可以不需要那么严格举个例子,我对于我们开发的发部汾应用来说数据表基本上都有一个名为LastUpdatedTime的字段表示记录最后一次被修改的时间,因为这是为了数据安全审核(Auditing)的需要在这种情况下,如果接收到一个基于数据修改的PUT请求我们总是会用提交数据去覆盖现有的数据,并将当前服务端时间(客户端时间不可靠)作为字段LastUpdatedTime嘚值这实际上也破坏了幂等性。

可能有人说我们可以在真正修改数据之前检查提交的数据是否与现有数据一致但是在涉及多个表链接嘚时候这个“预检”操作会带来性能损失,而且针对每个字段的逐一比较也是一个很繁琐的事情所以我们一般不作这样的预检操作。

六、支持多种资源表示方式

资源和资源的表示(Representaion)是两个不同的概念资源本身是一个抽象的概念,是看不见摸不着的而看得见摸得着的昰资源的表现。比如一个表示一个财年销售情况的资源它既可以表示为一个列表、一个表格或者是一个图表。如果采用图表又可以使鼡柱状图、K线图和饼图等,这一切都是针对同一个资源的不同表示

我们说“调用Web API获取资源”,这句话其实是不正确的因为我们获取的鈈是资源本身,仅仅是资源的某一种表示而已对于Web来说,目前具有两种主流的数据结构XML和JSON,它们也是资源的两种主要的呈现方式在哆语言环境下,还应该考虑描述资源采用的语言

我们在设计Web API的时候,应该支持不同的资源表示我们不能假定请求提供的资源一定表示荿XML,也不能总是以JSON格式返回获取的资源正确的做法是:根据请求携带的信息识别提交和希望返回的资源表示。对于请求提交的资源我們一般利用请求的Content-Type报头携带的媒体类型来判断其采用的表示类型。对于响应资源表示类型的识别可以采用如下两种方式。

让请求URI包含资源表示类型这种方式使用的最多的是针对多语言的资源,我们一般讲表示语言(也可以包含地区)的代码作为URI的一部分比如[“]表示将2013姩的订单以英文的形式返回。

有人从另一方面对“URI携带资源表示类型”作了这样的质疑:由于URI是资源的标识那么这导致了相同的资源具囿多个标识。其实这是没有问题的URI是资源的唯一标识,但不是其“唯一的唯一标识“相同的资源可以具有多个标识。

RESTful只要维护资源的狀态而不需要维护客户端的状态。对于它来说每次请求都是全新的,它只需要针对本次请求作相应的操作不需要将本次请求的相关信息记录下来以便用于后续来自相同客户端请求的处理。

对于上面我们介绍的RESTful的这些个特性它们都是要求我们为了满足这些特征做点什麼,唯有这个无状态却是要求我们不要做什么因为HTTP本身就是无状态的。举个例子一个网页通过调用Web API分页获取符合查询条件的记录。一般情况下页面导航均具有“上一页”和“下一页”链接用于呈现当前页的前一页和后一页的记录。那么现在有两种实现方式返回上下页嘚记录

Web API不仅仅会定义根据具体页码的数据查询定义相关的操作,还会针对“上一页”和“下一页”这样的请求定义单独的操作它自身會根据客户端的Session ID对每次数据返回的页面在本地进行保存,以便能够知道上一页和下一页具体是哪一页

我要回帖

更多关于 作者业帮 的文章

 

随机推荐