软件开发随笔系列二——关于架构和模型

软件开发管理 专栏收录该内容
1 篇文章 0 订阅

软件开发随笔系列二——关于架构和模型


我感觉,关于软件的架构(architecture、framework、skeleton等词,毕竟这词是从国外来的,国外这个词是参考建筑行业的概念)其实很难有一个明确、清晰的定义。每个架构师都有自己的定义和看法。甚至同一个需求,不同架构师画出来的架构图(其实什么才是架构图,每人的定义也不一样)也不一样。

从我自己的角度,非要给“架构”一个定义的话,我认为就是在软件真正动手做之前能说清楚这软件要做成什么样和怎么做。这样一个产出物,就是架构。一般情况下,会把设计过程分为架构设计、概要设计和详细设计。详细设计和其他有明显区别——当然,现在伪代码一级的详细设计基本也很少人做了。而架构设计和概要设计,一般就是笼统的说架构是系统级,概要设计是模块级别。但什么是系统级别?尤其现在复杂的分布式设计中,到什么程度,才能算是充分设计明白这个系统呢?以前我们一般对比建筑行业,盖楼之前先弄出一个微缩版本的房子——这才叫模型——然后各种图纸,对着图纸就可以施工了。

而实际上,我们既没办法在开始编码之前给软件先做一个微缩版本的“模型”,也没办法输出一堆让工程师看着就能直接开发的图纸。其实,就算软件真的做出来之后,项目组成员都未必都理解这个软件怎么做的,何况没做之前——软件本就是一个抽象的东西,为抽象的软件建立更加抽象的软件模型,确实很难。

回想每次作为架构师拿到一份项目需求,一般都做下面几个事情:

  1. 分析、甚至是学习其中的业务要求,归纳、分类所需要的功能
  2. 对最主要、最核心的几个业务功能分析、整理其业务流程,可能包括操作流程、数据迁移等。
  3. 这个业务需要留下什么数据记录,以及需要什么数据支撑业务运行。
  4. 思考用什么样的技术来实现,包括开发语言、运行环境、可用的基础组件/工具等,以及重点、难点技术如何解决。

软件架构概要

也就是,包括:功能流程数据技术的“3+1”组合。这意味着,架构师需要什么都懂,从业务到技术都要懂——要能看得懂业务需求,能转换成开发团队看得懂的功能说明或者功能需求;深入理解,研究业务流程;最后还得掌握很多的技术,知道怎么实现这个需求。基本属于出去能对付客户,在家能撑得住团队的核心人物了。

当然,如果有幸在一个有好的产品经理或者业务分析师的公司,最为繁杂的业务功能和流程分析,应该不需要架构师去做。但架构师也必须看得懂,并且能理解所有的功能和流程的需求,才能有针对性的设计对应的数据和技术方案。不要相信放之四海而皆准的技术框架,这种框架能给架构师一个指导,但并不能解决一个软件该如何做好。

很不幸的是,我所设计过的软件基本都需要我自己从业务开始理解、学习到技术实现的设计。这里插一句,很多时候很多技术人员说业务很难学,怎么办?我跟他们说不是业务难学的问题,你学计算机花了四年时间,貌似也没太学明白,你指望你几天时间把另外一个专业就学明白了?

然而从纯技术转型架构,这个确实是一个必经的关隘。这就涉及到如何建立自己的知识体系,快速学习的问题了。我称为“思维锻炼”。这很重要,但是另外一个话题了。

软件模型

架构和模型往往分不开。“模型”是一个很高大上的名词。什么叫模型,我常用的定义是:客观事物的抽象表述。软件设计进入到面向对象阶段之后,几乎所有的教材都让程序员扔掉代码逻辑(循环、分支、递归、函数等),而把你的代码看着一个一个鲜活的“对象”。比如我常用的茶壶作为例子。

茶壶

  • 能装水,能倒水;
  • 一般的规格是长宽高和容积、颜色等;
  • 由一个容器主体,外加把手,壶嘴和盖子组成。

这样,就形成了抽象的“水壶”,虽然我们还不知道这个水壶是方的还是圆的,但无所谓,慢慢细化规格描述,总会清楚的。然后,我们用同样的方式,在程序里建立了一个叫“水壶”的对象——有一些属性,聚合了一些子对象(容器,盖子,把手,壶嘴),实现了一些方法(倒水,装水)。

很清晰,很简单。可是,我们要做的软件往往不是像水壶一样看得见摸得着能轻易地能形容出来的。一个软件,本身就不是一个“现实”的物体,而是在计算机中,由一些界面(功能)组成,实现了一些业务流程,存储了一些数据。本来就很抽象。前面说过,就算做出来了,一样摸不到。所以在面向对象诞生之后,出现过很多方法、工具来设计软件模型。比如,最出名的UML,最近挺火的领域驱动设计(DDD)。一些大公司也有一些自己的实现框架,比如我老东家IBM的The Information FrameWork (IFW)。当然了,我觉得,如果把这些东西当做是可以拿来直接用的工具,然后就能形成想要的软件的想法,是不可取的。我不认为软件行业存在这种神器,以前没有,以后也不会有。把其当做一个方法论,帮助自己最终形成自己的分析方法才是最重要的。

这些方法论,首要的都是教我们如何把“业务”说清楚,但业务越来越复杂,设计的标的,就从对象,到组件,到领域,不断的扩大。

我自己这么些年的总结,对软件模型的理解也由简到繁,又由繁到简几次变更。形成了自己的一些模式,我称为一种三维表示法。

模型三维表示法

也算是溯本追源,体现了一个软件应该具有的基本点:

  • 展现一些界面,提供一些功能;
  • 实现一些业务流程、交易流程;
  • 使用以及存储了一些数据。

把这三个维度表示清楚,应该也可以把一个软件说明白了。所以,在我这里定义:

软件模型=功能模型+流程模型+数据模型

这三个维度有各自的表达方法,其中数据部分比较传统,也比较规范,E-R图(实体关系图)还是非常实用,到现在依旧是最广泛被接受的模型表示方式。先借用数据库理论中对模型的分级定义:

  • 概念模型(Conceptual Data Model),是一种面向用户、面向客观世界的模型,主要用来描述世界的概念化结构,基本上跟计算机、数据库无关。
  • 逻辑模型(Logical Data Model),是一种面向数据库系统的模型,是具体的DBMS所支持的数据模型。此模型既要面向用户,又要面向系统。
  • 物理模型(Physical Data Model),是一种面向计算机物理表示的模型,描述了数据在储存介质上的组织结构,它不但与具体的DBMS有关,而且还与操作系统和硬件有关。

