如何通过设计模式优化开发

1、设计模式总结

以下举例23种国际通用设计模式和若干种「杂交」奇门异术,描述语言均使用个人曾经参与过的项目经历,不特指某种编程语言,方便个人的理解与二次运用。注意,有些模式之间很类似,甚至作用重叠,运用方式可大可小,这是由于模式并不是完全隔离和独立的,有的模式内部其实用到了其他模式的技术,但是又有自己的创新点,如果一味地认为每个模式都是独一无二,与其他模式完全区别的,这是一种误区;这也是为何本章后段提到了若干种相对个性化的设计模式——那些都是个人和朋友们在实际开发和需求中总结出的方法,它们可以解决特定需求里的特定问题,由于普适性没有前23种那么强,所以列出仅供了解。

公司项目参考:设计模式 及其 在 javascript & react 中的应用

同学开发经验:有关代码设计的一些思考

理论研究相关:《设计模式》读书记录分享-蔡智明

1.1、结构型模式

1.1.1、外观模式

官方定义

外观模式又叫做门面模式。在面向对象程序设计中,解耦是一种推崇的理念。但事实上由于某些系统中过于复杂,从而增加了客户端与子系统之间的耦合度。例如:在家观看多媒体影院时,更希望按下一个按钮就能实现影碟机,电视,音响的协同工作,而不是说每个机器都要操作一遍。这种情况下可以采用外观模式,即引入一个类对子系统进行包装,让客户端与其进行交互。外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。外观模式又称为门面模式,它是一种对象结构型模式。

相关项目

基于Python的短视频爬虫和长视频分发;项目需求是输入短视频网址序列和长视频分发平台账号密码,输出是最终在长视频平台上看到整合好的视频;那么我们不可能等程序运行到某个阶段就输入某个值,而是预先把全部信息输入进去,甚至不止输入一组数据,可能是好几组,然后可以在相应平台看到多个视频;而生成的短视频又有排序、使用哪种模板的问题,所以外观模式可以帮助我们解决这个问题,那就是定义好入参,剩下就交给程序,而不是多次输入。

1.1.2、修饰器模式

官方定义

该模式虽名为修饰器,但这并不意味着它应该只用于让产品看起来更漂亮。修饰器模式通常用于扩展一个对象的功能。这类扩展的实际例子有,给枪加一个消音器、使用不同的照相机镜头。

相关项目

基于Python的短视频爬虫和长视频分发;在整合长视频的时候,需要给视频加背景、二维码、说明文字、进度条……其中部分组件还需要多次叠加,由于这些组件中没有相同的属性,所以无需定义抽象方法或接口,这种情况下可以用修饰器模式完成,一个套一个,一层层完成之后向上呈递。

1.1.3、享元模式

官方定义

运用共享技术有效地支持大量细粒度的对象。程序中使用了大量的对象,如果删除对象的外部状态,可以用相对较少的共享对象取代很多组对象,就可以考虑使用享元模式。它和桥接模式最大的区别在于,享元面向类编程,桥接面向抽象类或接口编程(个人理解)。

相关项目

基于Python的Jira通知卡片;其中鉴权模块需要在发送卡片、调取个人信息、获取工单信息等多处使用,就可以独立出来做成一个模块来使用。

举例:高并发带来的问题,每一次调用接口都需要token,无形之中加重了服务器的负担:

修改:定义全局变量,每一个工作流仅调用一次:

1.1.4MVC模式

官方定义

用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑。

相关项目

Jira后端、ITIS后端、个人新媒体矩阵后端等基于Springboot或Golang开发的项目,成体系的服务生态可以考虑这种集成性和对口性极强的设计模式,弊端在于比较「重」,不适合大规模的改动。

1.1.5、代理模式

官方定义

为其他对象提供一种代理以控制对这个对象的访问,在某些情况下一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介的作用,比如你去杂货店买一个插座,而不是去生产插座的工厂去买。再比如,你去访问某个网站,你并没有访问权限,但你可以通过代理去访问这个网站,然后代理再把内容传给你;还有一种情况是代理会帮你执行一些附加操作,比如要做韭菜炒鸡蛋,你去买韭菜时老板送了个鸡蛋给你。

相关项目

后台管理前端的登陆、校园网解除限速网络跳板、后端分发消息队列等;具体应用场景有:远程代理,也就是为一个对象在不同的地址空间提供局部代表。这样可以隐藏一个对象存在于不同地址空间的事实;虚拟代理,是根据需要创建开销大的对象。通过它来存放实例化需要很长时间的真是对象。用于惰性求值,将一个大计算量对象的创建延迟到真正需要的时候进行。例如html中,图片需要load很久,所以通过虚拟代理来代替真实的图片;安全代理,或叫保护/防护代理:控制对敏感对象的访问。用来控制真实对象访问时的权限;智能(引用)代理:在对象被访问时执行额外的动作。此类代理的例子包括引用计数和线程安全检查。是指当调用真实的对象时,代理处理另外一些事。

1.1.6、适配器模式

官方定义

所谓适配器模式是指是一种接口适配技术,它可通过某个类来使用另一个接口与之不兼容的类,运用此模式,两个类的接口都无需改动。适配器模式主要应用于希望复用一些现存的类,但是接口又与复用环境要求不一致的情况,比如在需要对早期代码复用一些功能等应用上很有实际价值。

相关项目

数据综控后端;部分repository中的查询方法查出来的条目符合A控制类的返回需求,但不符合B控制类,此时需要定义一个新的服务类来转换该接口类返回的数据,然后向B控制类呈递。

1.1.7、桥接模式

官方定义

将抽象部分与它的实现部分分离,使它们都可以独立地变化。

相关项目

前后分离的项目,一个后端可以给N个前端用,一个前端可能又对接N个后端的N*M个接口,将前后端各自抽象出api端口来对接;而什么是「前」,什么是「后」,这是需要将需求抽象出来定义好的。再举一个JDBC的例子,我们在Springboot项目开发中只用写JQL语句就行,剩下的交给驱动器本身来实现,无论最终用的是Oracle、Mysql、MangoDB都不碍事。还有一个比较经典的例子,就是若依框架对仓库接口的舍弃——毕竟绝大部分业务都是对表的增删查改,那么直接把这些查询方法写到一个xml里就完事了,写完之后其他业务都能够使用,就不用重复写了,大大降低了项目的耦合性。

