// 上一篇:局部化(localization)

// 下一篇:最近最少使用(LRU)


基于语言提供的基本控制结构,更好地组织和表达程序,需要良好的控制结构。

前情回顾

上一周,我们谈到了分支卫语句状态机局部化。它们是相互补充协作的关系,并且我们都只使用函数就达到了说明的目的。为什么仅仅使用函数来说明呢?回到第一篇提到的分枝/叶子,可以看到,无论上层代码怎样组织,在对象层面做了怎样的抽象封装,最终是要在函数这个级别实现具体的调用动作的。在对象层面的组织,有很多书和文章,但是无论是老手还是新手,都有许多程序员不能良好的组织函数内代码,使得其具有更好的可读/可维护。我们看一个小例子:

function initApp(){
let appMetaPath = path.join(__dirname,'appmeta.json');
let packageDir = path.join(__dirname, 'test/packages');
let appMeta = JSON.parse(fs.readFileSync(appMetaPath)); let uid = appMeta.uid;
let token = appMeta.token; let app = new Application();
let appInfo = JSON.parse(fs.readFileSync(path.join(__dirname,'test/packages/app.json')));
let packageConfigPath = path.join(packageDir,'calculator/config.json');
let packageConfig = JSON.parse(fs.readFileSync(packageConfigPath));
app.init(appInfo, function(err,info){
// ... 用到相关信息
}); }

很短的一个叶子代码,简单调整顺序改进下:

function initApp(){
let appMetaPath = path.join(__dirname,'appmeta.json');
let appMeta = JSON.parse(fs.readFileSync(appMetaPath)); let appConfigPath = path.join(__dirname,'test/packages/app.json');
let appInfo = JSON.parse(fs.readFileSync(appConfigPath)); let pacakgeConfigPath = path.join(__dirname, 'test/packages/calculator/config.json');
let packageConfig = JSON.parse(fs.readFileSync(pacakgeConfigPath)); let app = new Application();
app.init(appInfo,function(err,info){
//... 用到相关信息
});
}

其实不复杂,代码如果读起来是顺序结构就更好读,也更利于维护。

典型代码

  • 例子1
int write(char* buffer){
thisLock.lock();
//....
if(err1){
thisLock.unlock();
return RESULT.ERROR1;
}
//...
if(err2){
thisLock.unlock();
return RESULT.ERROR2;
}
//...
thisLock.unlock();
return RESULT.SUCCESS;
}
  • 例子2
function findItemByBindedGroupID(groupID, onComplete){
var mysql = require('mysql');
var pool = mysql.createPool(...); pool.getConnection(function(err, connection) {
connection.query('SELECT * FROM group WHERE ?', {groupID: groupID},
function (error, groups) {
if (error||groups.length===0){
connection.release();
return onComplete(1);
} let group=groups[0];
let itemID = group.bindedItemID;
connection.query('SELECT * FROM item WHERE ?', {itemID:itemID},
function(error, items){
connection.release();
if (error||items.length===0){
onComplete(1);
}else{
onComplete(0, items[0]);
}
});
});
});
}

结构分析

这是一段典型的打开资源读/写关闭资源的操作,问题在于当你要写很多这样的代码时,代码就会显得繁琐,在每个返回分支都要记得关闭资源也是一个很容易被忘记的动作,于是就会出现典型的资源泄露。在不同语言里,如何对资源做自动释放,在日常开发中出现的频率很高。不同语言有不同的做法。例如C++语言里的Lock代码,有多种方式改进它:

  • goto方式
int write(char* buffer){
thisLock.lock();
int ret = 0; //....
if(err){
ret = RESULT.ERROR1;
goto quit;
}
//...
if(err2){
ret = RESULT.ERROR2;
goto quit;
}
//...
ret = RESULT.SUCCESS; quit:
thisLock.unlock();
return ret;
}
  • do-while方式
int write(char* buffer){
thisLock.lock(); do{
//...
if(err){
ret = RESULT.ERROR1;
break;
}
//...
if(err2){
ret = RESULT.ERROR2;
break;
}
// ...
ret = RESULT.SUCCESS;
}while(true); thisLock.unlock();
}

可以看到,很多人限制了不能用goto,但do-while并不比goto少多少代码,还多了一层嵌套。最后,就是用C++的对象方式(RAII)解决:

  • RAII方式