参照上述分级,对照功能和流程两个模型,也可以有一个大概的,并不严谨的分层定义:

  • 概念层:对需求直观的概念化表述,与实现技术和方式无关。
  • 逻辑层:面向实现但不依赖具体技术(开发语言等),精确体现需求的表述;
  • 物理层:可以转换为系统实现的表述,依赖于具体的平台和技术。

总结起来就是从简单的示意图一直到可以生成代码(部分是可行的)的设计过程。据此,有一个不太严谨的表格,大家可以参考:

三维度分

功能和流程最终的实现就是可部署的代码包了,在物理层面,实际上就合二为一了。而数据部分,最终可以生成单独的数据库可接受的SQL文件。

我们分别讨论模型的三个维度,基本遵循这三个层次进行。

功能模型

接触到一个新的需求,设计一个新的领域的软件,最开始研究和分析的,就应该是功能。如:

  • 研究同类业务参与人员都要做什么。
  • 参考类似软件有什么功能等。

功能设计很重要,基本就是确定软件的目标了。架构师不一定需要做这个工作,这往往是产品经理的职责。但我们站在一个软件的设计者角度,也值得研究。

在这里的定义,功能模型大概可以类比UML中的类图,表示功能的分布的静态关系。

概念层

功能的概念层表述,在UML中可以用用例图,比如针对电商系统,可以有一个简单的例子:

电商用例图

用例图其实没啥不好——除了不好看之外——最大的问题就是“用例”(Use Case)的定义不但拗口还不好理解。记得以前经常争论:这算不算一个用例?不能让大家无异议地直接理解的概念都是挺难用起来的。

所以,后期我自己也放弃用这种我需要先做科普什么是用例,然后再表达软件需要什么功能的方式了。回到最简单的概念——功能点。表述方式也不拘一格,好看、清晰就行——脑图,示意图,表格都可以。

功能概念模型

至少看上去好看多了吧,而且,不具备任何技术概念的人都能看懂(UML直到不被流行了对于大多数人来说都是晦涩难懂的)。直观、清晰,是最重要的。功能模型的概念层有几个要点:

  1. 边界:系统都需要提供什么功能,有需要跟什么外部资源(人、系统)打交道。相当于画了一个圈圈,分清楚那些是圈圈里面要做的,那些是关联方做的。
  2. 参与方:谁用这个系统做什么。现代系统不会只服务一个人,也不会只服务一类(一种角色)人,列清楚都什么人需要用这个系统,分别用这个系统的那些功能。
  3. 功能分组分类:功能分组分类很重要,甚至我认为是最重要也最消耗时间精力做的事情。可以类比功能菜单一样,清晰合理的分组分类,对之后的设计很有指导作用。

边界

参考刚刚的例子,一个简单的电商系统的边界是比较明显和清晰的,用户会直接操作系统,而系统有需要使用第三方支付公司提供的系统实现支付结算退款等资金操作。而另外的很多系统,需要对接的第三方就很多,甚至没有操作界面,都是后台服务。比如银行的中间业务系统:

中间业务系统

这个系统的边界就比较复杂了。它并不提供直接的界面给任何用户,而是通过“大前置”系统转发柜面系统或手机银行系统、网银系统以及各类自助终端系统等的请求,同时需要使用各种第三方系统的功能。这样我们确定功能的时候,就不能单纯的叫“缴费”了,而是需要明确“交水费”,“话费充值”等。一般情况,可以只把和需要开发的系统直接关联的资源(人或系统)画出来就行,但是为了清晰展现业务的全貌,多画一些也不是坏事。毕竟,有些功能实现的时候,也得考虑隔了两层系统的人如何操作更加便捷。

当然了,上述的两个例子并不是说电商系统一定比银行系统要简单多少。而是关注的粒度不同。划分边界,对于更明细的粒度,可能关联的系统越多。比如在一个比较有规模的电商平台,已经微服务化分成了多个子系统,我们需要新增一个“分销”系统,可能会有如下的边界:

分销

把边界划清楚,并不是为了不管外面的事情,而是为了更好的响应外部的需求,以及适应外部的变化。这是很重要的一点。

参与方

都有哪些类型的用户使用这个系统。一般来说,如果是带界面的系统,会比较容易理解一点,比如一个办公系统,可以像下面那样分析:

  • 员工上去办理各种申请,查询各种文件;
  • 领导对各种申请进行审批;
  • 人力资源开通员工账号,设置员工状态;
  • 财务部门专门对报销等财务信息审核、处理;
  • 管理员设置系统参数等。

可能还有很多其他的“角色”。这里我们用“角色”来表示参与方会更加精确,因为一个用户可能同时具备多种角色,类似于身份,比如在办公系统,某个财务部门员工需要审核报销,但他也是需要为自己的报销发起申请。

而对于面向公众的服务系统,一般分外部和内部用户两种。外部用户一般就是客户了,不同类型、不同级别的用户能使用的功能不尽相同。

内外部用户

内部用户一般和组织架构有关系,毕竟一般公司都是按照不同的部门划分权责,不同部门的员工参与系统的操作就各不一样了。

划分参与方的角色能更好地分析系统的功能需求,而且,对后面设计流程模型和数据模型都有帮助。有很多业务流程是需要多角色配合的,而且涉及到各种状态变迁。

分组分类

对功能分析最重要的一项就是分组分类了,我认为。现在系统都相当复杂,最明细功能点(比如菜单的最明细层)都会几十个、甚至几百个,这些功能点必须有组织的码放整齐。我们一般都会采用一种层次结构(hierarchy)形态组织众多的功能。具体的方法,其实没很多可说,其实原则就是一个,著名的

自顶向下,逐层细化。

我自己再加一句:

自外而内,逐步深入。

自顶向下不用多说,这句话太出名了,通用的分析方法。类似脑图这样的工具,很好的支持了功能分析,一层一层的罗列出来,然后归纳,再罗列,再归纳。所有的功能组织成一棵树状结构。一般原则,一个节点的子节点不要太多,七八个就好。

功能分解

上图是一个功能分解的组织的例子,不完整,只是简单举例示意而已。

