一枚端同学的自白(纲领篇)

16 篇文章 0 订阅
3 篇文章 0 订阅

动机

make it work, make it right, make it fast”。
此篇文章题目叫自白,这样可以不限于某个话题,写的比较松散些,但是可以肯定的是,通篇关乎right! 不仅如此,鉴于自己的工作背景,此文仅对面向对象浓墨重彩

首先上面这句名言来自Kent Beck,主要说的是开发的三个顺序阶段。但我这里举个一个不太严谨的对比:
如果说work代表着产品的基本效用,核心功能,用户需求满足,外观交互,即围绕面向用户的产品价值
那么right代表包括开发期和维护期在内的产品的质量,内在品质,即围绕面向开发维护者的架构价值; (我知道Kent原话说的right不是这个意思,我偷换到了一个接近而有借鉴意义的概念)
fast有的时候来自于用户(“这太慢了”),然后我们改进它,此时它是必要的产品价值,而有的时候来自于竞品压力(“看,XXX已经控制在100毫秒了,而我们的产品还在300毫秒”),此处的fast我统一成性能瓶颈和或对黑科技的追求,而不单单指程序运行速度,可以看到,其实此时更多的是指附加价值,标榜一个品牌

虽然这三者都可以关乎项目的成败,但从财务角度可以说work和fast可能给雇主增加收入,而right可能减少成本(也不尽然),也许这是导致right目前地位不高的原因。

另一个特点是,right和fast都不会影响work(功能,组件行为)。但是right和fast之间可能存在矛盾,为了fast可能会导致代码难以理解,而为了right,可能增加性能开销。
为了用户,为了大局,牺牲right可能是无奈之选,但是正如Kent所说,除了某些无可挽回的重大决策,fast应该是最后一个阶段才做的,而不是首先牺牲right,或者处处牺牲right。80%的fast问题出在20%的代码上,而且不要臆测,用工具去度量出这些热点!

对应的,我职业生涯经常遇到这么几类人才,他们的分工和通力合作助力组织目标的实现:

  • 追求极致的产品体验,他们有的热衷于实现酷炫的动画,优化嵌套滑动,对UI技术有像素级的把握,有的对交互逻辑、视觉设计有独到的感觉,能敏锐的观察到用户的痛点并提出意见。他们大部分工作在产品需求满足上,并以服务客户为傲。

  • 追求各种基础技术,造轮子,比如线程库,注入框架,日志组件,监控系统,自动化工具,擅长抽象和框架化技术,喜欢研究软件复杂度问题,他们基于对广博语言的透传理解,和对api、模式、思想的优雅设计实践,通过一套类体系构建和元编程技术,将产品业务或研发业务进行工程化包装。

  • 追求极致的性能优化,利用某种度量分析工具以及对内存,gpu,cpu,io,网络,操作系统,进程线程,执行文件等某一方面深度的掌握,可能通过改造某个算法,可能用更低级的语言重写,可能利用缓冲缓存预取,完成某种量化指标的提升。

看起来,这篇文章好像应该助力你走向第二类人才。但是之所以有这篇文章。是因为我作为android端开发,我看到了太多类似下面这样的话(或现象),不论是面试还是工作合作:

  • 你用过什么图片框架,Fresco还是Glide,对他们了解吗?你们是用Gson解析数据吗?哦?反射没性能问题吗?
  • A:Java已经过时了,Kotlin又引入了新特性,你知道吗?不仅简洁(声明式),还助力函数式编程,我们打算切过去。。
    B:Flutter性能很牛逼,而且现在最时髦,我们打算切过去。。
  • 他们还在MVC,我们现在用上了MVVM,你们现在居然是用react+redux?
  • 不要用枚举,用public字段而不是getter-setter,那样会有性能问题!这个field一开始为什么要new出来,等需要的时候再new啊,你可以判空啊!
  • 每个变量都要有文档,每个函数都要写单元测试,单测覆盖率要达到100%。
  • 我们让每个服务都做成一个进程,再搞个多线程的服务调度管理器,每个服务一个project工程,代码上互不依赖,这样有扩展性,一个服务不会影响另一个,逼格也高,并发性能也好。
  • A:我写了个Clean架构Demo,并开源了出去。
    B:Clean架构垃圾,好繁琐,感觉为了分层而分层。

还有很多,不一一列举,这些不好吗?这些不对吗?没有,我的意思不是想抨击某个上述的具体内容或做法,而是我作为一个平庸的程序员,劝另一些不那么天才的程序员去关注一些被我们不太重视的一些东西。

不论是行业鄙视链还是晋升链,都要求我们足够牛逼,“我做出了一个酷炫的XXX效果”,“我写出来一个XXX框架”,“我将XXX的性能提高了多少”。
还有一条崇拜链,比如“Android源码没那么做,我们也不那么做”,“Fresco这么牛逼,早就想到这个了,会帮我们解决这个问题的”,“你看那个公众号了吗,XXX公司说XXX业务支撑了XXX并发”。
其实他们谈论的大多是语言华丽的特性,UI组件,框架,性能,偶尔也会吹吹自己处理过多么复杂的业务或重构过多么大型的项目,就算提到设计模式或架构模式,也是“形”而非“意”。

我再次强调,这没什么不对,这些能人大牛做到的东西是秉承着极强的信念毅力,技术实力来实现的,而我做不到或已经做晚了,你可以毫不留情的批评我吃不到葡萄说葡萄酸。
然而我是个平庸的程序员,身边可能也有迷茫的兄弟,甚至自己的小团队可能也没有那么出色,但是这妨碍我们成为一个高效凝聚,沟通充分,更有战斗力的组织吗? ,我们就这一群人,A项目黄了可以再做B项目。但是我们做的更好,更快的满足,每个人最终都会从中受益。
组织?是的,right是关乎一个开发组织的事(共识、纪律等),而单兵作战,收效甚微。

所以我请愿,我自白,我提议,我们可以对接下来的内容有所思考,有所沟通,有所共识,然后共同创建。注意,我的主要目的是激发,过程中给你的任何答案,不论正确还是错误,不过是一种参考。

如果组织轻到无法激发你的专业精神,比如你有委屈打算很快跳槽,比如你的boss和同事都凶神恶煞,那么我提前知会您,往下看的意义不大,你也很难去实践。

从哪开始