class AutoLock{
AutoLock(lock){
this.m_lock = lock;
this.m_lock.lock();
}
~AutoLock(){
this.m_lock.unlock();
}
}
int write(char* buffer){
AutoLock lock(thisLock); //对象析构的时候自动unlock
//...
if(err){
return RESULT.ERROR1;
}
//....
if(err2){
return RESULT.ERROR2;
}
//...
return RESULT.SUCCESS;
}

我们再看下JavaScript的例子,同样JavaScript里也有多种做法,例如使用之前提到过的状态机方式,不过此次我们希望只用基本的控制结构和函数来封装,同时不改变代码的通常读法。

class Connection{
constructor(){
this.m_database = ...
} open(onComplete) {
let self = this;
self.m_database.getPOOL().getConnection(function(err, conn) {
if (err) {
self.m_conn = null;
return onComplete(err);
}
self.m_conn = conn;
onComplete(0);
});
} close() {
let self = this;
if(self.m_conn){
self.m_conn.release();
self.m_conn = null;
}
} executeQuery(action, sql, values, onComplete) {
let self = this;
let r = self.m_conn.query(sql, values, function (err, results) {
if (err) {
log.error(`do ${action} failed, err:${err}`);
onComplete(err, results);
} else {
log.info(`do ${action} success.`);
onComplete(0, results);
}
});
log.info(`action: ${action}, sql: ${r.sql}`);
} usingQuery(action, onComplete){
let self = this; /**对onComplete做一层wrapper,调用之前先关闭连接*/
let hasClose = false;
let theComplete = function (err, results) {
if (hasClose) {
return;
}
self.close();
onComplete(err, results);
}; /**只打开连接一次*/
let hasOpen = false;
let open = function(callback){
if (hasOpen){
return callback(0);
}
hasOpen = true;
self.open(function(err){
callback(err);
});
}; /**
* 返回一个查询上下文,包含:
* - 带自动关闭连接的onComplete
* - 在首次查询时自动open的query接口,
*/
let context = {
onComplete: theComplete,
query: function(sql, values, callback){
open(function(err){
if(err){
callback(err);
}else{
self.executeQuery(action, sql, values, callback);
}
});
}
}; return context;
}
}

于是,我们可以这样使用,现在只需关心查询本身的逻辑即可:

function findItemByBindedGroupID(groupID, onComplete){
let conn = new Connection();
let context = conn. usingQuery('findItemByBindedGroupID', onComplete); context.query('SELECT * FROM group WHERE ?', {groupID: groupID},
function (error, groups) {
if (error||groups.length===0){
return context.onComplete(1);
} let group=groups[0];
let itemID = group.bindedItemID;
context.query('SELECT * FROM item WHERE ?', {itemID:itemID},
function(error, items){
if (error||items.length===0){
return context.onComplete(1);
} context.onComplete(0, items[0]);
});
});
}

语义分析

打开资源,对资源做一些操作,关闭资源。看上去很简单的一组动作,如果遇到中间有多次操作,每次操作都可能有错误,每次错误的时候都需要释放资源,即容易忘记,又繁琐。

我们只要能识别函数退出路径里必经之地,对必经之地做一层浅浅的hook,就能实现资源自动释放动作。

例如,在C++里,函数超出作用域之后,一定会调用栈上对象的析构函数,语言提供了这种保证,我们就可以在这个函数退出必经之地做自动释放。

例如,在JavaScript里,异步调用的时候,我们可以依赖于一个前置假设:“异步接口一定通过回掉函数退出”,那么我们就可以对这个异步回调的必经之地做一个浅封装,达到资源的自动释放目的,同时又不会剧烈改变代码的直观逻辑。

例如,C#语言内置提供了using语句,使得在using作用域退出的时候能自动释放实现了IDispose接口的对象。