自外而内,意味着我们应该从可见的功能出发,比如用户的操作菜单,以及系统提供的对外接口。前面提到的,先确定边界,一按来说,对外功能整理清楚,就完成了绝大部分的工作了。

菜单

对外提供的功能一般都有一个入口,对现代系统来说,基本上都可以用URL来表示了。入口的组织或表现方式,有三种:

  • 功能菜单:一般内部系统(MIS),或者后台运营管理系统都有一个明显的分级菜单。不管是放在左边竖向,还是上面横向。菜单功能的组织就是一个典型的分层组织。
  • 导航:面向客户的界面可能没有集中的功能菜单,一般通过页面导航、链接、按钮体现,会分布在不同的页面中。所以在表述功能模型的时候,合理的分组分类就很重要,可以清晰的知道想要的功能应该放在哪里。
  • 接口API:系统可能提供不带界面的API,调用之后执行某些动作然后返回某些数据或者提示。

除了可见的功能之外,还有一类型的功能对用户不可见,比如:

  • 支付状态自动查询:第三方支付系统回调超时需要自动查询支付状态。
  • 自动对账:根据设定好的时间自动下载对账文件进行对账。
  • 结算佣金:自动提取订单完成状态,为分销商结算佣金。

等等,还有一些非业务需求,比如需要记录访问日志等。

分组的作用:

  • 是便于分析、归纳,不容易遗漏功能。
  • 相当于建立了目录,便于检索,方便和业务人员沟通。
  • 有助于模块划分,同一类功能一般在一个模块实现。

物以类聚,人以群分,分组没什么标准,但是会让人看起来舒服。而且在这个分组框架下,可不断细化、完善。

逻辑层

功能模型的逻辑层,其实就是用更加规范严谨的表达方式,把功能描述清楚。由于功能描述确实没有太好的规范遵循,同样叫功能规格说明书的,不同公司不同模板,就算同样模板,不同人写出来的也千差万别。所以,遵循一定的规则,确定一个自己的规范就好。

一般来说,功能描述应该包括如下几方面的内容:

  • 功能组织图:完整的功能组织图,包含合理的分类以及所有的功能点。
  • 分层、分模块:所有的功能在系统中应如何有机的组织起来,落在不同的层和模块中。
  • 接口:规定好每一个功能的接口契约。

功能组织图

前面说过形成一个分层的组织架构形式的功能图,多级展现功能分组和所有的功能点。最后,输出一个表格。如下:

功能表格

其实,怎么做都可以,主要清晰,直观,最重要的,是把功能都分析完整了。主要包含如下要素:

  • 分组编码:一般情况我喜欢为每一个功能和功能分组都做一个编码,在交流的时候可以精确的表示是哪个功能或分组。
  • 分组名称:每个分组取一个有意义而且唯一的名称,让大家一看就知道什么意思。一般分组层粗不要太深,一般经验不要超过4层。
  • 功能编码:每个功能唯一的编码,分层中最终端的是一个具体的功能。
  • 功能名称:每个功能起一个唯一的名称。
  • 功能说明(备注):对功能进行简单的说明。

表格的好处,除了清晰之外,还方便于升级成为开发功能的跟踪(checklist),测试的跟踪等。虽然到了那个时候,会有一些专门的工具辅助了,但一个好的表格,永远是好用的。

层次、模块化

和分组同样,把功能分在不同的层次、模块,其实也是一种功能的布局。分组是从业务的角度,形成菜单让用户很容易找到需要的功能入口。而分层、分模块,则是针对软件设计的角度,如何布局才最合适开发。

分层概念在软件设计中非常常用。最常见的,会把一个系统分成界面交互层、服务接入整合层,应用层,数据层等各种划分方式。分层是对功能的横向切分,而模块则是纵向切分了。在同一个层次中,分成多个部分。

如下图是以前为某银行设计的掌上厅堂系统的功能分层:

功能分层

功能的分层、模块化可以看做是一种功能的布局,对一个架构强迫症的人来说,这个功能布局一定要好看,整齐,清晰。一般来说这种功能布局图,是一份架构或者业务说明幻灯片中最重要的一张图。为了把系统介绍的更加完整——有些技术性功能也是系统的优势——会把一些技术组件/功能也呈现出来。布局,其实也隐含着模块之间的关系:通过位置或者更加直接的线条连接,体现一种静态的依赖和聚合关系。

如一个电商系统的功能布局图:

电商功能架构

其实在我看来,具有功能布局图和分组功能清单,就已经很足以说明系统的功能了。至少是功能架构中最重要的部分了。所以,画的好看,很重要。

如果是对于模块之间依赖有明确要求,或者比较复杂,需要专门表示模块之间的依赖关系的话,也可以用关系连线明确的表示模块之间的关系。比如电商终中的分销模块:

分销模块关系依赖

从图的表述来看,复杂的东西一定要简化,如果把整个电商系统所有模块的关系依赖都画出来,这个基本上也没法看了。

接口

从面向对象开发开始,到网络化服务(dot net, SOA,微服务),前后分离等等软件设计模式的发展,接口也来越重要。从功能层面谈接口,我们一般特指WEB API接口,也就是URL形式的接口。近年来我很崇尚RESTful设计风格——RESTful+json实在太赏心悦目了。所谓RESTFul是一种基于HTTP的接口定义风格,几个特点:

  • 每种资源有一个URI来表示;
  • 对资源的操作包括:
    • GET用来获取资源
    • POST用来新建资源
    • PUT用来更新资源
    • DELETE用来删除资源。
  • 服务无状态,数据传递用JSON。

JSON这个是我自己加的。

操作从语义上非常好理解,获取(GET)某个资源,新增(POST)某个资源等。因此,这部分输出是一组表格,包含了系统提供的所有服务接口,如:

API接口

接口定义要素包括:

  • 接口编号:还是我的习惯,喜欢任何一个东西都有一个唯一编号。
  • 接口名称:说明接口的名称和作用。
  • URL:接口的入口地址。
  • 方法:操作URI资源的方法,如POST、GET等。
  • 请求参数:请求接口的参数,展现方式其实无所谓,为了让业务人员能看懂,我一般用表格,包括参数名称(代码)、名称和说明。
  • 响应数据:返回的数据。

其实具体形式没关系,简单清晰就行,对业务和对开发都能沟通清楚。

流程模型