“前事不忘,后世之师”。——《战国策·赵策一》

我们有一种习惯,当遇到一个新构想(可能是一种新技术,新思想,新模式等)时,最开始做的就是百度下,然后捡起一篇可能是csdn(或博客园等)的文章得到了解,里面可能是一段轻松诙谐的文字和类比来讲解,夹带着自己的理解。
比如什么是设计模式?可以去看百科,里面介绍了几个原则和各个设计模式,可能还用一些生活化形象化的例子介绍了某个模式,你懂的。
再比如MVC,很多网文对其进行了自己所理解的介绍,
比如典型的理解是M和V隔离并通过C来交互,这不就是中介者模式?而MVP就是C换成了P,M和V变成了接口。
了解了这些之后,对这个东西的学习旅程戛然而止。

我非常推荐去看看原作者提出某个构想的背景,遵循的原则,如果是书的话,这些一般会在序言、前言或者第一章中介绍。
另外了解这种构想在历史车轮中所处的位置,这种信息对你审时度势很有用
比如我在排队上班车,司机出来说剩10个座位,于是10个人头后面的人开始去寻找其他空闲班车,但是又来一波未听到这句话(不明“历史”,信息不对称)的人继续后继在那10个人头后,可想而知,不过是浪费时间。
而此刻,我正有意分享这段“历史”(这里因为本人浅薄的了解和篇幅的限制,将举感兴趣和有里程碑意义的事或点来讨论)。

对我影响深远的大师,他们大致的出场顺序如下(希望我也大致对齐了时间轴):
Trygve Reenskaug MVC和OOram的提出者。
Grady Booch 《面向对象分析与设计》的作者,uml之父。
GoF 《设计模式:可复用面向对象软件的基础》的作者。
POSA 这里代指《面向模式的软件体系结构》(尤其卷一)的作者们。
Bertrand Meyer 《面向对象软件构造》的作者,Eiffel语言之父,Design by Contract的提出者。
Kent Beck 《解析极限编程:拥抱变化》、《测试驱动开发》的作者,敏捷开发的发起人之一,xp和tdd之父。
Martin Fowler 《重构:改善既有代码的设计》、《企业应用架构模式》的作者,敏捷开发的发起人之一。
Robert.C.Martin (后文我们称为“Bob大叔”)《敏捷软件开发:原则、模式与实践》,《Clean Code》,《Clean Architecture》的作者,面向对象设计原则的继往开来者。
Eric Evans 《领域驱动设计:软件核心复杂性应对之道》的作者,ddd先驱。
Peter Coad 《Java Modeling In Color With UML》的作者,四色原型和FDD的提出者。
Jim Coplien 《Lean Architecture for Aglie Software Development》作者,和Trygve Reenskaug一起提出了DCI架构,组织模式之父,精益架构创始人。
Sebastian Markbåge、Dan Abramov React模型和Redux模型的核心设计者(之一),React和Redux框架的核心开发者(之一)。

注:其他的牛人,如云端教父Adrian Cockcroft(云原生架构先驱)因为离端架构太远而不再我们讨论范围内,或者我暂未读到他们的作品,揣摩他们的思想。

上面人物介绍中提到的书,我至少读了一遍,有些读了3遍。之所以提到这些人物,因为他们是文章下面各种观点的支撑者。现在我们开始往下走,回归本真!

子话题:设计模式是书?

如果你看过GoF的设计模式(我已经听到你在尖叫:这本书的示例是c++写的,而我是java程序员。不过还是建议你看一手资料),就明白这本书根本没有提到SOLID原则。
GoF主要提了“面向接口而非实现编程”,“组合(委托)优于继承”这两个原则,并且提到了“区分类和类型”,“封装变化”这些理念
这不是说23种设计模式没有遵守SOLID,虽然事实上有些模式确实违反了SOLID。
不过你知道的,我不是想较真这个教条,我们没必要处处保证不违反SOLID,设计模式的使用也不需要照搬原汁原味,这些原则和模式是我们尽量要保证的,但是务实的作风也要求我们能灵活运用。

SOLID是在这之后由Bob大叔整理组织的(一些原则之前早就有了,比如开闭原则是Bertrand Meyer提出的,但它们零散在各处,而另一些是Bob大叔首先提出的),而GoF的设计模式里也只是感谢了Booch和Kent。
Bob大叔在《敏捷软件开发:原则、模式与实践》中提到了SOLID(里面没有“组合优于继承”,“Demeter”这两个原则),并且提到了组件设计中包管理相关的原则,同时提到了包括“激活对象”,“空对象”在内的多个未收录在GoF中的其他设计模式。
请问这些模式为什么没人使用?它们是劣质模式?怎么可能!相关网文不多而已。设计模式不仅仅23种,比如在你到处判空时是否曾想过“空对象”模式(Martin称之为特例模式)?
我已经听到你在说移动端的内存性能,我想说在你没有证实之前,它不过是你假想的,但是调用者的维护负担确是实实在在,有一处没判空就可能会失控。
悲哀的是自己认为自己在讨论设计模式,不过是在把那一本书当圣经而已!

POSA(卷一)是另一本关于模式的书,它也是建立在GoF的设计模式的思考之上的,并且里面按照“工作组织相关模式”、“访问控制相关模式”、“服务扩展相关模式”等方式分类了设计模式,这比GoF按照创建,结构,行为进行划分要更细腻,感觉更具可操作性,建议可以读下。

另外,这些作品都在某个年代(或至今)产生了里程碑式的影响,后来的作品对前面的作品至少是有感知,可能也有过思考有过实践。就好比我们从瀑布模型,V模型,原型模型,到迭代模型,螺旋模型,到敏捷模型。

而这方面的网文很多都是没有时间轴的,只是一种平行拼凑。不是说这些网文不好,比如某个算法,如果看算法导论无法理解,网文是非常好的入门手段,但是作为有专业精神的你,应该在入门后尝试去读下原著,那样所得回报是不一样的,起码可以避免某种思想因为不同网文的不同理解版本而产生不必要的歧义和争论,相信我,这在组织达成共识上有一定作用。

团队内可以就23种设计模式之外的其他模式进行充分分享,达成共识,避免23种外就是焦油坑,很多时候我们的代码是一层华丽的设计模式里堆满了烂代码。

注:此篇文章也是一篇网文!

子话题:再看MVC