1.1.8、组合模式

官方定义

用于把一组相似的对象当作一个单一的对象,依据树形结构来组合对象,用来表示部分以及整体层次。和修饰器模式不同,组合模式要求子组件和父组件都能直接保持通信。

相关项目

Vue.js前端的组件开发模式、微信小程序定制组件等;这类框架依附于模板注册的运行方式,所以尤其重视组合设计模式。令我印象最深刻的一个例子是,当时开发小程序时有一个这样的功能,当用户完成了鉴权,点击那个按钮就可以投票;如果用户没鉴权,点击那个按钮就要跳转去鉴权的页面。如果按照修饰器的方式来开发,是基本无法做到一个按钮导向两个不同的结果的(除非设计两个按钮,不同情况下加载不同的),但组合模式通过父子组件的传值,设置监听和(回调)钩子的方式,就轻易解决了这个问题。

1.2、创建型模式

1.2.1、工厂方法模式

官方定义

工厂方法模式总共有三类,分别为简单工厂模式、工厂方法模式、抽象工厂模式;他们有一个核心原则:尽可能对代码作「增」操作,而非「删、改」操作。基于这个原则,我们要实现「传参后实例化对象」,使得整个系统处于较于动态和灵活的状态下工作。

相关项目

vue.js前端设计时动态生成的菜单栏;在实际开发中,某个用户打开vue模板引擎的网页后,引擎会根据用户信息(例如权限、等级)自动生成一个对应的路由,并且由路由卫士保证用户不会越界。既然路由是自动生成的,那么前端菜单栏也理应做到这一点,而非写死。所以最终采用的方案是,获取用户信息后,动态生成路由,然后动态实例化对应的菜单选项。相当于以后如果要加功能,直接写页面+添加路由即可,无需改变菜单栏的相关组件。

1.2.2、建造者模式

官方定义

将一个复杂对象的构建与它的表示进行分离,使得同样的构建过程可以创建不同的表示。

相关项目

后端MVC架构;针对某项业务(某个表)的增删查改完成后,如果想加入一些特殊的查找方式(比如查文章前50个字),也可以基于DTO→服务层→控制层的形式加入这种需求,而不必改变其他方法的实现模式。

1.2.3、原型模式

官方定义

用原型实例指定创建对象的种类,并通过复制这些原型创建新的对象。

相关项目

不用想了,看官方定义绝对无法理解。举个例子,在AI深度学习领域,初始化TensorFlow模块是极度耗时耗力的,在普通的IDE下运行时,每运行一次就要调一次库,然后就等很久……所以一般研究人员用Anaconda来替代普通编译器,一次调用多次使用,符合原型模式的设计思路。再举个栗子,同和君在剪视频的时候需用到素材库的A、B段素材,而他一次性要剪十条片子,可能在剪第二三条片子的时候会用到B、C、D段素材,难道每剪一个视频就要从NAS上拷贝对应的素材,剪完之后删除了再拷?所以一般情况下我们会把十条片子所需要的全部素材都拉到本地,剪完再统一删除。

1.2.4、单例模式

官方定义

该模式的主要目的是,确保某一个类只有一个实例存在。当你希望在整个系统中,某个类只能出现一个实例时,单例对象就能派上用场。

相关项目

小程序投票统计方法;只要是和全局「计数」、「统计」相关的方法,都可以看做是单例模式;也这样理解,如果某个类存在多个实例,那么每个实例的变化就无法被观测了。还是上述拷贝的例子,如果同和君对某个视频片段作了修改,那么这个修改只会存在于他的电脑本地而非NAS。也正是如此,原型与单例在同一个功能中只能二者取一。

关于原型&单例的一个最好的例子:Spring单例的Controller怎么保证并发的安全?

1.3、行为型模式

1.3.1、解释器模式

官方定义

将一系列指令转化成代码,能够执行的代码。

相关项目

基于Arduino的贪吃蛇;单片机读取到手柄传来的「上、下、左、右」控制信号后,通过程序对贪吃蛇的状态进行即时变更;输入的是方向,输出的是画面里小蛇的运动,其中的核心设计模式就是解释器。一般而言这种模式用于解释一个转换整体,把类似的小需求做成一个小函数封装好后扔在工具箱(utils)里,需要的时候调用就完了。

1.3.2、模板方法模式

官方定义

定义一个操作中的算法的框架,而将一些步骤延迟到子类中,使得子类可以不改变一个算法的结构即可重新定义该算法的某些特定的步骤。子类实现的具体方法叫作基本方法,实现对基本方法高度的框架方法,叫作模板方法。

相关项目

其实我们每次写Python脚本时最初的步骤就运用了这个方法,比如import一个什么类,from什么类import一个什么方法……就是去别人封装好的项目里调用模板方法。Vue.js里使用状态管理库(store)也是一个道理,比如在其中创建cookie_cache对象就是和上述单例or原型模式混用了的具体案例。

1.3.3、责任链模式

官方定义

顾名思义,责任链模式是一条链,链上有多个节点,每个节点都有各自的责任。当有输入时,第一个责任节点看自己能否处理该输入,如果可以就处理。如果不能就交由下一个责任节点处理。依次类推,直到最后一个责任节点。

相关项目

恋爱冒险类游戏文本资源提取脚本;这类游戏的开发公司为了文本资源不外泄,加密方式写得稀奇古怪,可能A段落用UTF-16加密,B段就被特殊字符串隔离开,然后用cp936来加密了……而这些文本存在的地方又是随机的,这导致我们不能用单一的解码方式处理这些文本,所以我们只能通过先判断UTF-16解码模块能否处理,若不能传给cp936解码模块处理的方式来完成文本资源提取;上述过程就是责任链模式。

1.3.4、命令模式

官方定义

将请求封装为一个对象,将其作为命令发起者和接收者的中介,而抽象出来的命令对象又使得能够对一系列请求进行操作

相关项目

动态拼装前端所需json;有些需求需要查多个表,并对数据做多重处理才能发送给前端,而前端只调用一个接口一次,所以需要在Controller层定义多个方法来调用好几个Service层的类,那么这个控制类就叫调用者,下边的服务层和DAO就是接收者,以上设计模式就是命令模式。

1.3.5、迭代器模式

官方定义