前面提到,功能模型可以类比UML中的类图,是一种静态模型。而流程模型则是一种动态模型,表现上是不同的操作步骤组成一个业务流程,导致记录数据的变更或状态改变。功能模型体现了要做什么,流程模型则体现了如何做到。

流程有很多比较成熟的工具来表现,比如流程图,泳道图,活动图,状态图等。甚至如适用于工作流的系统还可以用BPMN的实现来表示,画完流程图对应的流程实现就出来了。但工作流引擎不适合用在对并发和响应时间都有要求的交易系统中,所以大部分情况,我们都要老老实实分析业务流程,落实到每个交易的实现流程,画出流程图,写出说明,然后交给开发。

概念层

在流程模型中,我定义概念层是纯业务流程的表述。

业务流程

一个系统本质上是为了实现若干的业务流程的。比如还是电商系统,最核心的流程可能是:

电商主流程

一个简单的流程图,可以从很高的层面表现用户在电商系统的最主要的操作,或者是设计者希望用户的遵循的操作流程。在如此高层次的流程图中,每一个步骤其实是一组操作的组合。同样遵循自顶向下,逐层细化的原则,把大流程的每一个步骤细分为具体的操作流程,然后小流程中的每一个步骤展开为交易流程。

流程分层

一个操作流程中的每一个步骤一般情况可以认为是用户的一下操作(点击),比如浏览商品的操作流程可能为:

  1. 查看商品列表;
  2. 查看商品详情,包含查看商品评价等。

可选操作如:

  • 收藏商品;
  • 给好友推荐商品等。

这里的每一个操作,基本上会对应一个后台的操作(WEB API接口),而这个WEB API接口的实现会包含多个相对独立的操作步骤,即经历多个模块,子系统或者微服务。某电商系统的一个流程例子:

电商流程

这里为了更明显的看出逐层展开的过程才这样画。实际的交付,会单独画出"提交订单"的交易流程,会更好看。这里的交易流程采用的是类似活动图一样的方式,可以更好的体现出多个子服务之间的依赖和时序关系。

参与用户

这里表述的参与用户,和功能模型中的参与用户定义一致,都是具有该系统某些角色的用户。功能模型中定义不同的角色可以操作不同的功能;而在这里,则是从一个流程的角度出发,不同的角色在不同的阶段介入流程。当然,实际上最终也会体现到具有不同的功能。其实就是更换一个视角来分析。

可以用泳道图(跨职能流程图),把角色加入到流程中。比如用户申请退换,除了用户进行申请之外,还需要运营岗进行审核,收到退货之后,经确认,转单给财务退款,最后更新订单状态。如下图:

退货流程

当然泳道图还有很多作用,除了表示不同的用户,还可以表示不同的组织部门、不同的系统等。反过来,也有很多流程只有一个用户执行,那就不需要划分参与角色的区别了。

逻辑层

对于流程模型的逻辑层来说,就是面向设计的流程设计了。这里应该有三个重点需要关注:

  1. 状态迁移:重要的状态的变迁,比如订单状态。
  2. 流程图:对于重要的交易流程的设计。
  3. 一致性:由于现在的系统往往是分布式的,在一个业务流程的实现中需要考虑一致性。

状态迁移

状态图其实是很古老的东西。从计算机专业的本科课程开始,我们就知道软件处理的都是有穷状态。还有一门专业课程叫“有穷状态机”,来表示各种状态以及状态的迁移。典型的案例就是电梯在各楼层停靠。比如停靠在1层,是一个状态,有人在5层要下去,按了按钮,就是一个状态迁移条件,触发了电梯的状态往5层转移。而大部分的商业软件的各种业务流程都会涉及到一些重要实体的状态变迁。比如,用户的状态,订单的状态,资金的状态等。

如电商系统中的订单,是最核心的实体之一。订单状态比较复杂:

订单状态

严格来说,一个状态图应该包含如下要素:

  • 源状态:存在特殊状态表示开始,每个状态都应该有唯一的命名,比如订单处在“待支付”状态。
  • 触发器:发生某个事件的时候,导致状态发生迁移,比如用户“发起支付”或者用户“取消订单”,分别导致状态向各自的目标状态迁移。
  • 条件:附在在触发器,符合条件转移,或者形成分支。
  • 目标状态:原状态由某个触发器触发,进入目标状态。
  • 操作:进入目标状态的同时,附带的操作。

对于状态多的实体,状态图会比较复杂。但要知道,越复杂的状态才更需要一个完整、准确的状态图,否则很容易有遗落,甚至状态有矛盾。

状态图设计并不难,充分理解业务就好。只有一点,别把操作当做状态,这两个词看着没啥相似的地方,但在实际设计的时候,却是很容易搞乱的。比如,“退货处理中”这个状态,接下来会迁移到什么状态呢?很多时候会直接下意识的给出“拒绝退货”和“同意退货”这两个词。但这两个词其实是动作,是触发器,而非状态。拒绝之后,进入什么状态?这是需要好好思考的。

流程图

此处流程图应面向设计,充分表述清楚核心功能的实现流程,尤其设计到跨层次,跨子系统,跨模块的调用关系和次序。流程图的设计依然是用标准的流程图,泳道图,时序图,活动图等,甚至直接用原型描述操作流程。按照前面对流程分三层的划分,这里应该重点提现:

  • 操作流程:一个用例(这里用例比较准确)实现过程的每一个独立的操作,包括用户点击、页面跳转等。在实际设计操作流程的时候,往往会从流程图开始画,然后变成页面跳转的示意图,然后,结合功能点的具体要求,变成原型了。
  • 交易流程:对于用户提交的每一个后台请求,在后台系统实现的流程。如果要精细地设计,可以使用时序图,泳道图。而对于架构师来说,不会对所有的流程都进行设计,而是会对核心的流程进行跨系统流程的设计(结合状态图)。

原型设计工具真是一个很好的东西,不管Axure还是墨刀。一般来说,原型设计是产品经理的工作,但作为万能的架构师,最好也会。这里不深入展开,只需要知道成败在细节——产品经理可能更关系页面布局、操作;架构师可能更关系流程和数据。

交易流程的设计是为了更加清晰的看出时序(流程中时序是非常重要的),可以采用时序图的方式,但可以把设计层面放的高一点,比如是子系统之间的时序交互。比如,关于订单触发分销的时序:

分销时序