MVC中V是输出,C是输入,M是这之外的所有业务逻辑,这就是Trygve提出的Smalltalk上用的经典MVC。这里最重要的论点是M不能依赖V和C,从而分离关注点,使得一个M可以有多种V的形式,而且由于M更具可测试性,所以V和C都可以是很薄的一层。至于V和C是否要分离,V和C要不要依赖M,相对来说不那么重要,一般来说V和C不分离,且是一对一关系,它们同时依赖M。

这里我额外想讨论三个点:

第一,C一开始不是中介者(我说的应该不是你心中的变种,据我了解Web MVC中C是中介者),而且C如果作为中介者(与MVP很像),那么C里将为了分离M和V,里面堆满胶水和其他复杂逻辑,这无疑分散了关注点和M的地位。
端程序员本来就重V,甚至以V为中心,他们会毫不犹豫的选择将M做成贫血模型,然后将逻辑堆积在C中。

第二,如果V和C是很薄的一层,且围绕M建设,那么你会发现这个架构有的貌似六边形架构,只不过V和C在六边形架构中处在适配器这个层次,但不打紧,庆幸的是以M为核心进行建设的思路并未改变

第三,另一个常见的思维方式是一个V和C要有一个配套的M,事实上这种已经偏离MVC,而走向POSA中所说的PAC(Presentation-Abstract-Control)架构(多年后另一个人在未读过POSA的情况下重新以HMVC的名字提出同一架构),
PAC不是坏架构模式,你可以试着去了解它,如果你正走向它,且你的业务较为复杂,不如大大方方拥抱它,而不是处在四不像的含蓄,那多少会令人惊讶。
PAC

后来MVC出了一个改进版本,即从Model中分离出Application Model。
业务的扩大和复杂意味着又要分离出一层,这个版本的MVC开始貌似Clean架构,
即分离出企业Domain级的Entity和Application级的User Case。可见以M为核心进行建设的思路并未改变

很多公司现在在搞中台化,Entity可以认为是这个层级的,它要保证多个应用可以共同复用某些业务线,而且端中台化应该重视M,而非输出带M的V,那样是以V为中心的构建。
(V可以作为基础设施层的通用组件输出)

Web MVC中存在另一个变种,就是提取出一个Front Controller,我感觉此种情况下,这个Front Controller更像是经典MVC的Controller,而这个变种中的Controller则扮演类似Application Model的工作。不过我不是很确实。

再之后Martin对MVC,MVP(分成Passive View和Supervising Controller)和Presentation Model(这可以认为是MVVM的前身)进行了总结,建议反复看下。
强烈推荐使用Passive View MVP或Presentation Model,它们都更接近Humble View,可测性更好,更重视M。很多开发者一提到MVVM就想到的是data-binding手段,但是当他们看到Android的相关框架时就失望了。要是没有data-binding手段,意味着你需要手动做这些事,将产生很多样板代码。所以他们干脆放弃了这个他们嘴里“花哨”的模式。事实上VM(View专属的Model)最大的好处是Humble View,希望你在看到这些后再有所权衡。
MVP

最后,可能你身边会有高工,基于他们自己的经验和务实的风格摸索出了一套MVC变种(他们可能强加于你,尤其是面试官,你之前的理解是错的,他的才是正宗MVC),此时你或你们完全可以给这个变种起个别名,模式的实践,关键在于组织达成共识(至于他的理解是否正宗,你可以一笑而过),请观察是以M为主要关注点?还是天天在变动C或者V?

抛开舆论和伦理道德的问题,你完全可以采用MVCPVM(Model+View+Controller+Presenter+ViewModel)来应对复杂的业务逻辑线,Controller、Presenter、ViewModel可以并存而不冲突,当然其中的含义可能评论原来的MVX模式。另一个问题是这种模式可能由于过于分层而变得死板,并且跨层可能需要多次转换数据,这是一个决策权衡问题,留给你的组织。以下的图来自于bob大叔的一次演讲:
clean_mvcpvm

子话题:分层

说到分层,最经典的是Martin提到的三层架构:表现层,领域层,数据源层。其他的分层都是这三层的细化变种
后来Evans在领域驱动设计中提到了另一种分层架构(见下图),可以看到应用层可以绕过领域层直接与基础设施交互。不过我不太认同图中意思的地方是,基础设施应该依赖领域层,而不是相反。
这两种都是我们喜闻乐见的架构。

POSA中提到了一种独特的分层,就是以继承关系完成分层,比如可能超类完成核心业务,次超类作为附加业务层,子类再叠加上表现层。这种分层方式在我们的代码中也非常常见,只不过可能没有那么多层,层的划分不那么严格,用于比较窄的范围内。
这种以继承关系完成的分层,其实是以实现继承为手段,以复用为目标达成的分层,代价常常是破坏封装和SOLID

随着业务的壮大,这种水平分层将变成按照业务线垂直划分,然后每个垂直的业务线各自再按原水平分层进行分层,慢慢的这个垂直的业务线走向独立的服务子系统(进程)。

而六边形架构,Clean架构是非传统的分层架构,他们都给了我们新的契机去更好的围绕业务逻辑为中心进行建模(见下图),一图胜千言。
值得注意的是,拿Clean架构来说(六边形架构雷同),要避免UI绕过洋葱,而直接使用DB
事实上Martin注意到了六边形架构,他指出六边形架构是对称视图,而三层架构是非对称视图。这种非对称视图是有所裨益的:表现层是系统对外提供的服务,数据源层是系统接受外部提供的服务。为别人提供服务的接口和使用别人服务的接口有较大差异,应该加以区分

此时你可能像我一样,寻求过Android领域的Clean架构框架或典范,而且至今在关注。这里是其中一个。 有些人在了解Clean架构的细节,也有很多人在寻求“形”而不是“意”,希望你能避免为了Clean架构而Clean架构。
layer_arch
hex_arch
clean_arch

子话题:架构问题!

细胞之所以存在,是因为细胞膜定义了什么在细胞内,什么在细胞外,并且确定了什么物质可以通过细胞膜

4+1

一说到架构问题,很多人眼中的印象是“4+1视图”(由于我经常用,所以以己度人)。每每演讲者都是拿出一个巨庞大和复杂的结构图来告诉你他们的架构变迁和未来展望,尤其是现在的后端架构(前端有架构?)或者公司的技术架构,比如云原生(见下图),这是涉及N多团队的庞大架构。很多时候这种“吓人”的东西往往意味着架构是架构师的事情,和自己无关,自己决定不了什么。
在这里插入图片描述