为复杂的聚合性数据结构提供某种遍历方法,个人理解为把数据的基本操作集成进函数,使得某个方法能实现更大范围的灵活操作。

相关项目

实际上这类设计模式已经被很多数据库封装好了,比如MySQL、Microsoft SQL Server等,每次通过语句查询时实际上就是在使用迭代器;其中我们为了优化各种查询,通常会建立多个索引,方便系统内部建立哈希表实现更快的迭代;一般情况下不用于正式开发。

1.3.6、中介者模式

官方定义

用一个中介对象来封装一系列的对象交互,中介者使得各对象不需要显式的相互引用,从而使其松耦合,而且可以独立地改变他们之间的交互。

相关项目

Web项目开发时总会遇到这样的问题:云服务器上的版本是正式生产版,而在本地开发调试的是测试版,两者的配置各不相同;如何快速的把本地更新的内容同步到云上呢?这里就需要引入一个中介者(ConfigController)了,它会根据系统引用不同的配置(获取系统信息→配置相应参数),从而帮助我们快速部署。

1.3.7、备忘录模式

官方定义

在不破坏封闭的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。

相关项目

在开发基于Unity的坦克大战游戏时,若用户点了暂停OR存档,那么这个时候所有对象的基本信息都会被储存到一个存档表中,待重新读取时从表中取出相关条目赋值给对象并渲染;开发爬虫时也会使用这个设计模式,把爬过的条目的特征码写入静态文件,万一程序意外中断时还可以通过该文件恢复爬虫进度,无需重头爬起(尤其是针对一些反爬网站,往往不能顺序遍历,而是需要shuffle之后再爬,这种情况下就必须使用文本or数据库记录爬过的条目)。

1.3.8、观察者模式

官方定义

指多个对象间存在一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。这种模式有时又称作「发布-订阅模式」、「模型-视图模式」,它是对象行为型模式。

相关项目

Vue.js模板页面里的双向绑定;在文本框修改文字,与其绑定的网页区(一到N个)也会发生相应的改变;以及小程序内的按钮监听,多个组件并行监听一个操作(切换页卡),若被触发,相应功能(向后端发请求、根据本地资源和模板渲染新页面等)会并行开始执行。

1.3.9、状态模式

官方定义

用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题,将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化。

相关项目

动态规划算法,比如经典的谷歌面试题「扔鸡蛋」,状态需要从「跳跃临界层」转入「N+1试探」;看似和责任链模式很近似,实际上状态仅注重「下一个状态」,而非责任链式的「交给其他多个处理模块」,详情参考:https://zhuanlan.zhihu.com/p/92268852

1.3.10、策略模式

官方定义

把方法分别封装成独立的类,然后将这一类使用接口统一管理起来,让需要使用这些方法的用户能够随时调用他们。

相关项目

Jira工单处理流中,把每个流程划分为一个方法,并且定义状态接口统一管理,通过Context类与主程序交互;这样如果添加新流程,只需要在数据库中写入卡片模板&执行条件,接着新增流程子类即可,无需修改主程序。

1.3.11、访问者模式

官方定义

指作用于一个对象结构体上的元素的操作,访问者可以使用户在不改变该结构体中的类的基础上定义一个新的操作。

相关项目

弊大于利,暂不考虑使用。

1.4、混合模式

1.4.1、工厂+策略+模板

使用案例

 

1.4.2、待补充

……

 

 

设计模式 及其 javascript & react 中的应用

大纲

  1. 设计模式的诞生与发展
  2. 设计模式的定义与分类
  3. 面向对象设计原则
  4. 本人对设计模式的应用和理解(策略模式与装饰模式)

 

设计模式的诞生与发展

模式(Pattern)起源于建筑业而非软件业

模式之父:美国加利福尼亚大学环境结构中心研究所所长 - Christopher Alexander 博士

  • 20世纪80年代末,软件工程(1968)界开始关注Christopher Alexander 等在这一住宅、公共建筑与城市规划领域的重大突破
  • “四人组”(Gang of Four,GoF,分别是Erich Gamma, Richard Helm, Ralph Johnson和John Vlissides)于 1994 年归纳发表了 23 种在软件开发中使用频率较高的设计模式,旨在用 模式 来统一沟通面向对象方法在分析、设计和实现间的鸿沟

设计模式的定义与分类

  • 设计模式的定义:一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验总结
  • 设计模式的分类:创建型,结构型,行为型
  • 创建型:主要用于创建对象
  • 结构型:主要用于处理类或对象(组件)的组合
  • 行为型:主要用于描述类或对象如何交互和怎样分配职责

面向对象设计原则

通俗来说,设计模式的存在是为了让软件代码编写得更优雅,更易于扩展和维护

用套话来说,设计模式的存在是为了满足面向对象设计原则

设计模式的应用与理解

  • Javascript 并不是一门纯面向对象语言
  • React 官网提倡组件组合而不是组件继承
  • 用辩证的眼光看待,不能滥用设计模式,设计模式不是用得越多越好,每个设计模式都有其优缺点

策略模式

  • 定义一系列行为,将每个行为封装起来,并让它们可以相互替换。
  • 策略模式让结果可以独立于使用它的客户变化。
  • 对象行为型模式
  • 策略模式提供了一种 可插入式 的实现方案

问题

  1. 某软件公司为某电影院开发了一套影院售票系统,在该系统中需要为不同类型的用户提供不同的电影票打折方式,具体打折方案如下:
  2. (1) 学生凭学生证可享受票价8折优惠。
  3. (2) 儿童可享受每张票减免10元的优惠(原始票价需大于等于20元)。
  4. (3) 影院VIP用户除享受票价半价优惠外还可进行积分,积分累计到一定额度可换取电影院赠送的奖品。
  5. 该系统在将来可能还要根据需要引入新的打折方式。现使用策略模式设计该影院售票系统的打折方案。

代码演示

解决方案 1

  1. function getPrice(price, identity) {
  2. if (identity === 'student') {
  3. return price * 0.8;
  4. } else if (identity === 'child') {
  5. return price - 10;
  6. } else if(identity === 'old'){
  7. return price * 0.7;
  8. } else {
  9. addPoint();
  10. return price / 2;
  11. }
  12. }
  13. function addPoint() {
  14. log('为 VIP 添加积分');
  15. }
  16. const identity = 'VIP';
  17. log(`${identity}的票价为:`, getPrice(30, identity));