当然,我个人来说,更喜欢用泳道图来画。最后,就是用流程图描述在一个子服务中的处理步骤了。这里不展开说了。

一致性

对于一个分布式系统来说,一致性是很重要的,而且还是很麻烦、很复杂的事情。关于一致性的问题,我在另外一个文章中软件开发随笔——分布式架构中有专门的讨论。有兴趣的可以看看,请不吝赐教。

模型层面为什么要关注一致性呢?在这里的主要是考虑业务上的,并不是单纯数据库的事务提交或者回滚。因为从业务上,每个操作都是独立发起后台请求的,而两个或者多个操作之间,存在因果依赖关系。比如,预览订单的时候,用户选定使用积分或者优惠券,此时需要冻结这部分积分。如果不冻结会如何呢?用户这笔订单暂时没有支付,而去支付另外一笔订单了,同样使用了积分或者优惠券,然后支付成功了。这时候回去再支付上一笔订单的时候,发现积分不足,或者优惠券已经无效了。这笔订单的支付就会无效了。给用户的体验就不好了。

反过来这样的顺序,借用上面的时序图来表示一下。如:

订单支付

从上图中看到,红色加粗的四个操作,可以认为是一个逻辑上的“事务”。时序图不好表现分支,但可以增加描述,表现其特点:

  • 冻结积分:冻结之后,积分账户的可用余额减少,如果用户再次下单,选择积分的时候,就只能看到被冻结之后的可用积分,而不会选择超出可用范围,用户体验会比较好。不至于像上面说的,用户选择了,但实际支付的时候发现积分不足。
  • 积分支付、现金支付、更新订单状态,三个操作是有时序依赖的,后面的任何一步发生错误,都需要依次撤销之前的操作。比如,现金支付失败了,需要把积分支付给撤销掉。就是所谓的“冲正”。需要注意的是,冲正之后,积分依然是冻结状态的。
  • 如果订单超时,或者用户取消订单,需要对积分进行解冻操作。

这里需要关心在什么地方需要考虑一致性,而不是采用什么机制保障一致性。在分析流程确定了那些操作具有“事务”要求之后,需要做的是:

  • 提供对应的反交易。
  • 对异常的处理需要调用对应的反交易。

功能模型和流程模型的物理层

物理层就是面向实现的,对于功能模型和流程模型来说,最终的实现基本上都可以认为是代码。有一些特殊运行平台,比如工作流引擎,可以直接运行BPMN的脚本。当然,我们也可以认为这个配置脚本也是一种代码也是可以的。代码实现基本上功能和流程是分不开了,毕竟功能的实现就是通过代码组织的流程,当然,最后会记录一些数据在数据库。

所以,我这里定义功能模型和流程模型的物理层包括几个内容:

  • 可以直接运行的配置、脚本,比如Activiti使用的BPEL。
  • 可以直接生成代码的脚本、配置。比如类图,UML流行的时候,通过Rational Rose或者Together都可以直接生成JAVA源代码,尤其对接口类(Interface)适用。另外,某些MIS工具,也提供自己的定义格式,可以直接生成针对表格的怎删改查的代码。
  • 针对实际基础设施的部署,包括网络、服务器、存储、安全等。实际上就是一个网络拓扑图以及众多的部署脚本了。现代容器技术给了我们更加灵活、简便的实现,比如通过Docker来部署,一个Docker-compose.yml就可以把所有的节点配置好。

物理层直接面向实现,其实更多是技术层面的设计。需要针对特定的开发语言,运行环境(操作系统、虚拟化、容器等),甚至网络环境去设计。另外,有一些公共的功能,比如用户认证授权等,其实在开源世界,比如Spring Cloud都有支持,我们只需要部署、并且用起来。

意味着我们需要很懂技术了,当然这也是架构是的任务。不过我们这里主要分析如何建立软件的模型,并不展开具体的技术实现。再次给自己再做一个广告:软件开发随笔——分布式架构,有兴趣的可以看看,请不吝赐教。

数据模型

数据模型应该是最有规范的的一套体系。早些年就有“面向数据设计”,针对数据模型来设计界面和功能——当然现在又有新的“面向数据设计”了,基于大数据发现用户的偏好等进行系统优化,这是另外一个事情了。

我自己的体会,在十几年前开始比较大量的接触国外的系统,比如银行的Core Banking System,财富管理系统,资金系统等。发现几个特点:

  • 这些系统都很贵,国内的软件是青铜,他们的就是钻石。
  • 这些系统采用的技术都很保守,很传统,甚至可以说是很落后。就算到现在也一样。比如当年我们说的经久不衰400系统。
  • 这些系统的数据库表设计,也就是数据模型都很好,很清晰。

也就是,数据模型才是最值钱的。一个很复杂的系统,几十张,上百张,甚至几百张表的实体和关系,设计的很和谐。

ER模型

数据模型本身是一个很古老的东西,尤其在关系数据库出现之后,利用实体关系(E-R)图来设计数据模型的技术就出现了。但是很多人在设计数据库的时候,往往很茫然,不知道如何入手。确实,E-R模型的设计直接可以用的指导太少了,我总结一下,大概也就这些:

  • 从业务出发,识别实体,啥叫实体?太难明确定义了,你看花是一个实体,他看花瓣和花蕊分别是实体,另外的人会认为花粉也是。这完全是看你对业务的理解,关注点在哪里。
  • 识别关系,这个简单,一对一,一对多,多对多,强依赖(主键),弱依赖(非主键)。某些ER工具在做逻辑模型的时候,吸收了面向对象的特点,提供泛化(继承)关系,更方便设计实体。
  • 范式:基本需要掌握的是从第一范式到第三范式。学术的这里不讨论,大概知道就是不断地拆表,消除冗余,消灭异常(修改异常、删除异常、插入异常)。

E-R图本身的理论和范式,这里不做讨论,太成熟了,很容易找到资料。

从这里看出,可以用的指导太少了。所以,要设计好数据模型,需要几个东西:

  • 好的E-R模型设计工具,最好能支持逻辑模型设计,并转换为物理模型,以及生成对应数据库的SQL文件。
  • 好的模型参考,因为E-R模型本身的指导太少,所以一个好的模板作为参考是非常重要的。

工具层面就不多说了,主要是可用的太少了,还贵。这是一个很难过的事情。

这里主要说模型参考。

数据模型参考