也不尽然,每个程序员都应该关注架构。尤其是一种隐形情况,就是迁移问题。
比如某个程序员写了一个卡片,里面用org.json进行了数据解析,用Fresco进行图片的加载显示,可能他还进行了父子类抽象。
而此刻由于这个卡片还是个例或者这个业务未受重视而未引起“架构师”的警觉(你知道“架构师”很忙,未必事必躬亲关注这些细节),但是后来随着越来越多的程序员照着这第一份卡片的代码风格铺开,业务线上有了非常多的卡片。
那么此时想再从Fresco切换到Glide,或者再从org.json切换到Gson,就可能面临十分沉重的改动,此时当初未受重视的细节将俨然成为一种架构问题。

也许由技术经验丰富的开发者完成第一个卡片(也就是所谓的骨架搭建),就可以避免这个问题,也许做好code review和代码走查就可以提早发现这样的问题,也许“架构师”根本不会错过这波行情。
但是备不住“铁打的营盘,流水的兵”,而且技术丰富的开发者一般都是沉醉在高难度技术细节上而不会被派往一线业务。

你读到这,可能看出我经验尚浅,仍显稚嫩!而沉重冷静的你应对这种问题已有胸有成竹的方案。
但是Bob大叔在这个问题上的发言仍可以说是掷地有声,您不妨也以空杯的心态看看:
不要嫁给框架,那是单向婚姻,框架只是技术细节,隔离这种细节,并尽可能延迟决定这种细节,让框架成为一个可选项
Android开发很容易成为固件开发者,他们太依赖系统框架(而系统框架也在经常变化删除),要成为软件开发者,开发的软件也要足够soft,要在框架和业务逻辑之间画一条边界

不仅如此,我们还要:

  • 在数据库和业务逻辑之间画一条边界
  • UI和业务逻辑之间画一条边界,UI应该成为谦卑对象(被动的,极薄的一层展现)
  • 要在不同的变更轴的工件之间画一条边界

比如你做了很多Android UI,现在要全切到Flutter UI(虽然这最终并没有发生)或者Web UI,这很明显就是架构问题!因为你需要重复做很多工作。
隐藏在原Android UI中你仍需要重复去做的那些工作(比如某些判断),它们都属于业务逻辑,应该是可测的,不应该是UI的一部分。否则可能要推倒重来,这种成本也很可能是昂贵的。

再比如如果你只是做简单的数据映射,那么即是你用了org.json或者Gson,都可以相对容易的切到Jackson,因为数据映射是自动化过程,里面没有业务逻辑。
但是现实中却不是这样,你可能将原数据值"1"或"Yes"或""映射成了false或true,而且这种为了协调多端和Server而自定义的映射关系可能五花八门,分散在各处。
那么此刻无论你切换成什么新框架,都需要小心翼翼的提取出这些细节来迁移,而且遗漏的风险会增加你的bug率。
它们本该作为业务逻辑的,却被耦合在了数据解析框架之下不能自拔。

身边不乏有人痴迷于框架组合,他们的想法是为每个环节找一个框架,在他们眼里框架就是靠谱。
比如数据这块我要用这个ORM(比如hibernate),然后我要用个MVC框架(比如Strusts2),我用一个依赖注入框架(比如Spring)。
哦,我是移动端,我要用这个图片加载框架,我要用那个网络请求框架。。

事实上,你无法保证一个更流行更理想的新框架是否在前方召唤你,你也无法预判一个正在流行的框架是否未来会停止维护,那么就不要嫁给某个框架,给自己的生活多添一份生机。
希望你能明白,我不是在否定框架的贡献,也不是阻止你去用某个框架,而且很多公司为了统一的目的而(集中维护,降低风险,特定优化)定制了某些框架并强制推广,它们如同系统框架一样重要和值得(只能)信赖,但我依旧建议只要可能就做适当隔离。

不论你所处的是分层架构,MVC架构,还是六边形架构,Clean架构,亦或者是它们的变种或简化,都要更关注业务逻辑,要让其他的层,数据库、UI、框架都成为业务逻辑的插件(通过接口抽象+依赖反转),这是我想在组织内达成的!好比Guava,虽然是有几个人写成,但是风格上却出奇的一致,他们对要遵循什么原则,什么是“优雅”等都做到了充分沟通。

上面的话,Bob大叔更确切的指出,是让低层组件成为高层组件的插件。并举了这样一个例子:

function entrypt() {
	while(true)
		writeChar(translate(readChar()))
}

上面代码的错误在于让高层组件中的函数entrypt()依赖了低层组件中的函数writeChar()和readChar()。

子话题:单元测试

关于单元测试的几个误区:

  • 老板下令要单测了,于是去找网文教程,看看怎么写单测,JUnit、Mockito、PowerMock都整齐了,学习各种奇技淫巧,最近我新写了3个类,每个方法都补上单测,把里面的依赖都mock起来,work吧,兄弟!
  • 我想测XXX功能,怎么写单测啊!嗯?不行吗,那单测有啥用?应付老板?

我们依旧回到历史,为什么要单测?

首先,顾名思义,测试是用来发现bug,单测是程序员在提早发现bug,这种自测是改良编码质量的手段。自我测试代码的重点是您拥有测试,而不是获得测试的方式。

其次,敏捷先驱Kent Beck提出了XP(极限编程),测试驱动开发(他本人写的同名书值得看上至少3遍),即TDD,然后和Erich Gamma(GoF之一,eclipse的核心开发者之一),一道开发出了JUnit框架,它是Smalltalk的移植版。换句话说,一开始我们所说的单测是TDD的产物,虽然现在可能不仅如此。而TDD是获得测试的方式,同时是获得(基于反馈)进化设计的方式

再次,《重构:改善既有代码的设计》、《修改代码的艺术》这两本书告诉我们,单测的契约保证可以用于重构,如果你已经有了单测,那么也就有了old的契约,如果没有,则去发现和设计一些临时代码能mock出这些old契约,重构后可以将new的契约和old的契约进行回归比对,保证重构前后契约的一致性,尤其是复杂的大型的重构。

