如何让软件插件化

如何让软件插件化

软件的插件化,核心是对软件进行抽象,达到一种程度使得所有人根据你抽象的模型编写完代码后能够顺利在核心软件开发者未知情况下直接使用。

很现成的例子是我们常用的插座:

目标:提供电力

如何使用:将电缆接入插座(plug-in/plug-out)

具体实现类:满足中国国标三口插座/两口插座

我们再以Spring启动事件为例,看一下Spring是如何实现的

Spring-Context初始化时候以以下函数为重点Application.refresh()

其中Fresh消息发布地方在FinishRefresh(),

在publishEvent中就能看到到底Spring是如何将其插件化的了:

重点在于需要现在把监听的对象通过spring ioc进行管理,然后就可以简单的通过订阅者模式进行事件的的发布。

划重点:

重点在于发布一个规范让所有人进行遵守,规范越是简单,那么就越灵活,越是复杂越是只能支持特定事物。

如Spring的需要两个要求,一个是对象需要通过Spring来管理并且需要实现一个特定接口。

所以另一个问题就是,如何得到规范,那就是考验设计者心智的情景了。

又是面试总结时间

更新1:按照此思路实现的简单版本

又是面试总结时间

这次面试的是一个初创公司的职位,其中一道题是不可能现场能够给出正确答案的,这里我的思考用时为六个小时。

原命题是:

如何设计一个LRUCache的System。能在接近O(1)时间内完成操作

那么以前两次提到的如何问问题,我们先确定使用场景,这是一个K-V键值系统,其中K-V值很小,但是延时要求高。

我们要求是在普通的服务器环境中构建一个尽可能低延时的LRUCache系统。

第一点是:确定数据结构

因为要以O(1)时间做查询,那么Map结构是跑不掉的,那么我们知道Map从语义上来说不一定是有序的。为了达到有序一定会有个辅助数据结构,这个结构一定是有序的。这是在我脑子里面第一个是小堆。小堆的维持堆结构操作最坏是nlogn,那么在频繁的写时候,并不能满足O(1)的需求,放弃掉;

第二个出现在我脑海中的数据结构是树,同样维持树结构仍不能满足需求;

第三个是我已经离开了面试地点想到的,跳表

这不是一般意义上的跳表,我们按照时间先后使得跳表的查询能够想hash操作一样是计算出来的,此时的hash函数一定是个递增阶梯函数

自然而然的引出了我认为最正确的答案,则是对数据集进行划分。

第二点是:具体怎么做

首先我们按照内存大小进行划分,按照面试官的提示,内存为8GB左右,我们估算一个k-v键值对大约占用100bytes,那么最差会有8.6kw个键值对,我们知道CPU的一二三级缓存速度非常快,即使是遍历也会极快,我们假设CPU的三级缓存有16MB,则我们对数据块的划分为16MB一个那么我们就拥有了512个块,我们可以近似认为在其中遍历只需要O(1)的时间,假设我们拥有一个足够均匀的hash函数能够使得key平均的分布在者512个块,那么我们查询的时间为O(1)+O(k);k为块的大小远小于整体数据量,此时删除和查询一个或者插入一个元素,近似时间都在O(1)完成。

第三点是:一些其他问题

第一点是,我们的hash有可能不能使得key均匀分布

第二点是,假设服务器还部署了其他应用,那么还要能对空闲的块进行回收合并

总结是:

大部分这种面试题如果当场做不出,也很难当场做出的,要多和面试官探讨

第三方接入平台需求建模(二)

 

持久化

考虑到我们的系统是不稳定的,内存一定不充足的,故而我们要进行持久化,以下是完成持久化类的类图:

缓存层

从技术上考虑,该系统Query的请求会占到绝大多数,且并发数量会高,如果只有DB那么压力会单点压在数据库读上,故而对Query请求增加缓存层,同时考虑到部署的机器会有多台,还要使得多台机器上缓存一致性。

我这边是使用了Redis作为集群通信的中心,完成了一个本地存储的类不便展示,思路是将最近刷新应用信息的时间放在Redis上,只要节点当前的时间比redis的时间新或者相同则不处理,否则重新从数据库中拉去数据

为了简便,没有单独的建立cache的package,只是单纯的在持久层增加了一个ClusterMap,持久化类直接使用

为什么将第三方接入的文档放出来

  1. 反正内容足够简单
  2. 这是我第一次用DDD+充血模型的方式对项目进行分析
  3. 这是我第一次完整的完成了整个项目的

其实这次放出来的,是我者离职了半个月之后提炼的,原有的代码其实很冗余,我刚开始建模的时候并没有说把流程,职责区分,也没有把持久化代码和业务模型本身的代码进行区分,导致了后期改动相当的难,最主要的点是有部分业务代码直接用SQL实现了,这并不是一个好习惯。