业内最成熟的数据模型,是早年我们叫NCR模型,现在叫Teradata FS-LDM模型(针对金融行业)。当时我比较熟悉的是7.0版本,有将近1200个实体、5000个属性,200多个逻辑视图来阐述金融的数据模型。而之后一直在持续升级,不断地增加实体和关系。

为了更好的阐述数据模型,Teradata的数据模型分成三层:

  • 第一级:主题域,一般在10个左右;
  • 第二级:概念层,50多个关键实体;
  • 第三级:细节和逻辑视图,将近1200个实体、5000个属性和超过200个逻辑视图。

下图就是一个典型的主题域模型。一般耳熟能详的Party域(参与方,而非客户),以及协议、资产、产品、财务、营销、渠道、地理位置、事件和内部组织架构等。基本上,要设计一个业务系统所涉及的数据(表),都可以套到这些主题里面去。

NCR模型主题域

这里可以简单的说明一下各个主题域的意思(这模型主要针对金融机构,后面我们用“机构”一词来指代平台运营方):

  • Party团体,描述客户的基本信息,如不同的客户类型或角色等。
  • Party Asset资产,描述客户的资产,有些模型可能增加专门域描述金融资产。
  • Finance财务,客户的金融资产,比如各类账户情况。
  • Campaign营销活动,针对客户展开的各种试图达到某种目的活动;
  • Agreement协议,客户与机构之间的各种协议。
  • Channel渠道,客户与机构发生交互关系的渠道。
  • Event事件,客户发生的各种事件,与系统发生的交互也可以看做一种事件,但有些模型提示我们,可以把针对系统的交易当做一个独立的主题域。
  • Internal Organization内部组织架构。
  • Location位置:描述物理坐标或者定位(互联网位置、电话、移动终端标识等),以及这些位置标识和用户的关系等。
  • Product产品,我们能为客户提供的产品和服务信息。

主题域和软件的模块其实没有必然的联系。主题域单纯对客观事物进行分类。大概意思就是这个世界应该有些什么东西,但你要用这些东西来干嘛,就不管了。比如我们的传统玄学,世界由金木水火土五种基本物质组成。主题域的作用,就是给表分类。如果我们并不能直接应用NCR的模型进行改造,我们也可以根据主题域给我们的提示,在我们设计一个系统的表的时候知道应该有几类表需要设计。比如一个电商系统,可能出了Party Asset域之外,其他的域我们都会用到。

下图是Party的其中一个片段:

party

在这里,引入了泛化的关系,类似面向对象的继承一样,一方面为了充分表达“分类”的情况,另一方面,转换为物理模型的时候,父类的属性,会自动放到子类中,节省劳动力。

另外流传比较广泛的还有IBM的数据模型,花旗的数据模型等。其实大体方式差不多,但主题域的分类方式不太相同。有兴趣的可以看看我写的另外两个文章:

参考模型来进行ER模型设计

之前说过,设计表的时候,我们首先要识别实体。而有主题域的分类指导下,我们直接针对Party进行分类。根据Party的定义,是与系统发生关系的一切参与方。我们就可以之类对Party进行分类。当然,分类有很多种方式:

  • 参与方的组织形态:
    • 个人
    • 机构
      • 公司
      • 政府部门
      • 社会团体
      • ……
  • 参与方和系统/平台所有机构的关系:
    • 潜在客户
    • 客户
    • 商户
      • 零售店
      • 餐厅
      • ……
    • 供应商
    • 经销商

如此,我们找出所关心的所有分类,就可以识别所有的实体。其他的域,比如产品、协议等,也进行分类。这样,基本上就能把关心的实体识别出来了。虽然也不是那么容易,但毕竟有一个指导,把无目的的思考变成了特定分类的罗列,还是简单多了。

确定了实体之后,确定属性。比如,对于Party中的个人,正常思考,属性会包括如下:

  • 姓名;
  • 生日;
  • 身份证号;
  • 电话;
  • 地址;
  • 职业类型等等。

而与机构签订的协议,用户在机构拥有的账户,发生的交互事件等,我们可以看到都有专门的主题域,也就是有专门的一套表。所以我们知道在对应的主题域中专门针对这类实体进行分类,并建立和Party的关系就好。

回来在看这几个“基本属性”,貌似很简单,但深入一下,会发现,如果你关注的更多,比如,姓名是否变化过,住址是否变化过,或者多个不同的地址(办公地址,家庭住址,甚至多个家庭住址)都要关注?

如果关注这些的话,这些属性就会形成“表中表”,违背了范式要求。这样,我们就需要单独拆出来一个表,比如Party的名称,我们需要跟踪名称的改变情况,那我们就需要记录每一个名称以及名称的有效期,当前生效的名称。与Party形成一对多的关系。

又比如住址,由于有专门的Location域,我们会在其中专门设计地址的要素,比如从国家->省->市->区->街道分成多个层次,然后再输入具体地址,可以更好地利用地址数据来分析客户。这时候,就会现成一个Party和地址的关联表(多对多),同时在这个关联表中还得加上关系类型,比如居住地址,办公地址等。

关系示意

原谅我没有很好看的逻辑模型设计工具。有点丑陋,但意思出来了。

总而言之,一个既有的,成熟的模型,就是一个好的指导。熟悉他的表,熟悉他的设计方法,分析自己的业务的时候,也就顺理成章的把数据模型给设计出来了。

在具体设计表的时候,我觉得还有几点实践建议:

  1. 个人喜欢每张表用一个3位的编码表示,比如:T101_PERSON,第一位数字表示所处主题域。后面两位是域内的唯一编号。其实任何编码方式都行,只要能清晰的表示唯一。
  2. 按业务模块分别画E-R图,比如在电商里面,商品可以画一个图,订单可以画一个图。基本原则就是一张图不要超过一个A4页面范围。
  3. 外键关系一定要有,尤其逻辑模型层面,组合主键一定要保留,不要过早改成无意义的唯一序号做主键。丢失了关系,设计就会变得很难维护。

联合主键这个事情,一直都有很多争议。毕竟,如果一张表主键有好几个字段,在程序里面修改删除等,确实比较麻烦,另外有一些CRUD工具不支持联合主键。以至于很多人设计表结构的时候,凡是联合主键的都用一个无意义的自增字段或者序号来代表;而原来的主键字段则变成了非主键的弱依赖外键了。唯一性保证就需要专门创建联合唯一索引来实现,或者通过程序来保证。在物理实现的时候,这两种方法谈不上好坏,但逻辑设计的时候,还是尽量按照客观事物本身的内涵和关系去设计,不要过早加入物理实现的约束。