控制结构(5) 必经之地(using)的更多相关文章

  1. 控制结构(5): 必经之地(using)

    // 上一篇:局部化(localization) // 下一篇:最近最少使用(LRU) 基于语言提供的基本控制结构,更好地组织和表达程序,需要良好的控制结构. 前情回顾 上一周,我们谈到了分支/卫语句 ...

  2. feilong's blog | 目录

    每次把新博客的链接分享到技术群里,我常常会附带一句:蚂蚁搬家.事实上也确实如此,坚持1篇1篇的把自己做过.思考过.阅读过.使用过的技术和教育相关的知识.方法.随笔.索引记录下来,并持续去改进它们,希望 ...

  3. 控制结构(6) 最近最少使用(LRU)

    // 上一篇:必经之地(using) // 下一篇:程序计数器(PC) 基于语言提供的基本控制结构,更好地组织和表达程序,需要良好的控制结构. There are only two hard thin ...

  4. 控制结构(4) 局部化(localization)

    // 上一篇:状态机(state machine) // 下一篇:必经之地(using) 基于语言提供的基本控制结构,更好地组织和表达程序,需要良好的控制结构. 前情回顾 上一次,我们说到状态机结构( ...

  5. 控制结构(4): 局部化(localization)

    // 上一篇:状态机(state machine) // 下一篇:必经之地(using) 基于语言提供的基本控制结构,更好地组织和表达程序,需要良好的控制结构. 前情回顾 上一次,我们说到状态机结构( ...

  6. 控制结构(6): 最近最少使用(LRU)

    // 上一篇:必经之地(using) // 下一篇:程序计数器(PC) 基于语言提供的基本控制结构,更好地组织和表达程序,需要良好的控制结构. There are only two hard thin ...

  7. PHP语法(三):控制结构(For循环/If/Switch/While)

    相关链接: PHP语法(一):基础和变量 PHP语法(二):数据类型.运算符和函数 PHP语法(三):控制结构(For循环/If/Switch/While) 本文我来总结几个PHP常用的控制结构,先来 ...

  8. Python 30分钟入门——数据类型 and 控制结构

    Python是一门脚本语言,我也久闻大名,但正真系统的接触学习是在去年(2013)年底到今年(2014)年初的时候.不得不说的是Python的官方文档相当齐全,如果你是在Windows上学习Pytho ...

  9. 学习scala03 控制结构

    scala拥有非常宽松的控制结构. if与while scala中的if和while几乎和java中的结构一模一样. //if语句 val a= ){ println(“”) }else{ print ...

随机推荐

  1. 关于MySQL buffer pool的预读机制

    预读机制 两种预读算法 1.线性预读 2.随机预读 对预读的监控 一.预读机制 InnoDB在I/O的优化上有个比较重要的特性为预读,预读请求是一个i/o请求,它会异步地在缓冲池中预先回迁多个页面,预 ...

  2. FastJson将json解析成含有泛型对象,内部泛型对象再次解析出错的解决办法(Android)

    折腾小半天的问题,这里先感谢一下深圳的小伙子,远程帮我搞,虽然也没有搞出来==========FUCK 声明:Android开发下发生此异常,Java开发下并不会有这个问题 异常重现 简单说一下抛出异 ...

  3. UWP中使用Telerik UI For UWP

    国际惯例先上一张图吧: 首先去下载Telerik UI For UWP的SDK,安装好之后在项目中添加SDK的引用 建议使用引用SDK,如果引用dll的话需要引用的dll较多不太方便 引用好之后以一个 ...

  4. iOS Notification – 远程通知

    本文讲解iOS的远程通知的基本使用,主要包括远程通知的类型,处理远程通知的场景,以及远程通知相关证书的配置等等. 一.APNs简介 APNs是苹果公司提供的远程通知的服务器,当App处于后台或者没有运 ...

  5. 在word表格里打勾和打叉

    打勾:在单元格里输入R,再将其字体设置为:Wingdings 2. 打叉:在单元格里输入T,再将其字体设置为:Wingdings 2.

  6. python_cookie

    cookielib是一个自动处理cookies的模块 ## 核心类 CookieJar:是cookie的集合,可以包含很多Cookie类,是我们的主要操作对象 FileCookieJar:继承自Coo ...

  7. js获取ip地址,操作系统,浏览器版本等信息,可兼容

    这次呢,说一下使用js获取用户电脑的ip信息,刚开始只是想获取用户ip,后来就顺带着获取了操作系统和浏览器信息. 先说下获取用户ip地址,包括像ipv4,ipv6,掩码等内容,但是大部分都要根据浏览器 ...

  8. 在Linux环境下搭建Tomcat+mysql+jdk环境

    按照下面的步骤一步一步来搭建tomcat+jdk+mysql环境.   [Linux环境]------我搭建的是64位centos版本的linux系统 1.下载并安装一个VMware workstat ...

  9. Android 7.0 Power 按键处理流程

    Android 7.0  Power 按键处理流程 Power按键的处理逻辑由PhoneWindowManager来完成,本文只关注PhoneWindowManager中与Power键相关的内容,其他 ...

  10. Partition函数

    快排中核心的方法应该算是Partition函数了,它的作用就是将整个数组分成小于基准值的左边,和大于基准值的右边. 普通的Partition函数是这样的: public static int part ...