最后,不论是用于设计,编码还是重构,单测都是一种信心
如果你是天才或者不知为什么却总是自信满满的人,那么单测真的就是一种应付上级的手段; 如果重构后,你感到上线代码会出现bug而内心有所惶恐时(此时你经常寄希望于QA可以测出所有的bug,但是你明白我在说什么),那么就寻找契约,保证契约即保证了功能质量,而单测或者写单测是一种契约获得形式;
如果最近你的bug率在上升,你希望即将开发的新能力bug可以有所减少,亦或者你对这个新能力的代码设计毫无头绪,那么可以尝试TDD(Bob大叔甚至将TDD作为工匠誓言,专业精神,相信我,它不会太差)。

经典单测 vs 模拟单测(SUT指被测对象)
首先讨论经典单测(或经典TDD工作者),对待协作者上,采用Sociable test,除非遇到困难(不确定性风险权衡,难以忍受的速度,平台环境,比如缓存是否命中测试),否则基本不使用mock技术,而使用真实对象。
主要使用状态验证,通过在执行方法后检查SUT及其合作者的状态来确定所执行的方法是否正确工作。这意味着经典测试仅关心最终状态,而不是最终状态的得出方式

  • 真实对象意味着你需要在setup阶段进行复杂的环境准备,构建配置,这是工作量且也可能会降低性能;
  • 真实对象是需要维护的生产代码,它们作为协作者,其实现改动可能波及SUT,某个协作者的测试失败甚至导致整个系统中失败的测试泛滥。为了找到错误的根源,可能需要大量调试,不过他们觉得通过查看哪些测试失败可以很容易地找出罪魁祸首,主要是控制好测试的粒度
  • SUT可能需要提供额外的用于“验证状态”的方法;

假设您需要开发一个功能,首先确定要使该功能正常工作所需的域,可以让域对象执行所需的操作,然后逐层推开,直至最后考虑放上UI。这样做您可能永远不需要伪造任何东西。许多人喜欢这样,因为它将注意力优先集中在域模型上,有助于防止域逻辑泄漏到UI中,这是经典单测的魅力。

其次讨论模拟单测(或模拟TDD工作者,或BDD,即行为驱动开发),对待协作者上,采用Solitary Test,将始终使用mock技术而非真实对象,始终聚焦关注点在SUT并避免干扰。
模拟对象始终关注行为验证(可以有状态验证),更关注行为的方式而不是结果。

  • 需要每次测试时都创建mock。虽然有些时候可以在setup阶段mock,但是更符合他们理念的做法是,在每个test上创建可能的mock,以避免干扰,这也是工作量;
  • 由于有效的隔离(模拟掉主要对象之外的所有对象),测试失败大部分来自于SUT本身,不会波及其他;
  • 不像经典单测可以作为微型集成测试,模拟单测可能会掩盖某些真实协作中才能出现的bug;
  • 与SUT的实现耦合严重,这也会干扰重构,实现更改更容易破坏测试;

最后的话是,不是老板让你单测,你就下手的。你需要权衡是否采用经典单测,它的好处,什么时候可以这样干,并坚持原则直到见到收益或放弃。因为计算机并不是科学,两派缠斗这么多年,没有那么多非黑即白。

不要强制执行TDD(我说的不是单测)
因为渐入佳境是需要时间的,在这之前会因为开发模式的调整而导致效率下降。
Kent说过,比较好的方式是,一些TDD的“粉丝”使用TDD,并因此得到了收益且保证了开发效率,自然会吸引其他人进来,强制会招来不必要的反感而让你失去必要的思考。
对于那些没有单测保障的大型遗留项目,中途执行TDD会更加困难。
后补单测其实只是在补契约,而不会对bug率有任何降低(除非后面有人重构此处代码,那真是前人种树后人乘凉)。

TDD vs DbC
关于契约,Jim Coplien和Bob大叔有一场TDD争论(设计派和工程派的争论?)。
可以看这里
Jim觉得TDD不是从设计出发的,有碍于自然设计,并推荐Design by Contract(DbC)可以达到更好的效果。
而Bob的意思是,好的设计是需要富有经验、较高才能和预见的人才能实现好的,但是一个平庸的开发者可以借助TDD达到一个尚可的设计水平,而且TDD也有利于做好解耦。事实上TDD对Model-Driven-Design也是很有帮助的

Is TTD Dead?
另一场讨论来自于Martin、Kent、和DHH(David Heinemeier Hansson,Ruby on Rails创始人)之间。
可以看这里
我从这场辩论里面看到了以下几点:

  • 单测带来的负担,一旦你有了单测,你既需要维护生产代码,而且需要同步维护测试代码,这样的结果是测试代码时不时还会阻碍你修改生成代码。
    Bob大叔称之为脆弱的测试,想想为了修改一行生产代码需要修改几十处测试代码时,开发部门会怎么说吧。
    如何尽可能减少脆弱的测试,一个是不必要去追求100%的测试覆盖率和对单测速度的100%追求,测试的充分性比覆盖范围所能回答的要复杂得多;一个是尽可能面向契约,减少不必要mock,不要面向实现细节进行单测
  • Kent基本上不使用Mock技术(经典派),并提醒我们,不是TDD对设计有伤害,而是不良的设计决策导致。
  • Martin说TDD有适用的场景和不适用的场景,很多人犯的问题是没有足够的重构(TDD=Loop of Red-Green-Refactor)和过度测试,TDD仍然是经验尚浅的设计者的好起点。
  • 我们知道单测将引导你做好解耦,隔离,分层,这可能对设计是有利的,但是如David说,这种隔离可能会增加复杂性,当然, 其实David最终的意思是想避免TDD无脑变成政治正确。
  • 在TDD中,测试代码很重要,因为(有人相信)可以基于测试代码重新还原生产代码,这使得出现类似敏捷之前“文档胜于代码”的论调:“测试代码胜过生产代码”。

从契约维护上来说,单测维护契约的缺点是契约在测试代码处,而不是在生产代码处。这无疑分散了关注点。
从这个角度看,DbC也是极好的,无疑是Eiffel语言的一大特色。java本身并不支持契约,断言的话则约束较弱且会混乱代码。一般可以接入类似iContract这种框架(谨慎引入)来满足DbC for Java。

最后,不论是DbC和TDD,都欢迎你勇于拥抱,或者作为你从这里走的更远的起点。另外DbC和TDD,都与防御式编程无关,防御式编程是关乎容错鲁棒性的。