关于逻辑模型的设计最后补充一句就是尽量每次都按照正确的方式去努力设计,可能一开始慢,但多做几次之后,就掌握技能了,熟能生巧。

物理模型

逻辑模型转换为物理模型一般的ER工具都支持,比如把泛化关系转化为具体的表,多对多关系也落实为实际的关系表等。除此之外,我们一直说,物理模型是针对实现的,一方面是针对具体数据库的设置,另一方面更重要的是要针对程序实现做优化。比如:

  • 视图及物化视图:为了便于查询,有时候会把多个表的字段放到一起,形成一个视图,在程序端可以当一个表用。但动态视图效率很低,数据库一般支持物化视图,事先把数据保存下来,在查询的时候快速返回。而实际的表数据改变,物化视图中的数据也会对应的改变。
  • 索引:在业务上,大部分的查询都不能仅仅通过主键实现,索引的作用就很重要了,根据业务的需求,把常用的条件字段作为索引,提高检索效率。
  • 分区:对于数据量会持续快速增长的表,可以设置分区,不同的分区实际保存的文件不同,提高在一个分区中的检索效率。增长和时间相关的,一般可以通过日期做分区。
  • 分库:实际上就是数据集群了,采用何种方式集群需要考虑数据一致性的需求,是读写分离,还是横向分割(不同表在不同数据库),还是纵向分割(分库实现分区)等。

当然,最重要的一点,是选择好用什么数据库。开源的Mysql还是商业的Oracle,当然这两现在是一家。或者用Mariadb,PostgreSql等。其实都很不错。甚至有人会选择无强制模式要求的MongoDB。MongoDB确实是很优秀的数据库产品,没有强制模式要求,也就意味如果需求发生变动修改表结构不用做太多处理——存数据的时候直接加上就好了。但我个人认为,在部分特殊场合,比如需要快速记录交易流水/日志,而交易流水/日志的格式多种多样(不同的交易需要不同的数据),可以使用MongoDB。但作为一个系统的整体设计而言,决策一个表是否修改的思考和决策过程,应该会远超过实际改变了表结构之后对程序的修改时间。

如果严肃的对待“数据是资产”这句话,就需要慎重的建立一个清晰的数据模型,否则,混乱的数据结构会变成灾难。这也是各个企业后期大量精力去做所谓“数据治理”,投入大,成效小的原因。只有要可能,从一开始就重视数据模型是不会有错的。

面向分析的数据模型

达到或接近第三范式的数据模型我们认为有两个作用:

  • 满足联机交易系统(OLTP)需求,避免数据冗余,消除各种数据异常。
  • 构建数据仓库的时候作为企业数据模型反映业务全貌。其实前面提到的Teradata等模型,都是在数据仓库项目中推广开的。

而在为客户提供的分析界面的时候,基本上不会直接针对此模型去提取数据。这种为客户提供数据检索、分析的系统,称为联机分析系统(OLAP)。我们会定义这一类系统所处理的数据有如下几个特点:

  • 批量更新,至少也是非实时更新,不需要考虑交易事务的一致性。往往可通过ETL工具或者消息队列传递过来。
  • 不可更新,进入的数据不可变动,因此也不考虑跟新异常、删除异常等。
  • 有时效性,为了反应企业业务发展的全貌和过程,所有数据都会有时间表示,代表其生效的时间。

由此也产生了一系列专门面向分析的数据库产品,如Hadoop生态的HIVE、HBase等。具体产品、工具这里不展开了。主要考虑针对分析查询需求的数据模型构建。一个最基础的手段就是:

逆泛式

或者叫降低范式。简单的来说,就是允许数据冗余出现,表现有两种:

  • 根据客户查询的需求,直接把多个表的数据合并在一个表,类似于物化视图一样,但这里的数据往往需要经过复杂的处理计算,形我们一般称为“宽表”的结构,就是字段很多的表。这样客户在这个需求范围的查询,都可以在一张表内完成,不需要做表连接。
  • 多维模型,又称为立方体的一种特殊ER模型结构。通过维表和事实表的关联形成一个像星星的形状。

维模型如下图:

维模型

其中,时间维、地区维等维表,是降低了范式要求。比如按照第三范式的要求,地区维应该国家、省和市各一张表,才能避免数据冗余。但在这里,为了快速查询,把多个层次的数据合并在一个表里面。算是一种空间换时间的策略。

  • 维表:所谓维就是分析的角度,统计的方向。比如,我们希望看到某年某月所有的订单数量,或者某年某月某个产品的订单数量,甚至某年某月某个产品在某个地区的订单数量。一般的分析角度都会有很多,所以,维模型也称为多维模型。
  • 事实表:事实表是反映业务发生事实的表,由其所关联的维表的外键和需要统计的指标组成。因此,统计算法可以看做是在事实表不同条件分组汇总(SUM & GROUP BY)。从概念上,指标也可以看做一个维度,但为了减少事实表的记录数量,便于操作,把指标维拉平了放到事实表的字段里面了。

用户的界面则是选择出需要看的维度,其他维度蜷缩(汇总或者过滤),形成一个二维平面的表格:

报表

如上图,展现了机构和各种指标的关系。而这些指标的计算,则是汇总了所有时间范围、地区范围和币种范围。而我们可以附加过滤,只看今年和人民币的。表格形式不变,但里面的数据会发生变化。

维模型对于分析来说,是非常重要的,如银行需要的维表大概有:机构、科目、产品、币种、时间、地区、客户等公共维度。

技术实现

模型最终需要通过技术手段来实现,也就是变成可以部署运行的代码。软件技术发展到今天,出现了无数很好用的框架、组件、工具,这些都对我们的软件开发的方式造成了影响。分布式从分层、集群到现在的微服务,各种开源组件可以帮助我们迅速构建技术原型。当然了,各类型开源组件层出不穷,也给架构师带来了昂贵的学习成本。这里不打算讨论具体的工具和技术,这方面可以参考的资料很多,只要你知道你需要什么,肯定就能找到。“面向百度编程”是很重要的。在互联网信息时代,只要你知道有一个东西可以用,基本不是傻子都能在网上找到这个东西,以及这一类东西都是什么,怎么用。从入门到精通都不在话下。