解决方案 1 带来的问题是 许多 if - else 代码块,当又出现一个新的角色,比如 老人,老人价格是5折,那就要修改 getPrice 的代码,添加一条 else if 语句。随着角色的增多或者逻辑的复杂, if - else 语句块将难以维护和阅读

解决方案 2

策略模式 - UML

面向对象的设计模式 (typeScript)

Discount.ts

  1. interface Discount {
  2. calculate(price: number): number;
  3. }
  4. export default Discount;

MovieTicket.ts

  1. import Discount from '../abstact/Discount';
  2. class MovieTicket {
  3. private price:number;
  4. private discount:Discount;
  5. constructor() {
  6. price = 0;
  7. discount = null;
  8. }
  9. setPrice(price: number) {
  10. price = price;
  11. }
  12. setDiscount(discount: Discount) {
  13. discount = discount;
  14. }
  15. getPrice() {
  16. return this.discount.calculate(this.price);
  17. }
  18. }
  19. export default MovieTicket;

StudentDiscount.ts

  1. import Discount from '../abstact/Discount';
  2. class StudentDiscount implements Discount{
  3. calculate(price: number): number {
  4. return price * 0.8;
  5. }
  6. }
  7. export default StudentDiscount;

ChildrenDiscount.ts

  1. import Discount from '../abstact/Discount';
  2. class ChildrenDiscount implements Discount {
  3. calculate(price: number): number {
  4. return price - 10;
  5. }
  6. }
  7. export default ChildrenDiscount;

VIPDiscount.ts

  1. import Discount from '../abstact/Discount';
  2. class VIPDiscount implements Discount {
  3. calculate(price: number): number {
  4. addPoint();
  5. return price / 2;
  6. }
  7. }
  8. function addPoint(): void {
  9. log('增加积分');
  10. }
  11. export default VIPDiscount;

OldDiscount.ts

  1. import Discount from '../abstact/Discount';
  2. class OldDiscount implements Discount {
  3. calculate(price: number): number {
  4. return price - 20;
  5. }
  6. }
  7. export default OldDiscount;