子话题:贫血模型?

关于业务逻辑的组织,Martin在他的著作《企业应用架构模式》中提到了事务脚本,领域模型,表模块。目前大量的项目仍在采用事务脚本。

典型的模式是弄一些java bean(Android中更甚,基本上是裸结构体,退化类,全部public的字段,极少的方法)作为数据容器,在既定触发(比如用户打开操作)的控制流下,一条事务脚本一层层传递这些bean,在每一环节或层都留下点业务逻辑。
这像极了c语言的面向过程。
此时java bean即是贫血模型。如果业务不复杂,那么这种方式也还好,但是应对复杂业务,我们提倡使用领域模型。

DDD建议我们真正面向对象,提倡充血模型(并尽量避免领域对象过于臃肿)。一个行为如果是这个对象的,那就放在这个对象里,并做到足够封装。而不是用data加上一个Utils类的静态方法来完成。
模型驱动要求我们做好分层,我们建设Entity、Value Object、Event、Service、Factory、Repository、Aggregate、Assertion等等。我们划分组件(元素),并关注边界和通信(关系)。
而我们曾经在代码评审时揪住魔数,硬编码字符串,有无文档不放,却对重点有没有放在建模上视而不见,仅仅因为那些不容易发现?提交者说那样会影响性能时,评审人自己心理也无法确定吧?

后来从贫血充血模型之争演变为结构派和算法派的争鸣
算法派提出,一个完整的业务(算法)被多个交互完成任务的多个对象分割了,我们无法看到全貌,这增加了维护的复杂度。
另一方面,例如银行转账这个例子,转账的逻辑(不属于账户)按照充血模型该放在哪?

由Jim Coplien和Trygve Reenskaug提出的Lean架构DCI架构,使我们关注到另一个可行的设计。简单来说,DCI指出,我们根本没有面向对象编程,而是面向类编程,一个账户类里的属性可能分拨用在不同的用例场景上(不同的实例需要使用不同的属性),但是它们却都静态的堆满在账户类里,这无疑混乱了我们的心智模型。
所以DCI提出,原来的领域模型Entity,由贫血的Data和Role组成,它们在源码上是分离并在运行时mixin结合,结合的依据是某个场景Context。

这种技术在Objective-C、Swift上可以使用Category、Extension来实现,在Java中则要基于AOP来完成。
而此时业务设计(算法)不再是割裂的,呈现为Interaction(RoleMethod)。我们的关注点也与用例契合上了,转账的逻辑将由账户所扮演的源账户Role和目标账户Role在转账这个Context下发生。
DCI将分离what the system is和what the system does

DCI_mvc

“让我们逐步了解控制在应用程序中的流动方式。 用户按下发送与用例相关的请求的按钮。 应用程序接收该请求,实例化适当的上下文,然后将请求传递给它。 上下文会检索实现用例目标所需的所有数据对象,并为其分配适当的角色。 然后,它将消息发送到对象角色,该角色开始了对象之间的一系列交互。 如果需要与用户进行交流,则也可以由角色来完成。 完成后,应实现用户的目标。”

Peter Coad曾提出了和DCI很像的对象建模方式,中文常被称为四色原型。所谓的四色即指由MI(业务活动,类似Context),Role,Desc(类固有属性),和PPT(实例获得性属性)组成的对象模型。

至此我们得到了DDD和DCI/四色原型两个不同方向上的领域建模形式,而这两种都是经过验证的。
剩下的又是同一个问题,组织根据业务形态充分沟通并达成一致。这里有个常见的问题,就是过度设计,我们的业务用事务脚本就可以,那就很好啊。但是我们仍需要关注架构(走向),这是一个高效组织每个成员都应该关心的事,即便“架构师”能预见未来(业务的复杂性)。

显然,无论是DDD还是DCI/四色原型都可能框住某些程序员喷涌的“和稀泥”冲动,但是很明显,软件工程需要约束组织以长治久安

我很喜欢Bob大叔对三种编程范式的总结:无论是面向过程,还是面向对象,面向函数,这三种编程范式70年代就有了,之后再无新的编程范式,它们都是告诉你什么不能做(面向过程限制使用goto,面向对象限制使用函数指针,面向函数限制赋值,我不赞同这三种范式只包括这些限制,但是这些限制是确实的),而不是给了你更多的特权,都是限制你。

子话题:原则

我这里没有指定是什么原则,就是想聊的再松散一些。好吧,说实话,关于原则,我可能没有比较长的独立话题了,那么在这里拼凑一下了。
我的职业生涯中,遇到太多违反SOLID,面向接口抽象,封装,组合优于继承等这些原则,却不以为然的开发者。无论是公开字段访问,实现继承(相对应的是接口继承),总能让他们找到性能理由作为避风港。
所以我尽量避开这些老生常谈的问题,聊聊其他的小原则。

合并重复的原则
非常多的程序员非常喜欢使用“容器”的概念。一旦有几个不同的用例需求需要用到某几个共同的flag,消除重复的冲动,会使他们毫不犹豫的建一个类作为这几个flag的容器并到处传递。他们的眼中没有对象的概念,一切数据都只是个容器。这是典型的数据处理风格编程。

Bob大叔举了另一个面向对象的例子,当然Bob大叔是以违法SRP(单一职责原则)的例子来讲的,即Employee有三个函数calculatePay(),reportHours(),save()。很显然这三个函数分别面向财务部门,人力资源部门,技术运维部门,它们不应该放在一块,但是很多的程序员恰恰是因为这三个函数都可以归给Employee而故意重新放在一起。

这种假同源经常发生,有的看着是重复,但是慢慢可能会演化的很不一样,如果强行将它们绑在一起,会导致频繁变更(违反开闭原则),if-else会越来越多,代码会越来越复杂。而另一些情况,它们看起来说的不是一个事,但是这只是假象,需要做好分辨。
用例,以及其他能代表不同的变更理由和速率的手段,可能是判断重复代码是否合并的一个可行的办法

依赖倒转原则
我第一次看到这个SOLID的D原则时,仅仅认为这是一个强调抽象,强调面向接口的原则。
但是这里蕴含另一个道理:如果你在争论是广告依赖Feed还是Feed依赖广告,或许还有第三个选择,那就是广告和Feed都依赖于抽象接口,这样也就意味着推平了依赖关系,全部指向了抽象。