这里我试图把我自己常用的一种技术框架以及设计的思路方法展现出来,为大家提供点思路。

技术分层

实话实说,现在一个好的架构师真不容易。除了上面说的业务分析之外,在实现的时候,也必须懂的很多技术。不过确实,一个现代软件从设计、开发、运行到维护,确实需要大量的技术。而且技术还不断地在发展,架构是需要不断的往自己的“技术栈”添加新的东西。

当然,我个人并不喜欢“技术栈”这个词。把技术先进后出的堆砌,总觉得不是那么个味道。个人更新喜欢传统一点的“知识体系”这个词。作为一个架构师,学了无数东西,每个人都会在自己脑中建立自己的知识体系。把各种知识点归纳出来,形成一个合理的“架构”,或者建立一个形象的布局,知道那个地方需要什么东西。比掌握具体的技术点更重要。

知道要用什么技术,是架构师最重要的能力。

“形象”这个词,我一直认为很重要。软件开发本来是一个很抽象的事情。而我这么些年做的很重要的一个事情就是,把抽象的软件开发过程变成形象的、直观的展现。在前面讨论功能分组、数据中实体分类,我都有强调,归纳分类是非常重要的。

一个可以参考的技术层次和分组如下:

技术分层

从基础设施到运行环境、开发支持、互联网服务。逐层向客户贴近。

其中数据库,我放到了运行环境中。以前单纯的使用关系数据库保存数据的时候,是放到基础设施一级的。但各种NoSQL数据库作为高速缓存,内存数据库辅助计算等,也成为一种设计主流。也就是“数据库”更往应用层贴近了。

Devops是一个近年比较新的词。一个软件开发设计的周期包括:

开发过程

简单的说就是包含了:

  • 软件开发技术。
  • 质量保障技术。
  • 运行管理技术。

这是典型的瀑布流程。而现代软件开发采用迭代模式,每一个迭代周期都经理完整的上述过程。迭代能更好的保障软件交付的效率,但带来的问题是对于一个线上系统来说,会面临多次的升级,容易造成停机事故,影响业务。

所以出现了把上述三类技术融合在一起的Devops。融合性的技术都比较复杂,我认为比较重要的点在于:

  • 版本分支管理。
  • 构建、测试、部署自动化。
  • 容器技术及容器管理自动化。
  • 监控技术。

开发语言方面,我个人相对传统,强烈建议对于一个复杂的系统,应该采用强类型、面向对象语言,比如JAVA来进行主体的设计和开发。对于特定模块、服务,采用其他快速便捷语言补充,比如PHP,Python等。程序开发不仅仅考虑开发效率,还得考虑维护成本。

技术架构

上面按照一种“层次”来划分技术形成知识体系。还可以按照一个软件应该的“样子”,形成一个架构,或者脚手架,结合分层和分模块,更细致和形象的从技术角度描述一个软件的样子。如下图:

技术架构

相当于把从底层到用户层的分层设计,每个层次所需要的模块,以及公共的支持工具,都做了一个布局,有点像玩乐高。但所谓好坏,就是看起来优雅、匀称。

把具体的功能或者技术要求的实现替换上去,差不多可以得到一个大概的技术架构。不见得适合所有的项目,但对于我做过的项目,基本上可以覆盖。这了不打算展开,反正每个词百度或者狗狗一下,就都出来了。

总结

这篇文章基本上不涉及具体的技能、工具,更多是我对“如何设计好一个软件”这个问题多年来的思考和总结。要回答这个问题,我觉得第一是要说清楚这个软件究竟是什么样的。我自己的答案就是上面描述的“3+1”模型:

3:(功能模型+流程模型+数据模型)
1:技术实现

一般人都说做软件是一个逻辑性特别强的工作,确实也如此,尤其是在设计业务逻辑,算法上,人类需要用到的所有逻辑方法,都需要用上——演绎和归纳。但就是因为太过抽象,所以我们需要各种工具(流程图、脑图,甚至用图画工具画画,用笔画画)来努力让软件变得具现化,更直观,更形象。所以我认为其实至少在架构层面,形象思维和逻辑思维是同样重要的。

虽然前面写了那么多工具,画了这么多例子,其实,对于我来说,在设计一个软件的开始,最重要的在自己脑海里面把软件运作的整个过程推演之后,形成一个具象化的实物投影一样的概念。就跟我看到一个水壶之后,这个水壶会在我脑海中形成的立体影像。只不过形成这个投影的过程很艰难,尤其随着要设计的系统越来越庞大,一次性把所有东西都装到脑子里面推演放不下了。必须借助各种工具存下来。基本的流程,就是前面介绍的这么一大堆。

具体的工具和技术很重要。但是知道该怎么做更加重要。我特别欣赏据说是爱因斯坦说的一句话(虽然我也没考究过是不是他真的说过):

字典里有的东西我从来不记!

我的话则是:

网上能随便查到的东西我记来干嘛?

互联网信息爆炸时代,任何东西,你知道他的名字,甚至知道这东西的关键词,你都能找到所有的资料,然后快速掌握他。就怕你不知道有这种东西。几乎任何技术,都可以速成。我经常和开发人员说,你还没本事碰到别人都没碰到过的问题。所以你碰到问题,先上网找。找不到最大可能就是:

  1. 换搜索引擎(不可说)。
  2. 没看英文的网页。

英文好不好都必须能看懂英文网页。这是一个很基本的要求。

建立自己的知识体系,是最重要的——对于一个架构师来说。形成了对自己行之有效的思维模式,甚至一些设计模板,在拿到需求之后快速形成方案,并评估项目的难度、开发周期、迭代次数,甚至于投入资源等等重要问题的答案。

其实,做软件本身需要建模,而对自己的知识体系,也需要建模。这才是软件的“里”或者“道”,具体技术是“表”或者“术”。——一家之言,可以无视。

关于一些具体的开发技术、开源组件如何快速构建一个分布式系统,可以参考软件开发随笔——分布式架构(再次宣,最后宣)。有兴趣的可以看看,请不吝赐教。

  • 3
    点赞
  • 0
    评论
  • 9
    收藏
  • 打赏
    打赏
  • 扫一扫,分享海报

参与评论 您还未登录,请先 登录 后发表或查看评论
©️2022 CSDN 皮肤主题:游动-白 设计师:我叫白小胖 返回首页

打赏作者

老程序员一叶知秋

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值