接下来还有具体实现一个函数时候的状态转换图,还有生命周期图还没有总结好。希望我不要弃坑了

第三方接入平台需求建模

对照着需求文档我们对平台进行建模

首先第一点关注到的是关键词

  1. 应用
  2. 授权
  3. 审核
  4. 开发者
  5. 企业

所以创建以下几个对象:

关注到授权是个动作,我们将auth的动作添加到几个对象上去,注意到几个auth的对象不一样,我们需要做区分,按传参区分

此时我们动态的再脑中运行一下,发现这个模型只满足了授权的需要,我们把其他需求添加进来

现在就差Application的状态跃迁由谁来负责了

按照逻辑上来讲,开发者应该知道应用开发的整个生命流程,但是由于这是在第三方平台上,有一些不可见的逻辑,我们这时候应该把Application的状态职责由Application自己负责管理所以如下:

这个时候发现,Developer和Application都自己负责了自己的生命周期的进行,所有操作只是发起了一个流程。

另外,关注到,审核的信息其实是Application的一个Snapshot而已,所以无论外界对Snapshot做了什么都不会影响到Application本身,但是要注意到,Snapshot本来不应该可以被修改,故而要有final修饰。

这时候核心的几个概念就已经可以用这个类图来表达了。

接下来就是扩展Application得到需求所描述的模型了,其中以下几个需求完全是基于上述模型可以直接扩展:

  • 有的应用需要把代码托管到平台处
  • 有的应用还需要数据库应用
  • 有的应用不需要审核只需要用户授权即可直接添加到用户的列表中

最终得到以下类图

第三方应用接入平台需求

第三方开发者接入平台需求文档

  • 开发者要先认证才能提交应用
  • 开发者认证需要提交一系列材料
  • 应用的信息能够多次修改
  • 应用的信息应该多次修改后能够被检索
  • 应用需要被审核才能被上架
  • 应用含有如下信息:
    • 应用的名称
    • 应用的图标
    • 应用的分类
    • 应用的授权码/签名
    • 应用的说明
  • 要区分已审核应用和未审核应用
  • 有的应用需要把代码托管到平台处
  • 有的应用还需要数据库应用
  • 有的应用不需要审核只需要用户授权即可直接添加到用户的列表中
  • 开发者创建一个应用时候默认需要对开发者本身做授权
  • 企业用户只有在付款并且应用成功开通之后才进行授权

如何问问题

Q:为什么提出这个问题

A:每次面试的时候总是面试官提问题,应聘人回答,但是回答完之后却又发现其实觉得自己没有真正回答任何一个问题,我想通过这个问题,复盘一下我面试时候出现的问题。

Q:所以应该怎么问问题呢?

A:首先先区分这个是不是有效问题,

怎么定义有效问题这个词语呢,我举个简单的例子:

如何对10W的数据进行排序;(这是一个无效的问题)

如何对10W的冷数据进行排序;(这个开始缩小范围了)

如何对10W的冷数据在获取的时候排序(继续缩小范围)

如何对10W的冷数据在获取的时候得到前100位的数据;(范围继续缩小)

如何对10W的冷数据在获取的时候按照数据存放的时间获取最近的100条(这算一个有效问题了)

所以,我们如何定义一个问题是有效问题,即是:能够很清晰简单的回答的问题,比如对10W的数据排序一上来提问,是个人都会是懵的,除非他在日常工作中曾经遇到过相同情景的事情,那他回答的一定会是他的那个分支的问题。

Q:那我们遇到面试官提问的时候如何能够得到真正的问题

A:像平时和产品或者需求提出者那样交流。首先我们应当根据问题确定提问的上下文,比如,还是以上面排序的例子作为示范:

我和产品或者BA会这么沟通:

  • 我要对这10W条数据倒序输出
  • 你这是什么样的使用场景呢
  • 这样的,我需要展示用户的最近几条操作记录以供恢复账号使用,是给运营人员用的
  • 哦,那事实上你只需要最近的操作记录是吧?你大约需要展示多少条呢?
  • 100条
  • 那就是按照时间排序,把最近的100条操作记录返回给页面是吧?
  • 这些数据在业务上来说用户是无法串改的把?
  • 运营方也不许篡改啊
  • 也就是这是这些数据其实是固化了的?
  • 那整个就变成了,我要从10W条固化数据提取出按照操作时间得到最近100条操作一记录并返回给运营人员(得到完整需求)

那么,作为开发,我就会选择在存储的时候用一个Buket存储最近100条数据,然后多个Buket组成一条直链并按照时间严格做排序

查询或者取数据的时候,则只要沿着buket一直扫描或者直接由时间计算得到位置即可。

Q:但有时候提的问题例如:介绍一下SpringMVC是如何工作的

