闲聊框架设计
框架就是给编写代码的开发者以方便,让开发者能够傻瓜式的编程,拿来就用,不用太多考虑底层的,硬件的,平台的差异。开发者不用考虑那些复杂的逻辑,只需要专注自身的业务,提高效率即可。
假设,都是在Android上开发,以Android开发为例闲聊一下。
组件
为了提高效率,不用考虑那些复杂的中间逻辑,简化开发者的编程,比如,对象的创建,加载,配置文件的加载解析,对象的生命周期管理等。只要你定义了一个实体,你就必须去处理这些操作,每建一个实体就要重复写一堆创建,加载,解析,管理生命周期等的逻辑,代码显得很臃肿,随着实体的增多,管理也会很麻烦,所以组件诞生了。
如Spring中的Bean,Android的四大组件,框架都只是针对组件,对组件进行加载,解析,完成生命周期。框架做的事情很简单,应用根据需求实现不同的组件,配置组件,在组件中完成所需的业务逻辑即可。
这样设计的好处是框架只是针对组件编程,代码简化,应用只需要根据具体实现对应的组件即可,在组件中完成自己的逻辑,简化客户端庞大的加载,解析等一大堆的逻辑。
举个简单的例子,比如像其他功能模块提供基本功能的组件ComponentsetUp, shutDown, invoke,onEvent
组件的调度
对组件的调用
组件的配置可以定义成
(组件名称,组件权限,作者,作用,说明,接口[名称,参数…返回值],事件[事件名称,参数…])。
Parser专门用于解析配置,在查找组件时就根据配置来查找。
生命周期可以定义为同步接口invoke当调用完毕之后就删除了这个组件,也可以为invoke完毕之后等到下次调用再删除,或者定义为调用完一段时间之后在删除;异步事件加载完毕后一直在内存中,当有事件发生就产生回调。
这样,一个简单的组件机制就完成了。ComponentBase启示还可以再继承扩展为不同类型的组件,这个根据不同的需要自定义。
组件其实在页面开发的时候比较有用,比如一个公司对所有app页面都有统一的标准,导航栏,状态栏,返回键,设置键,大小,高度,颜色都有规定;还有列表中每项大小,主标题,副标题字体大小,图标排列都有规定;页面是否有跳转,跳转页面和不跳转得页面是怎样的,还有阅读的页面有记录的位置,以便于恢复。这种统一格式的app就需要做成组件和组件体系。当需要改动的时候,就不用每个实现的地方都需要改动,改动了可能还千差万别,造成不统一。一个新人去开发相应公司的每条ui标准的应用,可能会花大量时间阅读这些标准,浪费时间,还可能会遗漏,导致部门ui不合标准。
代码调用
服务端代码的实现
服务端代码供客户端调用,在开发的时候,双方应当商议好,怎么定义好接口,参数,返回值等。在定义好接口之后,服务端就根据商定好的去实现这些接口。
很多就是把实现放到接口里面,这样做代码很明显就太臃肿了。于代码的阅读,调试,修改,扩展,移植都非常不利。只要一涉及到修改变动,整个文件都需要修改,整个代码到处都受到污染。
于是,很多代码就分离出接口部分和实现部分,接口部分就是纯粹的接口,仅供调用,实现部分就是接口功能具体实现的地方。但是这种方式也不是很好,接口和实现也是耦合着的,在修改,移植的时候可能会牵连着接口部分跟着修改。
针对这种,又衍生出了三层代码,接口,中间代码,实现代码,接口就是纯粹的接口,中间代码将实现和接口隔离,完全解耦,当有改动,移植的时候,只需要改实现部分就好,并不会污染接口层的代码。
观察者模式改进为订阅-发布模式
public abstract class EventReceiver {
private EventManager mManager;
public EventReceiver(EventManager event) {
mManager = event;
}
public void onEvent(String event, Object[] objs);
}
public class EventManager {
List<EventReceiver> mEventList = new ArrayList<EventReceiver>();
public EventManager() {
}
public void registerEvent(EventReceiver receiver) {
if(!mEventList.contains(receiver)) {
mEventList.add(receiver);
}
}
Public void unregister(EventReceiver receiver) {
if(mEventList.contains(receiver)) {
mEventList.remove(receiver);
}
}
Public void processEvent(String event, Object[] objs) {
for(int i = 0; i < mEventList.size(); i++) {
String oEvent = mEventList.get(i);
if(oEvent.euqals(event)) {
mEventList.get(i).onEvent(event, objs);
}
}
}
}
public class Updater extends EventReceiver{
public Updater (EventManager manager){
this.mManager = manager;
this.mManager.register(this);
}
@Override
public void onEvent() {
Log.d(“Updater”, “version is updated”);
}
}
复制代码
- 客户端只需要实现EventReceiver, 并通过EventManager注册,就可以实现观察者模式。但是这样存在一个问题,客户端类和服务端类相互绑定在一起,特别时服务端类EventManager有需求变动,客户端会受到污染,被跟着改动。*
引入中间层和实现层,把代码分离
public abstract class EventReceiver {
public void onEvent(String event, Object[] objs);
}
public class EventManager {
public EventManager() {
}
public void registerEvent(EventReceiver receiver) {
}
Public void unregister(EventReceiver receiver) {
if(mEventList.contains(receiver)) {
mEventList.remove(receiver);
}
}
}
public class EventService {
List<EventReceiver> mEventList = new ArrayList<EventReceiver>();
public void registerEvent(EventReceiver receiver) {
if(!mEventList.contains(receiver)) {
mEventList.add(receiver);
}
}
Public void unregister(EventReceiver receiver) {
if(mEventList.contains(receiver)) {
mEventList.remove(receiver);
}
}
Public void processEvent(String event, Object[] objs) {
for(int i = 0; i < mEventList.size(); i++) {
String oEvent = mEventList.get(i);
if(oEvent.euqals(event)) {
mEventList.get(i).onEvent(event, objs);
}
}
}
}
复制代码
做一定的代码隔离,去掉客户端和服务端耦合情况
public abstract class EventReceiver {
public EventReceiver() {
}
abstract public void onEvent(String event, Object[] objects);
}
public class EventManager {
private static class Instance {
private static EventManager INSTANCE = new EventManager();
}
public static EventManager getInstance() {
return Instance.INSTANCE;
}
private EventManager () {
}
public void subscribe(EventReceiver receiver, List<String> events) {
EventServer.getInstance().register(receiver, events);
}
public void unsubscribe(EventReceiver receiver) {
EventServer.getInstance().unregister(receiver);
}
}
public class EventProxy {
private List<String> mEvents;
private EventReceiver mReceiver;
public EventProxy(List<String> events, EventReceiver receiver) {
mEvent = event;
mReceiver = receiver;
}
public void addEvent(String event) {
if (mEvents != null && !mEvents.contains(event)) {
mEvents.add(event);
}
}
public String getEvent(String event) {
if (mEvents != null) {
for (int i = 0; i < mEvents.size(); i++) {
if (event != null && event.equals(mEvents.get(i))) {
return mEvents.get(i);
}
}
}
}
public EventReceiver getReceiver() {
return mReceiver;
}
}
public class EventServer {
List<EventProxy> mProxys = new ArrayList<>();
private EventServer() {
}
private static class Instance {
private static EventServer INSTANCE = new EventServer();
}
public static EventServer getInstance() {
return Instance.INSTANCE;
}
public void register(EventReceiver receiver, List<String> events) {
EventProxy proxy = new EventProxy(receiver, events);
mProxys.add(proxy);
}
public void unregister(EventReceiver receiver) {
}
public EventProxy getProxy(String event) {
for (int i = 0; i < mProxys.size(); i++) {
EventProxy proxy = mProxys.get(i);
if (proxy != null && event != null && event.equals(proxy.getEvent())) {
return mProxys.get(i);
}
}
}
private void processEvent(String event, Object[] values) {
for (int i = 0; i < mProxys.size(); i++) {
EventProxy eventProxy = getProxy(event);
if (eventProxy != null) {
eventProxy.getReceiver().onEvent(event, values);
}
}
}
public void publish(String event, Object[] objects) {
processEvent(event, objects);
}
public class MyEventReceiver extends EventReceiver {
@Override
public void onEvent(String event, Object[] objects) {
Log.d("", "receive event: " + event + " values: " + objects)
}
}
}
复制代码
客户端的调用
MyEventReceiver mReceiver = new MyEventReceiver();
List<String> events = new ArrayList<>();
events.add("E_MY_EVENT_CHANGE");
EventManager.getInstance().subscribe(mReceiver);
复制代码
客户端和服务端做了一个中间隔离,客户端直接调用注册,监听就好,写得就比较优雅。
服务端的防御性设计
为了更好的贯彻傻瓜式编程,不给开发者编写代码带来太多麻烦,那么客户端应该要更简洁,服务端就应该提供统一的代码调用,便于调用端,不用考虑服务端实现的一切细节。
对于服务端,不能幻想客户端都是优秀的代码,所有的调用都是井然有序的,应该要考虑到客户端可能会是恶意的调用,针对客户端调用过多,调用次数过频,调用执行过长,导致一系列卡顿,无响应,崩溃等,我们应该要有相当的措施来优化解决这些问题。
防止调用次数过多
针对客户端可能会出现多个实例,调用次数过多,但是又不能把过多的调用舍弃,那就应该对过来的调用放到一个队列里面,做一个排队,当有足够的资源执行的时候,出队列执行,如果资源不足,就放到队列里面等待。
防止次数过频
可能会出现一个客户端实例,非常频繁地调用。可以设置一个调用时间间隔,在时间间隔之内,则舍弃;如果不舍弃,也可以将过频的调用放入等待队列,当资源不足时等待,当资源充足时出队列执行。
防止调用时间过长
如果整个系统资源紧张,调用过多且频繁,那么有可能出现一些调用比较重的任务长时间执行导致其他任务得不到执行。导致整个调度无响应,这个可以开启子线程来执行,不至于重任务导致整个服务端卡住。当然这个要做好资源的同步。
以反射调用为例说明一下。
反射是Java的一种很好的机制,能够通过统一的代码方式而不用引入所需要类接口而实现调用,屏蔽代码的差异,实现代码调用的统一。
反射的步骤:
- Class cls = Class.forName(“类名”);
- Method method = cls.getMethod(“方法名”);
- method.invoke(cls, 参数…);
为了代码的更好封装,调用可以将这三个步骤封装到一个类Reflector里面,只需要传参数类名,方法名,参数即可,调用端就不用写一大堆的代码。
Class Reflector {
…
public static void invoke(String clsName, String methodName, Class<?>… paratypes, Boolean Declared) {
Class cls = Class.forName(clsName);
Method method;
if(Declared) {
method = cls.getDeclaredMethod(methodName, paraTypes);
} else {
method = cls.getMethod(methodName, paraTypes);
}
if (method != null) {
method.setAccessible(true);
method.invoke(cls, paraTypes);
}
}
…
}
复制代码
这种方式统一了代码调用,并没有提高效率,可以添加缓存,不用每次class都去forName,也不用每次都去getMethod,有缓存直接调用,没有则获取并缓存再调用,提高效率
public Class Clazz {
Class<?> mClass;
HashMap<String, Method> mMethod;
getCachedMethod(String name) {
return mMethod.get(name);
}
putCachedMethod(String name, Method method) {
mMethod.put(name, method);
}
}
复制代码
改进后的invoke函数就可以这样写了
Public static HashMap<String, Clazz> mClazzMap;
public static void invoke(String clsName, String methodName, Class<?>… paratypes, Boolean Declared) {
Class cls;
Clazz clazz = mClazzMap.get(clsName);
if(clazz == null) {
class cls = Class.forName(clsName);
mClazzMap.put(clsName, clazz);
} else {
cls = clazz.mClass;
}
Method method = clazz.getCachedMethod(methodName);
If(method == null) {
if(Declared) {
method = getDeclaredMethod(methodName, paraTypes);
} else {
method = cls.getMethod(methodName, paraTypes);
}
} else {
method.setAccessible(true);
method.invoke(cls, paraTypes);
}
}
复制代码
代码的统一调用在进程间通信的调用,数据库访问调用等,这些地方都大有作为,简化客户端的调用方式,统一代码的风格有非常好的作用。
考虑到客户端可能会无节制的调用,会导致服务端的负载过大,资源不够的情况,那么服务端就需要做加强,做预防性设计。
防止调用时间过长
如果一个客户端的调用任务很重,有很多数据库的访问,文件操作,很多的额循环处理,那就得小心了,这个是否需要开启子线程处理,以免导致anr。
防止调用过多和调用过频
如果客户端频繁地调用,会导致服务端不停地执行,加重了服务端地负载,导致资源不够,内存不足,anr等。
怎么去处理这种问题
-
简单粗暴一点,就设定一个时段,在这个时段内再次调用地可以舍弃。
-
做得用户体验好一点,可以设置一个队列,当有过多得调用过来,设置一个队列,设置一个最多子线程执行数,比如10条,一旦超过这个数据,那么放入等待队列等待执行,当低于10条,那么选择最近得一条执行。当然还可以继续迭代,设置优先级,最高优先级得得到执行。
这样整个服务端的代码就会非常的健壮,能够抵挡住客户端大量的不是写得很好得代码调用,增强整个系统得稳定性和用户体验。
综上,我们设计一个线程池,将调用过来地任务进行排队,当资源足够时,将之放入线程中执行,否则,放入队列中等待。
Class Pooler {
Private static Pooler mPooler;
private Pooler () {
}
Public static synchronized Pooler getInstance() {
if(mPooler == null) {
return new Pooler();
}
return mPooler;
}
Excutor excutor = new ThreadPoolExecutor(4,
10,
20,
TimeUnit.SECONDS,
New LinkedBlockedQueue<Runnable>);
executor.sumbit();
Class Task implements Runnable {
private Work mWork;
public Task(Work work) {
mWork = work;
}
@override
public void run () {
mWork.run();
}
}
public Interface Work {
public run();
}
Public MyWork implements Work {
Caller mCaller;
MyWork(Caller caller) {
mCaller = caller;
}
@Override
Public void run() {
mCaller.reflector.invoke(mResult.objs[0], mResult.objs[1], mResult.objs[2], mResult.objs[3]);
}
}
public Class Caller {
public Reflector reflector;
Object[] objs;
Public Caller(Reflector reflect, Object[] obj) {
reflector = reflect;
objs = obj;
}
}
}
复制代码
客户端调用地时候只需要构造好Caller,直接调用run就好
Pooler.getInstance().sumbit(new Caller(new Reflector, new Object[…]));
调用填充可以设置为一个基类,让用户自己去根据需要实现,服务端提供了基本的能力,客户端根据需要去实现。
服务端代码代码不应该写死,服务端代码应当只提供基本功能的代码,那些缓存,线程池等,在服务端一开始的时候就加上了这些,明显就有点过度设计了,这些应当时作为基本能力提供出来,不使用,当客户端需要时定制,客户端的需求是会变化的,当客户端需要的时候再去根据需要扩展使用。所以,服务端在设计时应该考虑到,提供相应的能力,给出说明和指导;而客户端根据自己的需要去定制。把变化交给客户端,服务端只提供基本能力。比如可能出现客户端页面有数据库访问,有图片的加载,有网络的访问,在页面进行io操作,要考虑到使用AsyncTask的方式来解决大量数据加载和页面刷新。
代码的控制,代码分离,代码整洁,应对变化不确定
代码隔离就不得不提Spring设计思想里面的控制反转,依赖注入。简而言之,就是不要试图通过代码控制代码,把那些实体都抽象化,不让他们相互依赖绑定,等到需要的时候实例化实体,通过依赖注入绑定这些实体,代码有耦合的地方就统一在一个控制部分绑定注入。如果需要改需求的话只需要改相应的依赖注入的逻辑。
我们就以拍电影来简单说明一下这种设计思想,涉及到出品方,制片方,导演,演员,角色,剧本来说明一下
举个例子,金庸的作品在出品方买下版权后,准备拍电视剧
这是一个非常简单直接的拍摄方式,演员就由黄日华内定了,剧本和演员就绑定在一起。但是如果出品方,或者制片方想要翻拍,需要换其他演员,那就需要把剧本里面的演员改成新的演员,比如改成张智霖,但是考虑后续还会翻拍,李亚鹏,胡歌也想试试。所以,剧本不应该和演员绑定,需要把演员抽象出来。
但是后来,观众吐槽,整部剧的台词生硬,场面不是原著里面所描述的,画面是5毛,抠图,配音的。出品方和制片方一起商量,必须来一个牛逼的导演,让一个好的导演来把控好一切。
导演就要掌控台词,场景,道具,选角,表演,剪辑,配音等所有的一切,完全由导演一个人来控制。
但是,后来出品方和制片方又怕导演权力太大,里面有一些不知道的内幕,导致剧拍不好,所以,制片和出品需要把导演抽象出来,导演可以海选。
还是发现有问题,人是没问题了,但是,剧本,台词和原著相去甚远,甚至改得面目全非,根本就不是射雕英雄传了。所以得抽象出剧本出来,编剧也可以海选。
做到这样,后续大陆,或者海外想要翻拍,编剧,演员,导演,完全由出品和制片方去决定,拍出更高品质的剧出来。
其实以上就是不断的代码解耦过程,将剧本与演员解耦,引入导演,让导演控制整个剧的拍摄。我们在不断学习好莱坞,别人的剧都是一季一个导演,甚至一集一个导演。后来将导演与具体的人解耦,编剧与具体的人解耦。不断地把各自的权力交接出来,最后由出品和制片根据需要来控制。需要绑定的时候,可以通过依赖注入(海选,满意签约)的方式来实现。
设计模式里面就有一条,依赖倒转,不要让代码控制代码,要通过需求去控制,当你需要的时候再去绑定。
设计模式里面还有一条,面向抽象编程,面向接口编程,电影里面的角色最好不要根据某个人来写,先虚拟好角色,写好之后再去海选演员,选择适合的那个,选好后签约开拍。当然了,剧组里面的化妆,灯光,摄影,编剧,所有人都不用去绑定,在需要的时候去招聘,合适了再签约。
依赖注入
有了抽象,引入了三方监制,那所有的包括演员,剧本,导演,工作人员等都不需要事先确定,只是在出品方需要的时候,交给监制去完成。监制根据需要去选择导演,导演和监制根据剧本去选择演员,灯光,摄影,化妆等都可以去面试选择。找到合适的人之后签约。如果又什么变化也可以解约,可以再去选择,非常的灵活,而不至于有一个人的因素导致整个工作无法继续(比如耍大牌,罢演)。
引入三方中介
为了最大限度发挥好各自的才能,演员就是为了表演而生的,导演只为艺术而生,剧本就是整个电影的灵魂,都是为了艺术献身,他们只能专注于本职工作,这也是设计模式的核心思想之一,单一职责。
但是就是怕艺术商业化了,人就不踏实了,演员狮子大开口,钱不到位不演;编剧,工作人员福利不到位不干活;导演说我不擅长协调这些,钱不是我出的,我也无法,咋办。
投资人,出品公司没办法,只好祭出一个监制,可以帮导演的忙,解决那些难搞的演员,能够安抚工作人员,能够达到制片人提出的各项要求,最终为投资人负责,真正的多面手,余下的人都可以安安心心的专职工作了(设计模式里面的单一职责,不会掺杂着人情世故,不会和任何人绑定)。
这个就有点像中介者模式,专门出一个人来搞定所有杂事,导演有除了本质之外的事搞不定,找监制,剧本涉及到审核,找监制,演员刷大牌的,找监制。反正一句话,哪方面的人才没有,监制都可以去海选,只要觉得合适,合同一签(代码注入,进行绑定的时候),大家都是来这里的合同工,严格按合同办事,效率就很高。
设计模式里面有最少知道,接口隔离,这些思想都需要引入三方,隔离变化,将代码最大程度解耦。
应对变化
在拍摄电影的时候,一定要平衡好艺术和商业,这是任何导演都要面对的问题,不同类型的片子,有不同类型的人群,甚至不同日期,都需要不同的片子,人的口味是变化的。怎么去应对这些变化,设计模式又弄出了装饰模式,防止子类暴增,桥接模式,应对有多种变化的。这两种模式应该是要用到的。
不同类型的片子,这个是一个庞大的体系,就需要用到继承,如商业片,文艺片,在各自下面又有分类,商业片有职场片,宫斗,武侠等,这是一个影片体系。在设计模式里面有一个里氏替换,有基类的地方,子类都可以替换。
以上就是举一个很形象的例子来简略说说设计模式,设计模式远不止这些了,总共有23种,可以去看看《Head First设计模式》。
各项功能的配套,权限,日志,监控,配置,调试测试认证
一个系统除了有良好得代码架构设计之外,应该还要有相应得配套机制,如权限,日志,监控,调试认证,配置等,
权限
客户端和服务端分离,就是大多数得C/S架构,在客户端调用得时候,在服务端统一设置对客户端得权限检测,避免掉那些权限不足的调用,增强系统的安全性
测试和认证
客户端和服务端分离的C/S架构也有很多的好处,想要检验服务端的功能实现是否有问题,就直接调用接口,输入测试数据,看看得到的结果,就可以做白盒测试。如果源码发给了第三方,看看别人有没有修改你的接口功能,也可以测试接口,看看是否满足接口的标准,来做认证。
日志
统一整个日志打印,特别是在容易出现错误的地方添加完整的打印,以免在出现异常时能够及时发现并修改。
配置
有很多功能时需要修改大量参数的,这个就可以做成参数配置的方式,可以通过修改配置参数来达到调整的目的,亦可以将这部分功能开放给用户配置,尽量减少修改代码。
监控
对于一些可能出现性能问题的,或者稳定性问题的地方,那么就应当适当加一些监控,以便实时掌控整个框架地运行状态,性能是否良好,是否稳定等。
代码重构
当我们接到一个需求,开始地时候都时很小很少,但是在此基础上添加了很多地小需求,然后又配套着一堆小功能,随着时间地推移,功能越做越多,需求越做越大,代码就会变得越来越臃肿,各种问题会慢慢地出现,那么我们是不是就需要开始重构了。
简单得重构可以从变量拆分,函数拆分,类体系优化等入手,这个在《重构-改善既有代码得设计》这本书里有很详细得说明。理解好原有得代码功能,做好重构记录,重构完之后要做严格得测试。
对于大量重复的代码调用,我们应当考虑一下做统一的封装,简化代码的书写。对于那些有共通的属性的统一代码,我们是不是可以考虑做成组件,或者组件体系,用装饰器模式来做组件体系,更加简化代码。防止编写大量重复的代码,继承大量的类。
对于客户端大量的无序的调用,我们应当考虑一下服务端应该要做一定的防御设计,以免过多的调用导致系统的稳定性,性能方面的体验下降。
对于那种经常变化需求,或者常常需要扩展,那么应对变化,设计模式就要考虑多使用了。