第二个曾经困惑的一个地方是我们应该做好底层组件(也就是机制),使用方拿着我们的底层组件,怎么搭这个“积木”,是使用方的事,或者说策略(配置)的事,此处策略理当依赖底层组件。
这意味着我们是从下而上做设计,这意味着策略层堆积了复杂的代码,但是我们认为那不重要,因为那不可复用。

但是依赖倒转事实上告诉我们,“积木”固然重要,但是积木是可替换的(策略包括规则,约束,配置和其他业务逻辑,有些部分也是易被替换的,需求常是如此,但是我们对用户的价值就是通过策略体现),积木应该是插件,应该是底层组件依赖抽象策略组件!这种思维方式的转变是有现实意义的。

最后一个是,此原则和IoC和DI(依赖注入)没有太多的关联,不应该混淆。

告诉而非询问原则,分离命令和查询原则
a. 客户端对象不应该首先询问服务对象,然后根据询问结果调用服务对象中的方法,而是应该通过调用服务对象的公开接口的方式“告诉”服务对象所要执行的操作。

b. CQRS告诉我们,操作可以分为命令和查询,要尽全力减少对命令和查询的混叠,由函数或函数链承载查询,而命令具有副作用,要把命令隔离到简单的操作中或独立的通道。

a中,如果询问,那么问题在于,您正在实现的逻辑可能是被调用对象的责任,而不是您的责任。对于您来说,在对象之外进行决策会违反其封装。这意味着该对象泄漏了内部状态。

b中,我们经常想做一个能力饱满的操作以增加复用性,简化到函数问题,一个getXXX的查询函数,里面可能在做init命令操作。你只是希望getXXX的调用方更方便,但是这种混叠大多数时候是弊大于利。

a和b都是让我们避免干扰,聚焦达意的封装指南。如果为了使用一个组件而必须去研究它的实现并推测其用途,如果理解程序的唯一方式就是沿着分支路径来跟踪程序的执行,抽象和封装就完全失去了价值。

慎用动态特征原则
这次只想讨论函数参数问题,我们经常把参数设计成Object或者Map。
之所以设计成Map,是因为在函数落地的那刻还没想好,这样后面扩充需求时可以保留函数的签名兼容。
另一种情况是当发现使用泛型约束类型时,发现存在泛型“传染”(即为了A是泛型,需要保证使用A的B也得是泛型,多来自于重构),那么就转而使用Object;或者不想编译依赖另一个类型,所以就用Object。

关于Object:
使用Object参数,即意味着要在方法内部进行强转使用,也就是其灵活性并没有宣传的那样好。违反里式替换原则,因为不是传递任何对象都可以工作,而知道哪些类型可以工作的唯一办法是查看方法细节。
不仅如此,缺失接口契约,难以理解的方法签名,放弃了使用静态类型系统的所有优点,可能导致大量的运行时错误。(编译器和IDE将保持沉默,实际上是在说“我知道如何使用它,但是我不想让编译器知道”)

关于Map:
使用Map参数,即意味着你使用了一个不用预先定义的类,但是里面的所有属性都是public的(因为你可以put、get、remove),对真正的业务行为毫无封装,这就是一个贫血模型退化类!
你之所以这么做,可能出于两个原因,你不想定义一个实际的类,你怕这些类越来越多难以维护,另一个是你怕发布出去后如果有新参数,可以不去改类和函数签名。

我不想从Map的性能开销出发,而是尽可能聚焦right。

  • 你需要去维护一堆字符串或常量,而且你看到的这些常量未必枚举了所有可能的key,你的工作量转移但并没有减少;
  • 在一个静态语言中,你的动态类型梦并不完美,比如你需要增加一个key,可能不需要改函数的签名,但是函数仍需要修改才能识别这个key,而调用端一般都需要感知到这个key,并在调用端留存这个契约(不论是常量还是硬编码字符串)。
    但是风险问题来了,因为你并没有任何一个检查手段可以检测出这种情况,除了手动排查和运行时报错祈祷。

其实这两种不过是设计偷懒的表现。解决办法就是开始你的设计!

封装重于复用原则
封装属于可维护性和可改变性,复用属于可复用性和可扩展性。

如果我们离开封装,那么:
随着代码的变大,添加下一个功能所需的时间越来越长。
随着代码的变大,在不破坏以前有用的功能的情况下添加功能变得越来越难。
为什么?
因为您需要记住所有变量都在何处被修改。而且,您需要记住可以按确切的顺序调用哪个函数,如果以不同的顺序调用它们,则可能会出错,因为某些状态变量尚未完全有效。

如果看到该数据的代码越少,则添加下一个功能时,您破坏某些东西的机会就越小。这是封装概念的主要目的。

但是如上所言,封装的好处是隐性的,封装的不好,别人在真正修改代码时才会骂你,但是封装的好,别人也不会夸你,反而会嫌你约束了他。
而复用则不同,当别人需要完成工作时,你的组件恰好能派上用场,你可以对他宣称你写的组件的可复用性和通用性,这种回报是显性的,这是你的贡献和勋章,如果派不上用场,你可以眯着,别人也不会找你的麻烦。
这导致很多人更看重复用性,为此你会看到:

  • 为了增加通用性和可定制可配置能力,将内部状态公开,提供繁多的setter;(不,公开服务而不是状态,最小化API,那是你的承诺)
  • 一个不怎么高明设计的类声明为基类,将内部属性弄成public或protected,提供一系列模板方法进而提供“实现继承”的入口;(不,类继承层次结构不能很好地替代代码重用机制的真正含义)

面向对象的重点是重用–事实并非如此,封装才是重中之重。
重用意味着依赖,这可能导致在编码之外的其他环节会浪费时间,使问题棘手,风险增加。
如果可能,不复用非通用的工件。

子话题:康威定律

亚马逊CEO说过:“如果两个披萨对于一个团队来说不够,那么这个团队就太大了。”