A:这个其实并没有在问问题,这是在考察背书能力毕竟清楚SpringMVC是如何工作的表明,我应该是这个领域的专家。而事实上大部分工作的码农依赖的是HTTP协议中的一些特定部分而非SpringMVC这么一个特定框架下的领域知识。遇到这样的面试或者说面试过程中大部分都是这种问题的话,要考虑下是否还要去这样的公司,毕竟能问出这种问题的公司要么整天是修业务BUG,要么是对SpringMVC这种框架做过hacker使用的公司,哪种日子都不会好过。

Q:所以面试前应该有什么准备工作呢?

A:面试前其实没什么准备工作,面试最主要的是让求职者和公司之间进行交流。不要把面试想象的太过于像考试,而不是交流,心态变化了,那么行动上就会自然的出现变化。

前K项问题

其实吧,这个问题问出来是没有答案的,因为在实际业务中会有很多种使用场景,我们需要按场景来分类才能得到真正的问题

必须要服务端全局有序的

数据量小的时候,直接快排

数据量较大时候,对数据做分段,然后归并排序

不需要服务端全局有序的

数据直接交付client处理排序

而client有区分是否是强力PC使用还是弱计算能力的计算设备

 

除了以上,还要区分,要求一直有序还是说,最后输出的时候有序,还是说其他需求

以面试时候提到的跳一跳排名来说,其实根本没有服务端排序这个事情,小程序获取成绩应该是三个步骤:

  1. 获取用户列表中已开通跳一跳的用户
  2. 获取步骤1中所有user的成绩
  3. 对成绩按大小倒序排

就会发现和前K大这个问题没有任何关联。。。

总的来说。。我发现面试的数据结构题目对于强人和应试型人才很有优势。。but,我这种经常是时候脑袋一转发现不对的人来说是很难的。。。毕竟我的注意力也不在这里面。。

假装读懂Java线程池

假装读懂Java线程池

线程池是干什么的

不用线程池则是一个任务对应一个线程,若采用线程池那么,固定数量线程可以完成超过线程池的任务数。

为什么要用线程池

每个Thread执行的任务时间可长可短可能死循环,我们假设一万个任务需要执行,其中有几个会死循环,那么可以断言,会产生内存泄漏因为线程无法释放。

如果采用线程池,第一可以减少上下文的切换提升效率,第二就算产生泄漏,也能控制损害范围不至于是整个系统崩溃

Jdk是如何实现的

Jdk将任务和线程的角色分离了,任务称为Task是一个Runnable对象(应该是为了无痛切换原有用Thread自己实现的run,不然单独建立一个Task对象会与存量代码产生割裂),完成Task的对象成为Worker,Worker就是实际工作的线程,注意这里的Task可以为任意数目,而Worker只能是有限数目。

具体流程是:

1.用户submit或者execute提交一个Task

2.查找/创建Worker并检查是否有空闲

3.将任务赋予Worker并改变Worker当前状态,增加WorkCount

4.Worker一旦将任务执行完毕且任务队列为空将当前Worker丢弃,并减少WorkCount

5.检查当前线程池是否满足最小启动线程值

6.阻塞在获取Task状态直到下一个任务队列不为空,重复流程2

当然还有其他一些细枝末节的,就不再这个主流程上展示了

为毛不画流程图

大哥。。我只能手绘。。工具不会用

如何避免将写过程式代码

如何避免将写过程式代码

这是我在写理才网的应用管理平台时候获取的教训。

我们先来看一个简单的应用提交的模型:

其中类Application是单纯的数据类,所有的业务都集中在了ApplicationService,如果这个程序没有迭代的需求当然这是一个很好很简洁的代码,但是一旦我们增添一个需求,比如我的应用是有多种类型的,某些类型做一些操作,另一些又不用,还有一部分公用的操作,我们看看上图的简单模型会变成如何:

这个时候我们会往Service的具体方法做如下处理:

If(application.getApplicationType()==type1){

//dosomething

}else if(){

//dosomething

}………………

只是增加了一个需求,代码就开始变得臃肿起来

另一种做法是什么呢我们来看看:

这种做法就是将不同类型的Application建立不同的数据类,每个数据类的Service方法都不同,这样也可以做到简单,但是不简洁,Service的膨胀依旧存在,而且比原来更加突出。最后就会变为,每种Application的类型都有一个对应的ApplicationTypeNService。不知道在阅读本文的各位有没有想过如何用C语言写出面向对象的代码。实际上就是如同最后一种方法所说,每个方法的第一个入参是一个结构体指针。

既然我们都已经采用了Java这样的面向对象语言,为何不直接利用起来呢。

我们看最后面的这个方法如何改造:

这个时候我们把公共的一些方法放在了基类Application中,不同类型的Application的特殊操作放在了其实现类中,相对于前两种方式就十分的简洁,并且实现种也极大的减少了if-else语句