index.ts(执行文件

  1. import MovieTicket from './domain/MovieTicket';
  2. import OldDiscount from './domain/OldDiscount';
  3. const oldDiscount = new OldDiscount();
  4. const movieTicket = new MovieTicket();
  5. // 初始化
  6. setPrice(30);
  7. setDiscount(oldDiscount);
  8. // 结果
  9. log(movieTicket.getPrice());

解决方案 2 采用了面向对象的策略模式,将每个 打折价格 抽象成类,通过注入的形式(setDiscount)获取最终的价格(getPrice);优点是 易于扩展和维护,满足开闭原则,当有新角色 老人时,新建一个 OldDiscount.ts 即可;但是这样带来的问题是,类的文件数量急剧上升,文件和项目的规模难以维护。

解决方案 3

config.js

  1. const PRICE = 30;
  2. function addPoint() {
  3. log('为 VIP 添加积分');
  4. }
  5. const CONFIG = {
  6. id: 2,
  7. callback: addPoint,
  8. priceList: [
  9. PRICE * 0.8,
  10. PRICE - 10,
  11. PRICE / 2,
  12. ],
  13. identityList: [
  14. 'student',
  15. 'child',
  16. 'VIP',
  17. ],
  18. };
  19. exports = CONFIG;

index.js

  1. const { priceList, id, identityList, callback } = require('./config');
  2. function getPrice({ priceList, id, callback }) {
  3. if (typeof callback === 'function') {
  4. callback();
  5. }
  6. return priceList[id];
  7. }
  8. log(`${identityList[id]}的价格为:`,
  9. getPrice({
  10. priceList,
  11. id,
  12. callback
  13. })
  14. );

解决方案 3 没有使用到面向对象,将 解决方案 2 的 calculate 方法统一抽象成 getPrice 方法。数据和配置项放在 config.js 里,将 函数 和 数据 分离。虽然可读性一般,但是减少了文件规模和数量。

以上是生活中的问题抽象化,那么在实际开发中,有什么地方和该场景类似,可以使用策略模式呢?

本人是这么认为的:

实际开发 - 应用场景

  • 绝大多数需求是以实验的方式进行的,目前本人碰到的实验有三个类别
  • 一种需要改变 Dom 结构,这个需要改动组件结构
  • 一种使得某些行为发生变化,如点击事件
  • 另一种:当我们需要根据服务端返回的实验参数的不同,显示不同的文案或数据时,可以尝试使用策略模式

LabChooser/index.js

  1. import utils from '../../../../packages/assets/javascripts/utils';
  2. export default class LabChooser {
  3. constructor(data) {
  4. data = data;
  5. }
  6. /**
  7. * @param {string} name '模块名.数据名'
  8. * @param {boolean | Number} isInLab
  9. */
  10. get(name, isInLab = false) {
  11. if (typeof name !== 'string') {
  12. warn('labChooser', 'name should be string');
  13. name = '';
  14. }
  15. if (typeof isInLab !== 'boolean' && typeof isInLab !== 'number') {
  16. warn('labChooser', 'isInLab should be boolean or Number');
  17. isInLab = false;
  18. }
  19. const finalData = this.data[Number(isInLab)];
  20. return utils.safeGet(finalData, name);
  21. }
  22. }

cashLab/index.js

  1. import LabChooser from '../index';
  2. import defaultData from './data/defaultData';
  3. import labData1 from './data/labData1';
  4. import labData2 from './data/labData2';
  5. export default new LabChooser([
  6. defaultData,
  7. labData1,
  8. labData2
  9. ]);

data/*.js

commonData.js

  1. // 通用数据
  2. export const descTitleKey = 'fe.page_apprentices.apprentice_header.bonus_day_no';
  3. export const descTitleVal = 'Day { dayNo }';
  4. export const unit = '₹';
  5. export const titleKey = 'fe.page_invitation_code.earn_steps.step_title';
  6. export const titleContent = 'Step {stepNo}';

defaultData.js

  1. import ApprenticeHeader from '../../../../containers/Apprentices/components/ApprenticeHeader/data';
  2. import headerImageUrl from '../../../../containers/InvitationCode/components/InviteHeader/data';
  3. import EarnSteps from '../../../../containers/InvitationCode/components/EarnSteps/data';
  4. export default {
  5. Apprentices: {
  6. ApprenticeHeader
  7. },
  8. InvitationCode: {
  9. InviteHeader: {
  10. headerImageUrl
  11. },
  12. EarnSteps
  13. },
  14. Rule: {
  15. cashLabText: {
  16. day1coins: 'Rs.3.',
  17. day2coins: 'Rs.3.',
  18. day3coins: 'Rs.4.',
  19. day4coins: 'Rs.10',
  20. day5coins: 'Rs.11.'
  21. }
  22. },
  23. Task: {
  24. number: 46,
  25. InviteHeader: {
  26. key: 'fe.page_task.invite_header.header_image_url',
  27. val: 'http://p0.sgpstatp.com/origin/f05daff3b73b402255fe',
  28. }
  29. }
  30. };

labData1.js

  1. import {descTitleKey, descTitleVal, unit, titleKey, titleContent} from './commonData';
  2. export default {
  3. Task: {
  4. number: 56,
  5. InviteHeader: {
  6. key: 'fe.page_task.invite_header.header_image_url_56',
  7. val: 'http://p0.sgpstatp.com/origin/f05db2a258b4c021cc90',
  8. }
  9. },
  10. Rule: {
  11. cashLabText: {
  12. day1coins: 'Rs.3.',
  13. day2coins: 'Rs.3.',
  14. day3coins: 'Rs.4.',
  15. day4coins: 'Rs.10',
  16. day5coins: 'Rs.11.',
  17. day1coinsText: 'you will receive Rs.13 if it is your first success invitation.',
  18. }
  19. },
  20. InvitationCode: {
  21. InviteHeader: {
  22. headerImageUrl: {
  23. key: 'fe.page_invitation_code.invite_header.header_image_url.cash_lab_1',
  24. val: 'https://p16.topbuzzcdn.com/origin/tos-alisg-i-0000/bf227e3e540a4c1ea3c40b8a559eac8c.png'
  25. }
  26. },
  27. EarnSteps: {
  28. earnStepTitle: {
  29. key: 'fe.page_invitation_code.earn_steps.title_number',
  30. number: 56
  31. },
  32. earnStepList: [
  33. {
  34. titleKey,
  35. titleContent,
  36. descKey: 'fe.page_invitation_code.earn_steps.step_one_desc',
  37. descContent: 'Click to share in WhatsApp'
  38. },
  39. {
  40. titleKey,
  41. titleContent,
  42. descKey: 'fe.page_invitation_code.earn_steps.step_two_desc',
  43. descContent: 'You friends install helo and login'
  44. },
  45. {
  46. titleKey,
  47. titleContent,
  48. descKey: 'fe.page_invitation_code.earn_steps.step_three_desc.cash_lab_1',
  49. descContent: 'if your friends enjoy Helo for 14 days, you will get Rs 41, and if they continue to enjoy Helo after, you may get another Rs 15.'
  50. }
  51. ]
  52. }
  53. },
  54. Apprentices: {
  55. ApprenticeHeader: {
  56. titleText: {
  57. key: 'fe.page_apprentices.apprentice_header.title_number',
  58. cashNumber: 56
  59. },
  60. topItem: {
  61. amount: 1000,
  62. unit,
  63. descTitleKey: 'fe.page_apprentices.apprentice_header.top_bonus_title',
  64. descTitleVal: 'First invitation reward',
  65. descContentKey: 'fe.page_apprentices.apprentice_header.top_bonus_desc',
  66. descContentVal: 'First friend install and login Helo',
  67. horizental: false
  68. },
  69. cashBonusList: [
  70. [
  71. {
  72. amount: 1300,
  73. unit,
  74. descTitleKey,
  75. descTitleVal,
  76. dayNo: 1,
  77. descContentKey: 'fe.page_apprentices.apprentice_header.day_one_earn_desc.lab',
  78. descContentVal: 'Installed & login Helo',
  79. arrow: 'right',
  80. horizental: false,
  81. moreCash: false
  82. },
  83. {
  84. amount: 300,
  85. unit,
  86. descTitleKey,
  87. descTitleVal,
  88. dayNo: 2,
  89. descContentKey: 'fe.page_apprentices.apprentice_header.day_two_earn_desc.lab',
  90. descContentVal: 'Launch Helo the next day',
  91. arrow: 'right',
  92. horizental: false,
  93. moreCash: false
  94. },
  95. {
  96. amount: 400,
  97. unit,
  98. descTitleKey,
  99. descTitleVal,
  100. dayNo: 3,
  101. descContentKey: 'fe.page_apprentices.apprentice_header.day_three_earn_desc.lab',
  102. descContentVal: 'Launch Helo the third day',
  103. horizental: false,
  104. moreCash: false
  105. }
  106. ],
  107. [
  108. {
  109. amount: 1000,
  110. unit,
  111. descTitleKey,
  112. descTitleVal,
  113. dayNo: 7,
  114. arrow: 'right',
  115. descContentKey: 'fe.page_apprentices.apprentice_header.day_seven_earn_desc.lab',
  116. descContentVal: 'Friend open Helo for 7 days continuously',
  117. horizental: false,
  118. moreCash: false,
  119. isWider: true
  120. },
  121. {
  122. amount: 1100,
  123. unit,
  124. descTitleKey,
  125. descTitleVal,
  126. dayNo: 14,
  127. descContentKey: 'fe.page_apprentices.apprentice_header.day_fourteen_earn_desc.lab',
  128. descContentVal: 'Friend open Helo for 14 days continuously',
  129. horizental: false,
  130. moreCash: false,
  131. isWider: true
  132. }
  133. ],
  134. [
  135. {
  136. amount: 1500,
  137. unit,
  138. descTitleKey: 'fe.page_apprentices.apprentice_header.bonus_total_label',
  139. descTitleVal: 'In Total',
  140. descContentKey: 'fe.page_apprentices.apprentice_header.total_earn_desc.lab',
  141. descContentVal: 'Friends enjoy Helo every day, you got 400 coins',
  142. horizental: true,
  143. moreCash: true
  144. }
  145. ]
  146. ]
  147. }
  148. }
  149. };

labData2.js

  1. import {descTitleKey, descTitleVal, unit, titleKey, titleContent} from './commonData';
  2. export default {
  3. Task: {
  4. number: 100,
  5. InviteHeader: {
  6. key: 'fe.page_task.invite_header.header_image_url_100',
  7. val: 'http://p0.sgpstatp.com/origin/f05db2a3af2b0017ae77',
  8. }
  9. },
  10. Rule: {
  11. cashLabText: {
  12. day1coins: 'Rs.8.',
  13. day2coins: 'Rs.8.',
  14. day3coins: 'Rs.10.',
  15. day4coins: 'Rs.20',
  16. day5coins: 'Rs.29.'
  17. }
  18. },
  19. InvitationCode: {
  20. InviteHeader: {
  21. headerImageUrl: {
  22. key: 'fe.page_invitation_code.invite_header.header_image_url.cash_lab_2',
  23. val: 'https://p16.topbuzzcdn.com/origin/tos-alisg-i-0000/78eda1a2a5ac44568728f6a4f2960120.png'
  24. }
  25. },
  26. EarnSteps: {
  27. earnStepTitle: {
  28. key: 'fe.page_invitation_code.earn_steps.title_number',
  29. number: 100
  30. },
  31. earnStepList: [
  32. {
  33. titleKey,
  34. titleContent,
  35. descKey: 'fe.page_invitation_code.earn_steps.step_one_desc',
  36. descContent: 'Click to share in WhatsApp'
  37. },
  38. {
  39. titleKey,
  40. titleContent,
  41. descKey: 'fe.page_invitation_code.earn_steps.step_two_desc',
  42. descContent: 'You friends install helo and login'
  43. },
  44. {
  45. titleKey,
  46. titleContent,
  47. descKey: 'fe.page_invitation_code.earn_steps.step_three_desc.cash_lab_2',
  48. descContent: 'if your friends enjoy Helo for 14 days, you will get Rs 85, and if they continue to enjoy Helo after, you may get another Rs 15.'
  49. }
  50. ]
  51. }
  52. },
  53. Apprentices: {
  54. ApprenticeHeader: {
  55. titleText: {
  56. key: 'fe.page_apprentices.apprentice_header.title_number',
  57. cashNumber: 100
  58. },
  59. cashBonusList: [
  60. [
  61. {
  62. amount: 800,
  63. unit,
  64. descTitleKey,
  65. descTitleVal,
  66. dayNo: 1,
  67. descContentKey: 'fe.page_apprentices.apprentice_header.day_one_earn_desc.lab',
  68. descContentVal: 'Installed & login Helo',
  69. arrow: 'right',
  70. horizental: false,
  71. moreCash: false
  72. },
  73. {
  74. amount: 800,
  75. unit,
  76. descTitleKey,
  77. descTitleVal,
  78. dayNo: 2,
  79. descContentKey: 'fe.page_apprentices.apprentice_header.day_two_earn_desc.lab',
  80. descContentVal: 'Launch Helo the next day',
  81. arrow: 'right',
  82. horizental: false,
  83. moreCash: false
  84. },
  85. {
  86. amount: 1000,
  87. unit,
  88. descTitleKey,
  89. descTitleVal,
  90. dayNo: 3,
  91. descContentKey: 'fe.page_apprentices.apprentice_header.day_three_earn_desc.lab',
  92. descContentVal: 'Launch Helo the third day',
  93. horizental: false,
  94. moreCash: false
  95. }
  96. ],
  97. [
  98. {
  99. amount: 2000,
  100. unit,
  101. descTitleKey,
  102. descTitleVal,
  103. dayNo: 7,
  104. arrow: 'right',
  105. descContentKey: 'fe.page_apprentices.apprentice_header.day_seven_earn_desc.lab',
  106. descContentVal: 'Friend open Helo for 7 days continuously',
  107. horizental: false,
  108. moreCash: false,
  109. isWider: true
  110. },
  111. {
  112. amount: 3900,
  113. unit,
  114. descTitleKey,
  115. descTitleVal,
  116. dayNo: 14,
  117. descContentKey: 'fe.page_apprentices.apprentice_header.day_fourteen_earn_desc.lab',
  118. descContentVal: 'Friend open Helo for 14 days continuously',
  119. horizental: false,
  120. moreCash: false,
  121. isWider: true
  122. }
  123. ],
  124. [
  125. {
  126. amount: 1500,
  127. unit,
  128. descTitleKey: 'fe.page_apprentices.apprentice_header.bonus_total_label',
  129. descTitleVal: 'In Total',
  130. descContentKey: 'fe.page_apprentices.apprentice_header.total_earn_desc.lab',
  131. descContentVal: 'Friends enjoy Helo every day, you got 400 coins',
  132. horizental: true,
  133. moreCash: true
  134. }
  135. ]
  136. ]
  137. }
  138. }
  139. };

如何使用

  1. {
  2. number: cashLabChooser.get('Task.number', cashLabNumber)
  3. };
  4. const { headerImageUrl } = cashLabChooser.get('InvitationCode.InviteHeader', cashLabNumber);
  5. const {earnStepList, earnStepTitle} = cashLabChooser.get('InvitationCode.EarnSteps', cashLabNumber);

该方案让 数据 和 选择器 分离解耦合,使用的时候也让 组件 和 实验数据 解耦合;但是带来的问题是,随着实验的增多,选择器的模块也随之增多;同时也需要我们维护 数据模块,写成具有相同结构的 map 映射。

策略模式-优缺点

  • 模式优点
  • 对开闭原则的支持,用户可以不修改业务逻辑的基础上选择策略,也可灵活添加新策略
  • 避免多重条件选择语句
  • 模式缺点
  • 必须知道所有的策略,并自行决定使用哪一种策略
  • 将造成系统产生很多具体的策略
  • 无法同时使用多个策略

装饰模式

  • 动态地给一个对象(组件)增加一些额外的职责。就扩展功能而言,装饰模式提供了比使用子类更加灵活的替代方案
  • 对象结构型模式

                      

 

  • 装饰模式在 react 中应用广泛,组件的 children,hoc,render props,hooks;都是装饰模式的应用
  • 甚至标签元素的包裹,某种程度上可以看作装饰模式的体现

装饰模式-应用场景

                      

 

                      

 

红色框的 padding margin 等 css 属性大致相同,蓝色框 的字体和 布局结构 也大致相同。不同的仅是绿色框内部的结构。

代码演示

TitleCard.js

将标题的内容通过 注入 的形式生成,部分 css 样式和 dom 结构已经实现,如果又有一个新的 卡片 结构需要实现,不需要从头造组件。

  1. import React from 'react';
  2. import Image from '../../../../../packages/components/common/Image';
  3. function createHeaderBtn(headerBtn, classPrefix, eventHandler) {
  4. return (
  5. <span className={`${classPrefix}-header-btn`} onClick={() => { if (typeof eventHandler === 'function') { eventHandler(); } }}>
  6. <span className={`${classPrefix}-header-btn-label`}>{headerBtn.text}</span>
  7. <span className={`${classPrefix}-header-btn-arrow-wrapper`}>
  8. <Image className={`${classPrefix}-header-btn-arrow`} src={headerBtn.img} />
  9. </span>
  10. </span>
  11. );
  12. }
  13. // render props 带有 title 的 组合组件
  14. /**
  15. * @param {Object} props : {
  16. * dataProps : {
  17. * headerBtn: {text, img},
  18. * title: string
  19. * },
  20. * eventProps: function,
  21. * renderBody: function
  22. * }
  23. */
  24. function TitleCard(props) {
  25. const { dataProps = {}, eventProps, renderBody, classPrefix = 'common' } = props;
  26. const finalPrefix = `${classPrefix}-card`;
  27. return (
  28. <div className={finalPrefix}>
  29. <div className={`${finalPrefix}-header`}>
  30. <span className={`${finalPrefix}-header-title`}>{dataProps.title}</span>
  31. {dataProps.headerBtn && createHeaderBtn(dataProps.headerBtn, finalPrefix, eventProps)}
  32. </div>
  33. <div className={`${finalPrefix}-body`}>
  34. {renderBody()}
  35. </div>
  36. </div>
  37. );
  38. }
  39. defaultProps = {
  40. dataProps: {},
  41. };
  42. export default TitleCard;

本人认为 还可以用装饰模式复用的地方....       

 

装饰模式-优缺点

  • 模式优点
  • 通过动态方式扩展一个组件的功能
  • 可对一个组件进行多次装饰
  • 可根据需要添加新的容器组件或装饰组件,原有组件无需变化,符合开闭原则
  • 模式缺点
  • 将产生很多小组件,难以维护
  • 更易出错,排错也更困难,对于多次装饰的组件,调试时寻找错误可能需要逐级排查,较为繁琐

 

总结

设计模式,只是一个解决方案。

因地制宜,就是天使;否则,就是魔鬼。

 

 

有关代码设计的一些思考

六大设计原则与23设计模式

1 单一职责原则

单一原则很简单,就是将一组相关性很高的函数、数据封装到一个类中。换句话说,一个类应该有职责单一。

2 开闭原则

开闭原则理解起来也不复杂,就是一个类应该对于扩展是开放的,但是对于修改是封闭的。我们知道,在开放的app或者是系统中,经常需要升级、维护等,这就要对原来的代码进行修改,可是修改时容易破坏原有的系统,甚至带来一些新的难以发现的BUG。因此,我们在一开始编写代码时,就应该注意尽量通过扩展的方式实现新的功能,而不是通过修改已有的代码实现。

3 里氏替换原则

里氏替换原则的定义为:所有引用基类的地方必须能透明地使用其子类对象。定义看起来很抽象,其实,很容易理解,本质上就是说,要好好利用继承和多态。简单地说,就是以父类的形式声明的变量(或形参),赋值为任何继承于这个父类的子类后不影响程序的执行。我们在抽象类设计之时就运用到了里氏替换原则。

4 依赖倒置原则

依赖倒置主要是实现解耦,使得高层次的模块不依赖于低层次模块的具体实现细节。怎么去理解它呢,我们需要知道几个关键点:

 

(1)高层模块不应该依赖底层模块(具体实现),二者都应该依赖其抽象(抽象类或接口)

(2)抽象不应该依赖细节

(3)细节应该依赖于抽象

其实,在我们用的Java语言中,抽象就是指接口或者抽象类,二者都是不能直接被实例化;细节就是实现类,实现接口或者继承抽象类而产生的类,就是细节。使用Java语言描述就简单了:就是各个模块之间相互传递的参数声明为抽象类型,而不是声明为具体的实现类;

5 接口隔离原则

接口隔离原则定义:类之间的依赖关系应该建立在最小的接口上。其原则是将非常庞大的、臃肿的接口拆分成更小的更具体的接口。

6 迪米特原则

描述的原则:一个对象应该对其他的对象有最少的了解。什么意思呢?就是说一个类应该对自己调用的类知道的最少。还是不懂?其实简单来说:假设类A实现了某个功能,类B需要调用类A的去执行这个功能,那么类A应该只暴露一个函数给类B,这个函数表示是实现这个功能的函数,而不是让类A把实现这个功能的所有细分的函数暴露给B。

我对6大原则的总结:

1.单一职责 (高内聚)

2.最少暴露 (低耦合)

3.依赖倒置 (依赖抽象)

我认为设计代码时单单做到以上3点, 写出来的代码都不会太差。 但是还可能有一些其他的问题, 比如代码复用性不高,业务对象间关系描述不够清晰等。

所以基于6大原则, 前人将一些行之有效的java设计case整理成更加具现的23设计模式,如图

设计模式与Java

这里需要注意的是, 我们通常讲的23种设计模式是和Java语言强相关的, 每种设计模式都会给出一或多种参考范式, 即与实现挂钩,非6大模式那种纯逻辑概念。 我认为在学习揣摩设计模式的过程里应该重意轻形。

这里有个单例模式的case,java与kotlin饿汉式单例的写法对比。

应该很难把 object xxx 这短短一行代码称作一种设计模式吧?( kotlin编译器将其范式隐藏在编译期,以下是decomplile字节码得到的kotlin饿汉式范式)

再比如观察者模式, 以下是Java中的设计范式。要在被观察者内部去提供注册观察者的接口再提供出去,看起来还是有点啰嗦的。

而在c#中, 语言本身就有 委托&事件 的设计, 可以说这个模式c#本身就提供给开发者。http://www.runoob.com/csharp/csharp-event.html  (再说一句题外话, 个人认为c#在语言设计层面有许多亮点。比如委托, 这个设计本身比interface更轻量级,  更加符合"接口"这个定义, 如果开发者更倾向于使用委托而非接口,那么就更可能会设计出符合"最少暴露"原则的代码。)

所以一些固定的范式是可以用类似语法糖的方式隐藏或优化的, 在学习阶段理解设计的思想更为重要。一些说法比如"设计模式在一定程度上就是弥补语言的缺陷" 也是在说明这个问题。

但是设计模式范式真的不重要吗? 当然不是。如果你是一个Java程序员, 设计模式范式就是你跟同事合作生产时的一种交流方式(前提是大家都理解范式),还是最高效的交流方式之一。 比如别人写个observer那么我大概就知道这个类向外暴露各种回调,或者写一个adapter, 这就是在对接两个接口。所以对待设计模式的正确方式,我认为应该是学习时重意, 干活时用形。除了23设计模式,分层设计也可以达到同样的效果,如mvc, mvp, mvvm 等。

但是设计模式本身就是完美的吗? 它就没有缺陷吗? 当然不是。 前面也说了, 设计模式只是前人对一些行之有效的设计case的总结, 而设计模式的本质, 我认为是"针对某些具体场景提供了一些效率较高的以代码复杂度换灵活性的手段"。所以它的第一个问题在于代码复杂度。很多时候我去看一些优秀开源库的实现,会发现要在源码流程里面跳很久才能找到我想找的那个功能节点的实现,中间还会经过很多抽象接口类型的代码跳转,看的很费劲。为什么会设计的这么复杂呢?因为作者为那个功能节点提供了很多的灵活性/扩展性的设计。所以代码复杂度高又会带来额外的成本 (这个成本包括 学习api&理解作者的设计理念&可能带来的额外调试成本&可能带来的额外维护成本) 。 另外,大部分的设计模式追求扩展性,也就是所谓的"依赖倒置"。那么为什么软件要追求扩展性呢? 主要点在于软件迭代。新的业务只需要按照规定好的接口类型实现即可, 不需要去更改以前的业务流程。(那么android开发中设计者使用什么样的策略让我们增加新的feature呢?) 所以扩展性的设计也可以说是面向未来设计。那么这就又带来一个问题, 如果"未来没来",会怎么样?这就是第二个问题,设计依赖对业务的判断。(这里的业务非狭义pm的需求,指代码的扩展方向) 结合了大量的设计模式,设计出n种接口类型,增加了n层的代码复杂度, 最后只上了一个简单的业务, 没享受到设计模式的好处反受其害。这就是过度设计(过度设计本质上就是你为可能发生的变动付出了过多的复杂度代价)。所以不要在业务前景不清晰的情况下过早的追求极致的灵活性, 更熟悉业务才能做出更加符合当前业务场景的设计。

设计模式的一些case

1.现在我们项目中 BaseAppData.java 这个类是一个巨大的耦合点(保存数据,处理数据,关注系统回调,关注业务回调,向外提供各种杂七杂八的能力, 可以说是一个bean+Controller+lifeCycle+service的臃肿怪物),存在很多问题。我在做push重构收敛接口的过程中碰到了此类与push耦合的问题。

此类定义了一个ResumeTask内部类,在其关注的onActivityResume回调中调用,其中调用了push的接口,以及很多其他业务的接口。那么如何将接口收敛回push,并且还能继续关注BaseAppData提供的回调呢?其实一个简单的observer模式就可以做到。

这样不只是push很多耦合的业务接口都可以拆走, 只要关注BaseAppData的回调即可。当后续重构了之后,回调器的功能被剥离出去,把这些observer的代码放在新的回调器里面即可,不影响之前的业务。这样下来算是通过设计模式简单地解决了一些问题。

2.当我们实现了一些功能,想让其具有更好的灵活性,代理模式就可以做到这点

其主要的思想是为服务实体实现一个代理类,实际的case比如安卓中的AMS。因为ActivityManagerService在另一个进程为我们app服务,我们没有办法直接获取到另一个进程对象的指针,直接使用transact接口 + 服务id方式的又对用户太不友好。所以android framework为我们提供了ActivityManagerProxy, 让我们像直接调用一样使用远程服务。

如果更进一步,本着为上层用户提供更易用的接口(接口调用 + 服务发现) ,就可以使用更高级一点的代理方式 -- 动态代理, 由框架在运行时动态生成基于被调用接口的字节码,再与实现逻辑动态对接 (比如InvocationHandler) 。  当然如果觉得动态代理性能不够好, 我在实现跨进程框架的使用使用的"静态代码"的方案也可以看看,本质是将runtime生成字节码的逻辑放在了编译期间处理, 牺牲了一些扩展性获取了额外的性能。

3.Android中ListView的实现

首先我们知道要想使用ListView必须给它设置一个adapter。也就是说设计者给它加上了一个适配器模式。那么为什么需要一个适配器模式呢? 我们都知道适配器模式是用来拼接两个或多个本不兼容的接口,那么ListView需要adapter来对接什么接口呢? 我理解是 数据&绘制流程 的接口。怎么理解?作为设计者,他知道listview需要一个getView(int position)接口来渲染每个item,但是他不知道上层用户提供的数据接口是怎样的 (可能是一个pojo,可能是多个),  所以他抛出了一个adapter让用户自己来对接数据与控件. (这里所说的数据和绘制流程接口是一个抽象的概念,并不指interface)。 这里就有一个问题,不用适配器模式的话,该怎么实现呢? 其实listview直接抛出一个抽象接口getView(int position)难道不行吗?继承也是可以的, 但是继承会带来更多的问题(mn问题,性能问题,子父类耦合问题。并且我认为很多设计是为了消除继承)。另外,listview是怎么保持与adapter的关系的呢?

其实就是一个简单的桥接模式。 这样用有个什么最大的好处呢? 我们现在看看项目中的一个类

层层继承,已经快数不过来了。其中很多都是因为某一维度特征(例如flavor)不一致被分成两份代码,又分别去继承,那么这种情况的类个数可以用m的n次方来描述。 如果将这个特征扔出去分别实现,抽象成接口持有,那么就会大大降低继承出来的类的个数。适配器 + 适配器桥接 ,这也是listview运用的两个设计模式。


人生有無數種可能,人生有無限的精彩,人生沒有盡頭。一個人只要足夠的愛自己,尊重自己內心的聲音,就算是真正的活著。