我是从人月神话中第一次知道康威定律:系统架构设计受限于(或等同于)企业内的沟通结构
进一步的,人们会倾向创建独立的子系统以减少沟通成本,这也是微服务为什么是关于组织而不是技术。
事实上在很小的范围内,它也在起作用,比如小到一个人修改另一个人的类文件,他也会有意识的在代码上和原作者保持距离。显然,这也意味着一个类拥有多种风格,也意味着再多的时间也无法让系统完美至极。
所以,组织达成共识才好实践某种技术方案。我们可以不是从整个大团队出发去统一下推,而是让沟通结构(口碑)在这里起作用,由下而上。
推行一种架构,需要密切关注组织内的利益相关者,否则你得到的只是口头承诺;相辅地,根据组织的沟通网络,素质水平等,来决策落地哪种架构,让它去演化。
架构虽然需要布道师和设计师,但落地的过程,实际也是一种社交活动,而技术决策往往也是政治决策,莫变成空中楼阁。

总结

组织想要高效,想做好right,就需要充分沟通,对重要原则和决策达成共识。为什么共识这么重要?
C++和Java有很多不同,至少有一点我们需要看到,本来C++就是极其复杂的语言,C++委员会“粗糙”的规定,使得C++野蛮生长,至今不同的编译器和平台,不同的STL导致复杂性变得更高。而Java则一开始就有详尽的规定(比如int的长度,单根系统)和统一的平台无关的java标准库(简直是世上向下兼容最好的语言之一)。

类似地,共识会让开发者们避免五花八门的设计,这十分有利于开发维护。
如果靠组织成员的sense自发,或者碍于面子和职阶,是有可能损伤组织的,
况且每个高工的要求都不一样,底层人民将很辛苦。

但是共识不是写进章程一成不变,要为好的软件架构而持续斗争,以更小的人力成本来开发维护
关于斗争,我们提到了学院理想派和工程实践派之争,组件派和决策派之争,设计派和演化派之争,结构派和算法派之争。他们就像左派右派之争一样,没有绝对的正确,它们可能需要结合,可能需要基于具体背景下中庸平衡,它们的回报也都不是免费的。
幸运的是,权衡是自由的,由你的团队共识。好的组织不应该是等级森严的,要允许绕开人情去沟通,每个成员都可以去捍卫好的原则。

我们这里推荐回归面向对象编程和建模的本真,寻求遵守设计原则,关注架构,组件,边界,依赖关系。
还讨论了这么几件事情,但是都并未给结论:
你的架构可以使用分层架构,MVCPVM架构(见上面,最要紧的是谦卑UI-被动View),六角形架构,Clean架构,并着重关注领域模型建设,这是应对复杂性和易变性的开始;
你可以使用TDD或者DbC来开展设计;
你可以使用DDD或者DCI或者四色原型来组织领域模型;
你的组织结构决定了你的架构设计,这是前提;

术语和技术细节(待续)

参考与引用

a. GUI架构:
https://martinfowler.com/eaaDev/uiArchs.html
https://martinfowler.com/eaaDev/PassiveScreen.html
https://martinfowler.com/eaaDev/SupervisingPresenter.html
https://martinfowler.com/eaaDev/PresentationModel.html
http://aspiringcraftsman.com/2007/08/25/interactive-application-architecture/
https://www.cnblogs.com/rubylouvre/archive/2012/11/19/2777240.html
b. 通体架构:
概览:
https://www.jianshu.com/p/c33de5e23fb4
clean-arch:
https://www.jdon.com/50899
DCI:
https://www.artima.com/articles/dci_vision.html
c. 其他:
https://github.com/android10/Android-CleanArchitecture/issues
https://www.infoq.cn/article/every-architect-should-study-conway-law
http://udidahan.com/2009/06/07/the-fallacy-of-reuse

附录

特别对Bob大叔说的两段文字有所感触,特摘录如下以共勉:

一、华丽新设计

最后,开发团队造反了,他们告诉管理层,再也无法在这令人生厌的代码基础上做开发。他们要求做全新的设计。管理层不愿意投入资源完全重启炉灶。但他们也不能否认生产力低的可怕。他们只好同意开发者的要求,授权去做一套看上去很美的华丽新设计。

于是就组建了一支新军。谁都想加入这个团队,因为它是张白纸。他们可以重新来过,搞出点真正漂亮的东西来。但只有最优秀、最聪明的家伙被选中。其余人等则继续维护现有系统。

现在有两支队伍在竞赛了。新团队必须搭建一套新系统,要能实现旧系统的所有功能。另外,还得跟上对旧系统的持续改动。在新系统功能足以抗衡旧系统之前,管理层不会替换掉旧系统。

竞赛可能会持续极长时间。我就见过延续了十年之久的。到了完成的时候,新团队的老成员早已不知去向,而现有成员则要求重新设计一套新系统,因为这套系统太烂了。

。。。

二、大潮所感染

后来公司内有一位硬件工程师被关系型数据库大潮所感染:他坚信我们的软件
系统在技术上有必要采用关系型数据库。他背着我召集了公司的管理层开会, 在臼
板上画了一间用几根杆子支撑的房子,问道: “谁会把房子建在几根杆子搭起来的
地基上?”这背后的逻辑是:通过关系型数据库将数据存储于文件系统中,在某种
程度上要比我们自己存储这些文件更可靠。

我当然没有放弃, 一直不停地和他还有市场部斗争到底。我誓死捍卫了自己的
工程原则,不停地开会、斗争。

最终,这位硬件工程师被提拔为软件开发经理, 最终, 系统中也加入了一个关
系型数据库。最终,我不得不承认,他们是对的,而我是错的。

但这里说的不是软件工程问题:在这个问题上我仍然坚持自己没有错,在系统
的核心架构中的确不应该引入关系型数据库。这里说我错了的原因,是因为我们的
客户希望该系统中能有一个关系型数据库。他们其实也不知道为什么需要,因为他
们自己是没有任何机会使用这个关系型数据库的。但这不是重点, 问题的重点是我
们的客户需要一个关系型数据库。它己经成为当时所有软件购买合同中的一个必选
项。这背后毫无工程逻辑一一是不理智的。但尽管它是不理智的、外行的、毫无根
基的需求,但却是真实存在的。
这种需求是从哪里来的?其实是来自于当时数据库厂商非常有效的市场推广。
他们说服了企业高管,他们的“ 数据资产”需要某种保护,数据库则提供了非常便
捷的保护能力。
直到今天我们也能看到这种市场宣传, 譬如“企业级”“面向服务的架构”这
样的措辞大部分都是市场宣传嗦头,而和实际的工程质量无关。
。。。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值