实际业务中 何时需要模型优化或图像重建的代数模型

模型优化不得不思考的几个问题 - 知乎专栏
{"debug":false,"apiRoot":"","paySDK":"/api/js","wechatConfigAPI":"/api/wechat/jssdkconfig","name":"production","instance":"column","tokens":{"X-XSRF-TOKEN":null,"X-UDID":null,"Authorization":"oauth c3cef7c66aa9e6a1e3160e20"}}
{"database":{"Post":{"":{"contributes":[{"sourceColumn":{"lastUpdated":,"description":"","permission":"COLUMN_PUBLIC","memberId":,"contributePermission":"COLUMN_PUBLIC","translatedCommentPermission":"all","canManage":true,"intro":"","urlToken":"ctriptech","id":23838,"imagePath":"v2-e349f8d2c6b.jpg","slug":"ctriptech","applyReason":"0","name":"携程技术中心","title":"携程技术中心","url":"/ctriptech","commentPermission":"COLUMN_ALL_CAN_COMMENT","canPost":true,"created":,"state":"COLUMN_NORMAL","followers":1989,"avatar":{"id":"v2-e349f8d2c6b","template":"/{id}_{size}.jpg"},"activateAuthorRequested":false,"following":false,"imageUrl":"/v2-e349f8d2c6b_l.jpg","articlesCount":53},"state":"accepted","targetPost":{"titleImage":"","lastUpdated":,"imagePath":"","permission":"ARTICLE_PUBLIC","topics":[],"summary":"我们平时都在积累自己的“弹药库”:分类、回归、无监督模型,kaggle上面特征变换的黑魔法,样本失衡的处理方法,缺失值填充… 大概可以归类成模型和特征两个点。我们在每个点都已经做得很好,所以我们都拥有一张绿卡,跨过了在数据相关行业发挥模型技术价…","copyPermission":"ARTICLE_COPYABLE","translatedCommentPermission":"all","likes":0,"origAuthorId":0,"publishedTime":"T17:24:02+08:00","sourceUrl":"","urlToken":,"id":3038627,"withContent":false,"slug":,"bigTitleImage":false,"title":"模型优化不得不思考的几个问题","url":"/p/","commentPermission":"ARTICLE_ALL_CAN_COMMENT","snapshotUrl":"","created":,"comments":0,"columnId":23838,"content":"","parentId":0,"state":"ARTICLE_PUBLISHED","imageUrl":"","author":{"bio":"聊技术,话人生 | 微信公号ctriptech","isFollowing":false,"hash":"637f3df5d868d89cbbaa62","uid":609300,"isOrg":true,"slug":"xi-cheng-ji-shu-zhong-xin","isFollowed":false,"description":"我们在这里向你分享来自携程技术人的一手干货,畅谈精彩技术人生。携程技术中心由超过3000位来自海内外的精英工程师组成,以技术引领业务增长,使携程技术中心成为世界顶尖的OTA技术团队。","name":"携程技术中心","profileUrl":"/org/xi-cheng-ji-shu-zhong-xin","avatar":{"id":"117bcececa0632","template":"/{id}_{size}.jpg"},"isOrgWhiteList":true},"memberId":,"excerptTitle":"","voteType":"ARTICLE_VOTE_CLEAR"},"id":661452}],"title":"模型优化不得不思考的几个问题","author":"xi-cheng-ji-shu-zhong-xin","content":"我们平时都在积累自己的“弹药库”:分类、回归、无监督模型,kaggle上面特征变换的黑魔法,样本失衡的处理方法,缺失值填充… 大概可以归类成模型和特征两个点。我们在每个点都已经做得很好,所以我们都拥有一张绿卡,跨过了在数据相关行业发挥模型技术价值的准入门槛。在这个时候,比较关键的下一步,就是高效的技术变现能力,所谓高效,就是解决业务核心问题的专业能力。这篇文章就在描述这些专业能力,也就是模型优化的四个要素:模型、数据、特征、业务,还有更重要的,他们在模型项目中的优先级。本文首先综合介绍模型项目的优先级,模型项目推进的四个要素,并按照优先级顺序依次展开四个要素细节实施过程中需要注意的方方面面。图1模型项目推进的4要素模型优化,离不开这四个要素:模型、数据、特征、业务。项目推进过程中,四个要素相互之间的优先级大致是:业务 & 特征 & 数据& 模型。图2:四要素解决问题细分 + 优先级图2:四要素解决问题细分 + 优先级业务一个模型项目有好的技术选型,完备的特征体系,高质量的数据一定是很加分的,不过有个大前提决定项目的好与坏,就是这个项目的技术目标是否在解决当下核心业务问题。业务问题包含两个方面,业务kpi和deadline。比如,如果业务问题是:在两周之内降低目前手机丢失带来的支付宝销赃风险,这时如果你的方案是研发手机丢失的核心特征,比如改密是否合理,基本上就死的很惨,因为两周根本完不成,改密合理性也未必是模型优化好的切入点;反之,如果你的方案是和运营同学看bad case,梳理现阶段的作案通用手段,并通过分析上线一个简单模型或者业务规则的补丁,就明智很多。如果上线后,案件量真掉了下来,就算你的方案准确率很糟,方法很low,但你解决了业务问题,这才是最重要的。虽然业务目标很关键,不过一般讲,业务运营同学真的不太懂得如何和技术有效的沟通业务目标,比如:我们想做一个线下门店风险评级的项目,希望你们通过反作弊模型角度帮我们把门店打个分(问题:风险是怎么定义的?为什么要做风险评级,更大的业务目标是什么,怎么排期的?这个风险和我们反作弊模型之间的业务关系,你是怎么看的?)是否可以做一个区域未来10min的配送时间预估模型?我们想通过你们的模型衡量在恶劣天气的时候每个区域的运力是否被击穿 (业务现状和排期?运力被击穿可以扫下盲么?运力击穿和配送时间之间是个什么业务逻辑,时间预估是刻画运力紧张度的最有效手段么?项目的关键场景是恶劣天气的话,我们仅仅训练恶劣天气场景的时间预估模型是否就好了?)为了保证整个技术项目没有做偏,项目一开始,一定和业务聊清楚三件事情:1、业务核心问题、关键场景是什么2、如何评估该项目的成功?指标是什么3、通过项目输出什么关键信息给到业务,业务如何运营这个信息从而达到业务目标?项目过程中,也要时刻回到业务,检查项目的健康度:图3图3数据、特征要说正确的业务理解和切入,在为技术项目保驾护航,数据、特征便是一个模型项目性能方面的天花板。garbage in, garbage out就在说这个问题。这两天有位听众微信问我一个很难回答的问题,大概意思是,数据是特征拼起来构成的集合嘛,所以这不是两个要素。从逻辑上面讲,数据的确是一列一列的特征,不过数据与特征在概念层面是不同的:数据是已经采集的信息,特征是以兼容模型、最优化为目标对数据进行加工。就比如通过word2vec将非结构化数据结构化,就是将数据转化为特征的过程。所以,我更认为特征工程是基于数据的一个非常精细、刻意的加工过程。从传统的特征转换、交互,到embedding、word2vec、高维分类变量数值化,最终目的都是更好的去利用现有的数据。之前有聊到的将推荐算法引入有监督学习模型优化中的做法,就是在把两个本不可用的高维ID类变量变成可用的数值变量。不过我普遍观察到自己和童鞋在特征工程中遇到的问题,比如,特征设计不全面,没有耐心把现有特征做得细致… 也整理出来一套方法论,仅做参考:图4 变量体系、研发流程图4 变量体系、研发流程在特征设计的时候,有两个点可以帮助我们把特征想的更全面:1、现有的基础数据2、业务“二维图”这两个方面的整合,就是一个变量的体系。变量(特征),从技术层面是加工数据,而从业务层面,实际在反应RD的业务理解和数据刻画业务能力。“二维图”,实际上未必是二维的,更重要的是我们需要把业务整个流程抽象成几个核心的维度,举几个例子:外卖配送时间业务 (维度甲:配送的环节,骑手到点、商家出餐、骑手配送、交付用户;维度乙:颗粒度,订单粒度、商家粒度、区域城市粒度;维度丙:配送类型,众包、自营…)反作弊变量体系(维度甲:作弊环节,登录、注册、实名、转账、交易、参与营销活动、改密… 乙:作弊介质,账户、设备、IP、wifi、银行卡…)通过这些维度,你就可以展开一个“二维图”,把现有你可以想到的特征填上去,你一定会发现很多空白,比如下图,那么在哪里还是特征设计的盲点就一目了然:图5 账户维度在转账、红包方面的特征很少;没有考虑wifi这个媒介;客满与事件数据没考虑。数据、和特征决定了模型性能的天花板。deep learning当下在图像、语音、机器翻译、自动驾驶等领域非常火,但是deeplearning在生物信息、基因学这个领域就不是热词:这背后是因为在前者,我们已经知道数据从哪里来,怎么采集,这些数据带来的信息基本满足了模型做非常准确的识别;而后者,即便有了上亿个人体碱基构成的基因编码,技术选型还是不能长驱直入–超高的数据采集成本,人后天的行为数据的获取壁垒等一系列的问题,注定当下这个阶段在生物信息领域,人工智能能发出的声音很微弱,更大的舞台留给了生物学、临床医学、统计学。模型图6 满房开房的技术选型、特征工程roadmap图6 满房开房的技术选型、特征工程roadmap模型这件事儿,许多时候追求的不仅仅是准确率,通常还有业务这一层更大的约束。如果你在做一些需要强业务可解释的模型,比如定价和反作弊,那实在没必要上一个黑箱模型来为难业务。这时候,统计学习模型就很有用,这种情况下,比拼性能的话,我觉得下面这个不等式通常成立:glmnet & LASSO &= Ridge & LR/Logistic. 相比最基本的LR/Logistic,ridge通过正则化约束缓解了LR在过拟合方面的问题,lasso更是通过L1约束做类似变量选择的工作。不过两个算法的痛点是很难决定最优的约束强度,glmnet是Stanford给出的一套非常高效的解决方案。所以目前,我认为线性结构的模型,glmnet 的痛点是最少的,在R、Python、Spark上面都开源了。如果我们开发复杂模型,通常成立第二个不等式 RF &= GBDT &= xgboost. 拿数据说话,29个kaggle公开的winner solution里面,17个使用了类似gbdt这样的boosting框架,其次是DNN,RF的做法在kaggle里面非常少见。RF和GBDT的雏形,CART是两位作者在84年合作推出的。但是在90年代在发展模型集成思想the ensemble的时候,两位作者代表着两个至今也很主流的派系:stacking/ bagging & boosting.一种是把相互独立的cart (randomized variables, bootstrapsamples)水平铺开,一种是深耕的boosting,在拟合完整体后更有在局部长尾精细刻画的能力。同时,gbdt模型相比rf更加简单,内存占用小,这都是业界喜欢的性质。xgboost在模型的轻量化和快速训练上又做了进一步的工作,也是目前我们比较喜欢尝试的模型。图7 The Child of RF&GBDT图7 The Child of RF&GBDT【作者简介】胡淏,美团算法工程师,毕业于哥伦比亚大学。先后在携程、支付宝、美团从事算法开发工作。了解风控、基因、旅游、即时物流相关问题的行业领先算法方案与流程。本文来自胡淏在“携程技术沙龙——云海机器学习Meetup”上的分享。没看够?更多来自携程技术人的一手干货,欢迎搜索关注“携程技术中心”微信公号哦~","updated":"T09:24:02.000Z","canComment":false,"commentPermission":"anyone","commentCount":1,"collapsedCount":0,"likeCount":33,"state":"published","isLiked":false,"slug":"","isTitleImageFullScreen":false,"rating":"none","titleImage":"","links":{"comments":"/api/posts//comments"},"reviewers":[],"topics":[{"url":"/topic/","id":"","name":"机器学习"},{"url":"/topic/","id":"","name":"算法"}],"adminClosedComment":false,"titleImageSize":{"width":0,"height":0},"href":"/api/posts/","excerptTitle":"","column":{"slug":"ctriptech","name":"携程技术中心"},"tipjarState":"inactivated","annotationAction":[],"sourceUrl":"","pageCommentsCount":1,"hasPublishingDraft":false,"snapshotUrl":"","publishedTime":"T17:24:02+08:00","url":"/p/","lastestLikers":[{"bio":"","isFollowing":false,"hash":"984599faef993a6dcbf706a9dd1af2af","uid":350000,"isOrg":false,"slug":"shen-qi-lao-tai-po-53","isFollowed":false,"description":"","name":"神奇老太婆","profileUrl":"/people/shen-qi-lao-tai-po-53","avatar":{"id":"v2-9a0dbcf222fa67f84fe52","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"数据挖掘","isFollowing":false,"hash":"bcfcb85d63af821d2ed0e7c","uid":878900,"isOrg":false,"slug":"hewei-74","isFollowed":false,"description":"","name":"稻草人","profileUrl":"/people/hewei-74","avatar":{"id":"da8e974dc","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"机器学习爱好者,在自然语言处理和图像识别方面有兴趣","isFollowing":false,"hash":"40e3d2bea2","uid":52,"isOrg":false,"slug":"ghtimaq","isFollowed":false,"description":"","name":"豆角茄子麻酱凉面","profileUrl":"/people/ghtimaq","avatar":{"id":"0b736aae787","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"不知道该怎么描述","isFollowing":false,"hash":"5bbb838d7c23de6ea9f2c28b","uid":967700,"isOrg":false,"slug":"pei-zhong-kai","isFollowed":false,"description":"","name":"蛋疼的小情调儿","profileUrl":"/people/pei-zhong-kai","avatar":{"id":"da8e974dc","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false},{"bio":"谦谦君子,卑以自牧","isFollowing":false,"hash":"59ee0aaee3e2e431eb8c7","uid":820900,"isOrg":false,"slug":"dataju","isFollowed":false,"description":"聚集数据的力量","name":"赵熙","profileUrl":"/people/dataju","avatar":{"id":"v2-4b4c1ca405","template":"/{id}_{size}.jpg"},"isOrgWhiteList":false}],"summary":"我们平时都在积累自己的“弹药库”:分类、回归、无监督模型,kaggle上面特征变换的黑魔法,样本失衡的处理方法,缺失值填充… 大概可以归类成模型和特征两个点。我们在每个点都已经做得很好,所以我们都拥有一张绿卡,跨过了在数据相关行业发挥模型技术价…","reviewingCommentsCount":0,"meta":{"previous":{"isTitleImageFullScreen":false,"rating":"none","titleImage":"","links":{"comments":"/api/posts//comments"},"topics":[{"url":"/topic/","id":"","name":"框架"},{"url":"/topic/","id":"","name":"Redis"}],"adminClosedComment":false,"href":"/api/posts/","excerptTitle":"","author":{"bio":"聊技术,话人生 | 微信公号ctriptech","isFollowing":false,"hash":"637f3df5d868d89cbbaa62","uid":609300,"isOrg":true,"slug":"xi-cheng-ji-shu-zhong-xin","isFollowed":false,"description":"我们在这里向你分享来自携程技术人的一手干货,畅谈精彩技术人生。携程技术中心由超过3000位来自海内外的精英工程师组成,以技术引领业务增长,使携程技术中心成为世界顶尖的OTA技术团队。","name":"携程技术中心","profileUrl":"/org/xi-cheng-ji-shu-zhong-xin","avatar":{"id":"117bcececa0632","template":"/{id}_{size}.jpg"},"isOrgWhiteList":true},"column":{"slug":"ctriptech","name":"携程技术中心"},"content":"Redis在携程内部得到了广泛的使用,根据客户端数据统计,整个携程全部Redis的读写请求在每秒200W,其中写请求约每秒10W,很多业务甚至会将Redis当成内存数据库使用。这样,就对Redis多数据中心提出了很大的需求,一是为了提升可用性,解决数据中心DR(DisasterRecovery)问题;二是提升访问性能,每个数据中心可以读取当前数据中心的数据,无需跨机房读数据。在这样的需求下,XPipe应运而生 。从实现的角度来说,XPipe主要需要解决三个方面的问题,一是数据复制,同时在复制的过程中保证数据的一致性;二是高可用,xpipe本身的高可用和Redis系统的高可用;三是如何在机房异常时,进行DR切换。下文将会从这三个方面对问题进行详细阐述。最后,将会对测试结果和系统在生产环境运行情况进行说明。为了方便描述,后面的行文中用DC代表数据中心(Data Center)。一、数据复制问题多数据中心首先要解决的是数据复制问题,即数据如何从一个DC传输到另外一个DC,通常有如下方案:客户端双写从客户端的角度来解决问题,单个客户端双写两个DC的服务器。初看没有什么问题。但是深入看下去,如果写入一个IDC成功,另外一个IDC失败,那数据可能会不一致,为了保证一致,可能需要先写入一个队列,然后再将队列的数据发送到两个IDC。 如果队列是本地队列,那当前服务器挂掉,数据可能会丢失;如果队列是远程队列,又给整体的方案带来了很大的复杂度。目前的应用一般都是集群部署,会有多个客户端同时操作。在多个客户端的前提下,又带来了新的问题。比如两个客户端ClientA和ClientB:ClientA:
set key value1ClientB:
set key value2由于两个客户端独立操作,到达服务器的顺序不可控,所以可能会导致两个DC的服务器对于同一个key,value不一致,如下:Server1: setkey value1; set key value2;Server2: setkey value2; set key value1;在Server1,最终值为value2,在Server2,最终值为value1。服务器代理proxy模式解决了多客户端写可能会导致数据不一致的问题。proxy类似于一个client,和单个client双写的问题类似,需要一个数据队列保数据一致性。为了提升系统的利用率,单个proxy往往需要代理多个Redis server,如果proxy出问题,会导致大面积的系统故障。这样,就对系统的性能和可用性提出了极大的挑战,带来实现的复杂度。此外,在特殊的情况下,仍然会可能带来数据的不一致,比如value和时间相关,或者是随机数,两个Redis服务器所在系统的不一致带来了数据的不一致。考虑到以上情况,为了解决复制问题,我们决定采用伪slave的方案,即实现Redis协议,伪装成为Redis slave,让Redis master推送数据至伪slave。这个伪slave,我们把它称为keeper,如下图所示:有了keeper之后,多数据中心之间的数据传输,可以通过keeper进行。keeper将Redis日志数据缓存到磁盘,这样,可以缓存大量的日志数据(Redis将数据缓存到内存ring buffer,容量有限),当数据中心之间的网络出现较长时间异常时仍然可以续传日志数据。Redis协议不可更改,而keeper之间的数据传输协议却可以自定义。这样就可以进行压缩,以提升系统性能,节约传输成本;多个机房之间的数据传输往往需要通过公网进行,这样数据的安全性变得极为重要,keeper之间的数据传输也可以加密,提升安全性。二、高可用任何系统都可能会挂掉,如果keeper挂掉,多数据中心之间的数据传输可能会中断,为了解决这个问题,需要保证keeper的高可用。我们的方案中,keeper有主备两个节点,备节点实时从主节点复制数据,当主节点挂掉后,备节点会被提升为主节点,代替主节点进行服务。提升的操作需要通过第三方节点进行,我们把它称之为MetaServer,主要负责keeper状态的转化以及机房内部元信息的存储。同时MetaServer也要做到高可用:每个MetaServer负责特定的Redis集群,当有MetaServer节点挂掉时,其负责的Redis集群将由其它节点接替;如果整个集群中有新的节点接入,则会自动进行一次负载均衡,将部分集群移交到此新节点。Redis也可能会挂,Redis本身提供哨兵(Sentinel)机制保证集群的高可用。但是在Redis4.0版本之前,提升新的master后,其它节点连到此节点后都会进行全量同步,全量同步时,slave会处于不可用状态;master将会导出rdb,降低master的可用性;同时由于集群中有大量数据(RDB)传输,将会导致整体系统的不稳定。截止当前文章书写之时,4.0仍然没有发布release版本,而且携程内部使用的Redis版本为2.8.19,如果升到4.0,版本跨度太大,基于此,我们在Redis3.0.7的版本基础上进行优化,实现了psync2.0协议,实现了增量同步。下面是Redis作者对协议的介绍:。三、DR切换DR切换分为两种可能,一种是机房真的挂了或者出异常,需要进行切换,另外一种是机房仍然健康,但是由于演练、业务要求等原因仍然需要切换到另外的机房。XPipe处理机房切换的流程如下:检查是否可以进行DR切换类似于2PC协议,首先进行prepare,保证流程能顺利进行。原主机房master禁止写入此步骤,保证在迁移的过程中,只有一个master,解决在迁移过程中可能存在的数据丢失情况。提升新主机房master其它机房向新主机房同步当然了,即使做了检查,也无法绝对保证整个迁移过程肯定能够成功,为此,我们提供回滚和重试功能。回滚功能可以回滚到初始的状态,重试功能可以在DBA人工介入的前提下,修复异常条件,继续进行切换。根据以上分析,XPipe系统的整体架构如下所示:Console用来管理多机房的元信息数据,同时提供用户界面,供用户进行配置和DR切换等操作。Keeper负责缓存Redis操作日志,并对跨机房传输进行压缩、加密等处理。Meta Server管理单机房内的所有keeper状态,并对异常状态进行纠正。四、测试数据我们关注的重点在于增加keeper后,平均延时的增加。测试方式如下图所示。从client发送数据至master,并且slave通过keyspacenotification的方式通知到client,整个测试延时时间为t1+t2+t3。首先我们测试Redis master直接复制到slave的延时,为0.2ms。然后在master和slave之间增加一层keeper,整体延时增加0.1ms,到0.3ms。相较于多个DC之间几毫秒,几十毫秒的延时,增加一层keeper带来的延时是完全没问题的。在携程生产环境进行了测试,生产环境两个机房之间的ping TTL约为0.61ms,经过跨数据中心的两层keeper后,测试得到的平均延时约为0.8ms,延时99.9线为2ms。综上所述:XPipe主要解决Redis多数据中心数据同步以及DR切换问题,同时,由于XPipe增强后的Redis版本优化了psync协议,会极大的提升Redis集群的稳定性。同时,整个系统已经开源,欢迎大家一起参与优化整个系统:XPipe: XRedis(在Redis3.0.7版本上进行增强的版本):【作者简介】孟文超,携程技术中心框架研发部高级经理。2016年加入携程,目前主要负责Redis多数据中心项目XPipe。此前曾在大众点评工作,任基础架构部门通信团队负责人。没看够?更多来自携程技术人的一手干货,欢迎搜索关注“携程技术中心”微信公号哦~","state":"published","sourceUrl":"","pageCommentsCount":0,"canComment":false,"snapshotUrl":"","slug":,"publishedTime":"T15:37:06+08:00","url":"/p/","title":"携程开源Redis多数据中心解决方案-XPipe","summary":"Redis在携程内部得到了广泛的使用,根据客户端数据统计,整个携程全部Redis的读写请求在每秒200W,其中写请求约每秒10W,很多业务甚至会将Redis当成内存数据库使用。这样,就对Redis多数据中心提出了很大的需求,一是为了提升可用性,解决数据中心DR(Disast…","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"commentPermission":"anyone","commentsCount":5,"likesCount":28},"next":{"isTitleImageFullScreen":false,"rating":"none","titleImage":"","links":{"comments":"/api/posts//comments"},"topics":[{"url":"/topic/","id":"","name":"框架"}],"adminClosedComment":false,"href":"/api/posts/","excerptTitle":"","author":{"bio":"聊技术,话人生 | 微信公号ctriptech","isFollowing":false,"hash":"637f3df5d868d89cbbaa62","uid":609300,"isOrg":true,"slug":"xi-cheng-ji-shu-zhong-xin","isFollowed":false,"description":"我们在这里向你分享来自携程技术人的一手干货,畅谈精彩技术人生。携程技术中心由超过3000位来自海内外的精英工程师组成,以技术引领业务增长,使携程技术中心成为世界顶尖的OTA技术团队。","name":"携程技术中心","profileUrl":"/org/xi-cheng-ji-shu-zhong-xin","avatar":{"id":"117bcececa0632","template":"/{id}_{size}.jpg"},"isOrgWhiteList":true},"column":{"slug":"ctriptech","name":"携程技术中心"},"content":"酒店业务部门是携程旅行的几大业务之一,其业务逻辑复杂,业务需求变动快,经过多年的研发,已经是一个代码规模庞大的工程,如何规范代码,将代码按照其功能进行分类,将代码写到合适的地方对项目的迭代起着重要的作用。MVP模式是目前客户端比较流行的框架模式,携程在很早之前就开始探索使用该模式进行相关的业务功能开发,以提升代码的规范性和可维护性,积累了一定的经验。本文将探讨一下该模式在实际工程中的优点和缺陷,并介绍携程面对这些问题时的思考,解决方案以及在实践经验基础上对该模式的扩展模式MVCPI。一、从MVC说起MVC已经是非常成熟的框架模式,甚至不少人认为它过时陈旧老气,在实践中,很多同事会抱怨,MVC会使得代码非常臃肿,尤其是Controller很容易变成大杂烩,预期的可维护性变得很脆弱,由此导致一方面希望有新框架模式可以解决现在的问题,但同时对框架模式又有些怀疑,新的框架模式是否能真正解决现在的问题?会不会重蹈覆辙?会不会过度设计?会不会掉进一个更深的坑?总之,这些类似“一朝被蛇咬,十年怕井绳”的担忧显得不无道理。但不管如何,我们需要仔细耐心的做工作。1.1、被误解的MVC在MVP模式逐渐流行之前,不管我们有意识或无意识地,我们使用的就是MVC模式。以Android为例,我们来看看MVC是什么样子。public class HotelActivity extends Activity {
private TextView mNameV \n
private TextView mAddressV
private TextView mStarV \n
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
mNameView = (TextView) findViewById(R.id.hotel_view);
mAddressView = (TextView) findViewById(R.id.address_view);
mStarView = (TextView) findViewById(R.id.star_view);\n\n
HotelModel hotel = HotelLoader.loadHotelById(1000);\n
mHotelNameView.setText(hotel.hotelName);
mHotelAddressView.setText(hotel.hotelAdress);
mHotelStarView.setText(hotel.hotelStar);\n
}\n}\n上面的代码,概括了Android MVC的基本结构,从笔者的经验来看,很多应用都存在这样的代码风格,也就是大部分人认为的MVC:Model :
Hotel,HotelLoader\nController:
HotelActivity\nView :
mHotelNameView\n
mHotelAddressView\n
mHotelStarView\n可以试想一下如果这个界面展示的数据非常的多话,MainActivity必然会变得非常庞大,就像大部分人所抱怨的那样。诚然,上面的demo是MVC模式,但是,它仅是从系统框架的角度来看,如果从应用框架来看,它不是。下面来看一下,从应用框架来看一下MVC正确的结构:1.2、MVC的正确姿势应用中的MVC应该在系统的MVC框架上根据业务的自身的需要进行进一步封装,也就是说,如果在我们宣称我们是使用MVC框架模式的时候,代表我们的主要工作是封装自己的MVC组件。它看起来应该是像下面的风格:public class HotelActivity extends Activity {\n\n
private HotelView mHotelV\n\n
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);\n
mHotelView = (HotelView) findViewById(R.id.hotel_view);\n
HotelModel hotel = HotelLoader.loadHotelById(1000);\n
mHotelView.setHotel(hotel);
}\n}\n跟之前的代码相比,基本结构是相似的,如下:Model :
Hotel,HotelLoader\nController:
HotelActivity\nView :
mHotelView\n仅仅View层发生了变化,这是因为,Model和Controller相对是大家容易理解的概念,在面临任何一个业务需求的时候,自然就能产生的近乎本能的封装(尽管Model的基本封装大部分工程师都可完成,但不可否认Model的设计是至关重要而有难度的);而对View的看法,可能就是“能正确布局和展示就行”。但这正是关键所在:我们需要对界面进行全方位的封装,包括View。具体来说,一个真正的MVC框架应该具备下面的特点:数据都由Model进行封装View绑定业务实体,view.setXXXController不管理与业务无关的View1.3 MVC模式的问题所在前面说到,很多人抱怨采用MVC模式使得Controller变得很臃肿,我相信,Controller变得臃肿是事实,但其归结于采用MVC模式是不正确的,这个锅不应该由MVC来背,因为,这个论点会导致我们走向错误的方向从而无法发现MVC真正的问题所在。为什么这么说呢,那是因为在本人了解到的很多情况下,大家并没有正确理解MVC框架模式,如采用前文中第一种模式,自然会使得Controller臃肿,但是如果采用第二种模式,Controller的代码和逻辑也会非常清晰,至少不至于如此多的抱怨。因此如果只是想解决Controller臃肿的话,MVC就够了,毋庸质疑。那MVC的问题是什么呢?我想只有深刻的理解了这个问题,我们才有必要考虑是否需要引入新的框架模式,以及避免新的模式中可能出现的问题。View强依赖于Model是MVC的主要问题。由此导致很多控件都是根据业务定制,从Android的角度来看,原本可以由一个通用的layout就能实现的控件,由于要绑定实体模型,现在必须要自定义控件,这导致出现大量不必要的重复代码。因此有必要将View和Model进行解耦,而MVP的主要思想就是解耦View和Model。由此引入MVP就显得很自然。二、 Android MVP2.1、参考实现Android 官方提供的MVP参考实现,大致思想如下:1、抽象出IView接口,规范控件访问方法,而不限View具体来源public interface IHotelView {\n
public TextView getNameView();\n
public TextView getAddressView();\n
public TextView getStarView();\n}\n2、抽象出IPresenter接口,定义IView 和 Model的绑定接口public interface IHotelPresenter {\n
public void setView(IHotelView hotelView);\n
public void setData(HotelMotel hotel); \n}\n3、IPresenter的实现类,实施数据和IView的绑定,并负责相关的业务处理public class HotelPresenter implements IHotelPresenter {\n
private IHotelView hotelV\n
public void setView(IHotelView hotelView) {\n
this.hotelView
= hotelV \n
public void setData(HotelModel hotel) {\n
hotelView.getNameView().setText(hotel.hotelName);\n
hotelView.getAddressView().setText(hotel.hotelAddress);\n
hotelView.getStarView().setText(hotel.hotelStart);\n
}\n}\n4、Activity实现IView,角色转变为View,弱化Controller的功能public class HotelActivity extends Activity
implements IHotelView {\n
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
HotelModel hotel = HotelLoader.loadHotelById(1000);
IPresenter presenter = new Presenter();
presenter.setView(this); \n
presenter.setData(hotel);\n\n
public TextView getNameView() {\n
return (TextView)findViewById(R.id.hotel_name_view); \n
public TextView getAddressView() {\n
return (TextView)findViewById(R.id.hotel_address_view);
public TextView getStarView() {\n
return (TextView)findViewById(R.id.hotel_address_view); \n
}\n}\n上述代码,主要的特点可以概括为:面向接口View - Model 解耦Activity角色转换就目前了解到的情况来看,很多采用MVP模式的应用基本上和android参考实现方案差别不大,说明该模式的应用场景也是很广泛的。2.2 Android MVP存在的问题尽管已经有了大量的应用,但不可否认该模式的还是存在一些问题,这些问题在携程的使用过程中也得到了体现。比如,上下文丢失问题,生命周期问题,内存泄露问题以及大量的自定义接口,回调链变长等问题。可以归纳为:业务复杂时,可能使得Activity变成更加复杂,比如要实现N个IView,然后写更多个模版方法。业务复杂时,各个角色之间通信会变得很冗长和复杂,回调链过长。Presenter处理业务,让业务变得很分散,不能全局掌握业务,很难去回答某个业务究竟是在哪里处理的。用Presenter替代Controller是一个危险的做法,可能出现内存泄漏,生命周期不同步,上下文丢失等问题。以下面的这个需求来看几个具体的示例:详情按钮的展示需要服务端下发标记位控制,展示时点击需要请求一个服务,服务返回时toast提示用户public class HotelPresenter { \n
private IHotelView mHotelV\n
private Handler handler = new
Handler(getMainLooper());
public void setData(HotelModel hotelModel) {
View button = mHotelView.getButtonView();
int visibility = hotelModel.showButton ? .VISIBLE :GONE;
button.setVisibility(visibility);
if (hotelModel.showButton) {
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
sendRequest();
private void sendRequest() {
new Thread() {
public void run() {\n
Thread.sleep(15*1000);\n
handler.post(new Runnable() {
public void run() {
Toast.makeText(???) //Where is Context?\n
}.start();
}\n}\n上述代码表明,HotelPresenter可以处理大部分的业务,但是在最后需要使用上下文的时候,出现了困难,因为脱离了上下文,展示一个Toast都不能实现为了避免这样的尴尬,因此改进方案如下:public class HotelPresenter {
private IHotelView mHotelV\n
private Fragment mF\n
private HotelPresenter(Fragment fragment) { \n
this.mFragment = \n
private Handler handler = new Handler(Looper.getMainLooper());
public void setData(HotelModel hotelModel) {
View button = mHotelView.getButtonView();
button.setVisibility(hotelModel.showButton ? VISIBLE :GONE); \n
if (hotelModel.showButton) {
button.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) { \n
sendRequest();\n
private void sendRequest() {\n
new Thread() {\n
public void run() {\n
Thread.sleep(15*1000);\n
handler.post(new Runnable() {\n
public void run() {\n
Context context = mFragment.getActivity();\n
int duration = LENGTH_SHORT;\n
//NullPointerException will occur\n
Toast.makeText(context,\"成功”,duration).show();
}.start();\n
}\n}\n改进的方案中,考虑到需要使用上下文,因此新增了接口传入Fragment作为上下文,在Presenter需要时可以使用,但是,由于Fragment生命周期会了变化,可能会导致空指针问题。于是新的问题又需要解决。主要是两个思路,一个是为Presenter增加生命周期方法,在Fragment的生命周期方法里调用Presenter对应的生命周期函数,但这就让Presenter看起来像Fragment的孙子;另外一个就是承认Presenter其实不太合适承担Controller的职责,从而提供接口给外部处理;如下:public class HotelPresenter {\n
private IHotelView mHotelV\n
private Handler handler = new Handler(Looper.getMainLooper());\n
public void setData(HotelModel hotelModel) {\n
View button = mHotelView.getButtonView();
button.setVisibility(hotelModel.showButton ? VISIBLE :GONE);\n
if (hotelModel.showButton) {
button.setOnClickListener(new View.OnClickListener() {
@Override \n
public void onClick(View v) {\n
if (mCallback != null) {\n
mCallback.onSendButtonClicked();\n
public interface Callback {\n
public void onSendButtonClicked();\n
private Callback mC\n
void setCallback(Callback
callback) {\n
mCallack =\n
}\n}\n这个方案很稳定,似乎成为了最佳的选择。但是自定接口和回调始终有那么一点痛三、MVP的扩展模式MVCPI由于前面的分析,MVP参考实现并不是万能的,携程酒店并没有完全采用参考实现方案,而是结合自身的实践经验思考之后设计出来的扩展方案。我们主要考虑了一下的几个问题:如何定义View接口?如何定位Presenter ?如何对待Controller?如何解决长长的回调链?通过对上述问题的思考,提出对应的解决方法,规避前面论述的各种问题,形成了携程酒店的MVCPI框架模式,并在多个业务场景运行,取得了较为满意的效果。下面,详细介绍MVCPI模式。3.1、 IView和Android 参考实现不一样的是,我们并没有采用强类型的接口作为表达View的方式,而是采用弱类型的接口来定义View。具体定义方式如下:public interface IView {\n
//用于展示酒店名称的控件 \n
int NAME_VIEW = R.id.name_ \n
//用于展示酒店地址的控件\n
int ADDRESS_VIEW = R.id.address_
//用于展示酒店星级的控件 \n
int STAR_VIEW = R.id.star_\n
//用于展示酒店详情入口的的控件\n
int DETAIL_BUTTON = R.id.detail_\n}\n上面的接口简洁的描述了作为业务控件的View需要具备的子控间ID,并不需要具体的实现类。因此也不需要Activity去实现这个接口,只需要在layout中申明这几个ID的即可,极大的简化了代码。3.2、 Presenter与参考实现的定位不一样,我们认为由Presenter取代Controller并不是一个好的做法,Presenter应是Controller的补充,主要起到View和Model解耦和数据绑定的作用,所负责的控件的上的业务还是有Controller决定如何去处理。另外setView接受的参数是一般的View,而非一个接口类型,内部根据IView定义的ID去查找子控件。如下:public class CtripHotelPresenter {\n
TextView mNameV \n
TextView mAddressV\n
TextView mStarV \n
Button mDetailB \n
public void setView(View view) {
mNameView = (TextView)mView.findViewById(IView.NAME_VIEW);
mAddressView = (TextView)mView.findViewById(IView.ADDRESS_VIEW);
mStarView = (TextView) mView.findViewById(IView.STAR_VIEW);
mDetailButton = (Button) mView.findViewById(IView.DETAIL_BUTTON);
public void setData(HotelModel hotel) {
mNameView.setText(hotel.hotelName);
mAddressView.setText(hotel.hotelAdress);
mStarView.setText(hotel.hotelStar); \n
int v = hotel.showButton ? View.VISIBLE : View.GONE;
mDetailButton.setVisibility(v);\n
}\n}\n3.3、 InteractorInteractor是我们定义出来的扩展元素,在MVP和MVC中都没有对应的角色。为了阐述它的含义,我们先来看看两个非常常见的场景。回调链过长在前面介绍过,Presenter自定义接口是很多候选方案中较为合理的选择,但相比MVC而言,MVP更容易出现如上图的一种调用和回调关系(甚至更长)。维护这种回调链通常来说是一件非常头痛的事情,从View的角度来看,很难知道某个事件到最后究竟完成了什么业务,Acitivity也不知道到要装配哪些回调。某个未知的新需求可能需要将该链条上的每个环节都增加回调。下面来是另外一种场景,大家可以脑补一下采用上面的回调方案,回调链会是什么情况。交互集中型界面在该界面有几个特点:几十种动态交互需求,分布于不同的模块分布于不同深度的嵌套层次中经过大量版本迭代后,无论产品经理,研发或者测试,都不清楚到底有哪些需求,业务逻辑是什么,写在什么地方等等......上述两个场景可以得出两个结论:排查问题非常耗时增加功能成本高,容易引致其他问题为了解决上述两个比较棘手的问题,我们引入了Interactor,用于描述整个界面的交互,一举解决上述两个问题。我们认为交互模型是一个功能模块的重要逻辑单元,相对于实体模型来说,交互模型更加抽象,在大多数的情况,并不能引起大家的注意,但它确实是如实体一样的存在,正是因为没有对交互进行系统的描述,才导致上面两种突出的问题。尽管抽象,但是交互模型本质非常简单,它有着和实体模型有相似的结构,示例如下:public class HotelOrderDetailListeners {\n
public View.OnClickListener mBackL // 返回按钮点击事件监听者\n
public View.OnClickListener mShareClickL//分享按钮事件监听者\n
public View.OnClickListener mConsultClickL//咨询按钮事件监听者\n
……\n}\n通过对界面整体分析后,我们建立如上的交互模型,所有的交互都在交互模型进行注册,由交互模型统一管理,进而可以对整个界面的交互进行宏观把控;然后在页面的所有元素中共享同一个交互模型,进而各个元素不再需要自定义接口和避免建立回调链。最后由Controller负责组装,进一步加强Controller的控制能力。3.4、 MVCPI全貌最后,整体介绍一下MVCPI的代码结构1、首先定义整个界面中有哪些用户交互,本例中就一个详情按钮交互public class HotelInteractor {\n
//点击详情的事件处理器\n
public View.OnClickListener mD\n}\n2、Presenter构造时需要传入交互模型,内部定义了IView接口,传入的View中需要包含它定义的ID的控件,在bindData时,详情按钮的点击不是通过匿名内部类去处理,而是直接引用交互模型中定义的mDetailpublic class HotelPresenter {\n
private View hotelV\n
private HotelInteractor mI\n
private Button mDetailB\n\n
public HotelPresenter(HotelInteractor interactor) {\n
this.mInteractor =\n
private interface IView {\n
int DETAIL= R.id.detail_\n
public void setView(View hotelView) {\n
this.hotelView
= hotelV \n
mDetailButton= (Button)findViewById(IView. DETAIL );\n
public void setData(HotelModel hotel) {\n
if (hotel.showButton) {\n
mDetailButton.setVisibility(View.Visibile);\n
mDetailButton.setOnClickListener(mInteractor.mDetail);\n
}\n}\n3、Controller负责界面各个元素(包括交互模型)的初始化和装配public class HotelActivity extends Activity {\n
@Override\n
protected void onCreate(Bundle savedInstanceState) {\n
super.onCreate(savedInstanceState);\n
setContentView(R.layout.activity_main2);\n
HotelInteractor interactor = new HotelInteractor();\n
interactor.mDetail = new View.OnClickListener() {\n
public void onClick(View view) {\n
viewHotelDetail();//处理详情业务; \n
HotelModel model= HotelLoader.loadHotelById(1000);
HotelPresenter presenter = new HotelPresenter (interactor);
View view= findViewById(R.id.hotel_view);
presenter.setView(view);\n
presenter.setData(hotel);\n
}\n}\n四、结论通过对MVC,MVP的介绍和研究,我们发现二者的关系并不是相互取代的关系,而是一种演化和改进的关系。经实践证明,MVC仍然具有强大的生命力,试图用MVP取代MVC几乎都会失败。携程在MVC模式基础上,结合MVP思想,加入Interactor元素搭建的MVCPI框架模式,一方面将数据绑定逻辑从Controller(或者View)中分离出去,另一方面将交互模型的控制纳入进来,进一步加强了Controller的控制能力。无论从代码的简洁性,维护性,扩展性来看,都具有较大优势,具有一定的实践推广价值。当然,任何框架模式都不是全能的,MVCPI也存在它不足,如果有好的意见和建议,欢迎加入,一起讨论推进框架模式的发展。【作者简介】赵伟麟,2014年加入携程酒店事业部,从事Android研发工作。擅长基于组件的业务架构,系统架构,建模,性能优化和重构,关注应用系统的扩展性和耦合性,追求简洁的代码。没看够?更多来自携程技术人的一手干货,欢迎搜索关注“携程技术中心”微信公号哦~","state":"published","sourceUrl":"","pageCommentsCount":0,"canComment":false,"snapshotUrl":"","slug":,"publishedTime":"T15:49:22+08:00","url":"/p/","title":"MVP模式在携程酒店的应用和扩展","summary":"酒店业务部门是携程旅行的几大业务之一,其业务逻辑复杂,业务需求变动快,经过多年的研发,已经是一个代码规模庞大的工程,如何规范代码,将代码按照其功能进行分类,将代码写到合适的地方对项目的迭代起着重要的作用。MVP模式是目前客户端比较流行的框架…","reviewingCommentsCount":0,"meta":{"previous":null,"next":null},"commentPermission":"anyone","commentsCount":1,"likesCount":9}},"annotationDetail":null,"commentsCount":1,"likesCount":33,"FULLINFO":true}},"User":{"xi-cheng-ji-shu-zhong-xin":{"isFollowed":false,"name":"携程技术中心","headline":"我们在这里向你分享来自携程技术人的一手干货,畅谈精彩技术人生。携程技术中心由超过3000位来自海内外的精英工程师组成,以技术引领业务增长,使携程技术中心成为世界顶尖的OTA技术团队。","avatarUrl":"/117bcececa0632_s.jpg","isFollowing":false,"type":"org","slug":"xi-cheng-ji-shu-zhong-xin","bio":"聊技术,话人生 | 微信公号ctriptech","hash":"637f3df5d868d89cbbaa62","uid":609300,"isOrg":true,"description":"我们在这里向你分享来自携程技术人的一手干货,畅谈精彩技术人生。携程技术中心由超过3000位来自海内外的精英工程师组成,以技术引领业务增长,使携程技术中心成为世界顶尖的OTA技术团队。","profileUrl":"/org/xi-cheng-ji-shu-zhong-xin","avatar":{"id":"117bcececa0632","template":"/{id}_{size}.jpg"},"isOrgWhiteList":true,"badge":{"identity":null,"bestAnswerer":null}}},"Comment":{},"favlists":{}},"me":{},"global":{"experimentFeatures":{"ge3":"ge3_9","ge2":"ge2_1","appStoreRateDialog":"close","nwebStickySidebar":"sticky","qrcodeLogin":"qrcode","favAct":"default","default":"None","mobileQaPageProxyHeifetz":"m_qa_page_nweb","newMore":"new","iOSNewestVersion":"4.2.0","newMobileColumnAppheader":"new_header","newBuyBar":"livenewbuy3","sendZaMonitor":"true","homeUi2":"default","answerRelatedReadings":"qa_recommend_by_algo_related_with_article","wechatShareModal":"wechat_share_modal_show","liveReviewBuyBar":"live_review_buy_bar_2","qaStickySidebar":"sticky_sidebar","androidProfilePanel":"panel_b","liveStore":"ls_a2_b2_c1_f2","zcmLighting":"zcm"}},"columns":{"next":{},"ctriptech":{"following":false,"canManage":false,"href":"/api/columns/ctriptech","name":"携程技术中心","creator":{"slug":"xi-cheng-ji-shu-zhong-xin"},"url":"/ctriptech","slug":"ctriptech","avatar":{"id":"v2-e349f8d2c6b","template":"/{id}_{size}.jpg"}}},"columnPosts":{},"columnSettings":{"colomnAuthor":[],"uploadAvatarDetails":"","contributeRequests":[],"contributeRequestsTotalCount":0,"inviteAuthor":""},"postComments":{},"postReviewComments":{"comments":[],"newComments":[],"hasMore":true},"favlistsByUser":{},"favlistRelations":{},"promotions":{},"switches":{"couldAddVideo":false},"draft":{"titleImage":"","titleImageSize":{},"isTitleImageFullScreen":false,"canTitleImageFullScreen":false,"title":"","titleImageUploading":false,"error":"","content":"","draftLoading":false,"globalLoading":false,"pendingVideo":{"resource":null,"error":null}},"drafts":{"draftsList":[],"next":{}},"config":{"userNotBindPhoneTipString":{}},"recommendPosts":{"articleRecommendations":[],"columnRecommendations":[]},"env":{"edition":{},"isAppView":false,"appViewConfig":{"content_padding_top":128,"content_padding_bottom":56,"content_padding_left":16,"content_padding_right":16,"title_font_size":22,"body_font_size":16,"is_dark_theme":false,"can_auto_load_image":true,"app_info":"OS=iOS"},"isApp":false},"sys":{},"message":{"newCount":0},"pushNotification":{"newCount":0}}}

我要回帖

更多关于 业务模型图 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信