分类 技术志 下的文章

见识思维导图

不瞒大家说在未听说思维导图之前,我自己就常常在废旧打印纸背面画那种’枝繁叶茂’的、很发散的需要费力才能将其中的信息串起来的’思考图’(我那时就这么叫它)。我喜欢思考,但却常常因为没有及时把自己思考出来的好想法记录下来而遗憾,因为有些灵感是’来去匆匆’的,你可以在不经意的0.1秒得到这个灵感,可是之后你再花1个小时’冥思苦想’也迸发不出来这样的想法了。现在我的’思考图’有了’大名’了,叫’思维导图’,不过伴随之而来的还有如何去画’思维导图’。习惯了’乱画思维’的我反倒感到一些不适应,不过这些理论毕竟是经过科学证明的,值得学习。而且按照一定科学的方法画思维导图可以帮助挖掘思维潜力。

在这里不想说思维导图有什么好处,如果你感兴趣大可到网上去’搜’。我则是因为喜欢这种表达思维的方法才主动去学如何去画思维导图。我的第一感觉我的思维过程类似一个’图的遍历’过程,而且是一个’先广遍历’和’先深遍历’的结合,往往在’深入的同时’又迸发出’广度’的想法。

画思维导图最好的工具组合就是一张白纸和一支铅笔(虽说思维导图建议最好五颜六色,但是我天生对颜色不敏感,所以只喜欢黑与白)或一面白板和一枝黑色彩笔。最原始的也是最快捷、最有效的。这也是我最喜欢的方式。不知道又没有人和我一样往往用着最先进的工具却’无米下锅’,如有的作家就喜欢白纸黑字的写作方式,他认为只有这样做才有源源不断的创作灵感,相反面对计算机屏幕和枯燥的键盘,太容易引起’思维麻木’了。在计算机上画思维导图不乏’制作精良’的工具,其中的典型代表有’开源的FreeMind和收费的MindManager,两者各有千秋,试试便知。

很多关于思维导图的教材都提到环境和工具的重要,在什么图景世界、静、音乐的环境等,使用色彩丰富的彩色笔等。我想对于不习惯于发散思维的人这些可能很重要,但是如果一旦习惯了这种思维方式,工具因素可能就不那么重要了。不过环境因素仍然起作用,而且感觉可能不同的人适应不同的环境。我自己就感觉我的思维在一个相对自我的环境下比较容易迸发灵感,我自己亲身体会:在笔直的长马路上疾步行走会让我感到思维加速,常常会有很多新想法萌生,而其他人则未必喜欢这样。

关于思维导图的一些教程主要以练习为主,因为本身思维导图就是一种发散的东西,非要用’规律化’的理论去约束它只能起到适得其反的作用。但是总’撒网(发散自己的思维)’也要学会’收网(整理和总结自己的思维)’, 否则就会没有收获,白想了。’撒网’需要练习,同样’收网’也许要训练,特别是当你所画的思维导图需要和别人分享时,比如一份工作计划,你不能把你的’纯思维’让大家去猜测,你需要整理出一份规整的报告,而你的思维导图则有助于你形成这样一份报告。

按照一些理论上所说,思维导图有四大要素:关键词(右上分枝)、层次(右下分枝)、图像(左上分枝)和联想(左下分枝),还有至关重要的一点要牢记:’始终围绕Central Topic’。至于如何去理解这些,这里不想多说,因为那不是我的成果。思维导图目前广泛应用在各种场合,如制定计划、产品分析和设计、会议记录等等,我想一切大脑能用得上场合的地方,思维导图都应该用得上^_^。

总之,我画思维导图的原则只有四个字:’随心所欲’^_^。

C语言也重构

所谓的重构是这样的一个过程:在不改变代码外在行为的前提下,对代码做出修改,以改进程序的内部结构。重构是一种有纪律的、经过训练的、有条不紊的程序整理方法,可以将整理过程中不小心引入错误的机率降到最低,本质上说,重构就是[在代码写好之后改进它的设计]。Martin Fowler

重构,一种改善代码’体质’的方法’。侯捷

重构是上进程序员每天的进行式。是一项工程而不是靠着天份挥洒的艺术。– 侯捷

如果有人说’重构’仅仅限于Java语言或者其他面向对象的语言,那他就大错特错了。按照Martin Fowler的诠释,’重构’就是一种代码整理,而且是在保持接口功能不变的一种代码整理,这样的话每种语言都可以’重构’,也许’重构’在你平时的工作学习中已经不知不觉地发生了,只是你还没有认识到那是’重构’罢了。自从从大学毕业后我就一直在’把弄’C语言,对C语言也算是’情有独钟’了,所以自然而然的就开始探索和积累C语言的重构方法。这里个人能力毕竟有限,并不能穷尽所有方法。

按照我的理解,重构[注1]应该是’三位一体’的,即"前提(或叫保证) + 理论 + 工具",前提(或保证)指的是单元测试,回顾一下本篇首Martin Fowler对重构的诠释 — ‘在不改变代码行为的前提下’,而如何保证代码经过’大修’后,能保证其外在行为不变呢,只有测试;理论则是指重构的来龙去脉、理论和方法了,这也是本篇所主要关注的;至于工具,那就是重构’催化剂’,保证对大型项目重构的效率,遗憾的是至今业界仍没有一款成熟的支持C语言的重构工具,而与此相反针对像Java语言这样的面向对象语言,各厂家则是’众星捧月’,工具也不断推陈出新,其中缘由也不难理解。

Internet上讨论C重构的资料少之又少,个人认为比较全面的是Alejandra Garrido的’CRefactory project‘,说这份资料比较全面是因为Alejandra Garrido将之分类清楚(但也许不是所有人都喜欢这样分类的,我就是其中之一),但是这份资料不够详细,关于每条item的说明仅仅几行,而且没有例子。不过不管怎样,这份资料都是值得看的,它也许会给你提供一些思路上的提示。

关于重构理论最权威的莫过于Martin Fowler那本’Refactoring – Improving the Design of Existing Code’了,虽然Martin青睐Java,但这并不太多影响其他’语种’的小工们从这本大师的书中汲取’思想的精华’。

在这里我仅针对C代码中的’Bad Smell’给出自己的重构想法,之所以没有像Martin书中那样给出分类,也许是因为自己对重构的理解还不那么到位的缘故^_^,也许以后会给出像模像样、合情合理的分类,好了,下面我们就一起开始嗅嗅代码的味道吧。

1、Duplicated Code
重复代码自古有之,历史悠久自不必说了。Martin书中将之放在Bad Smell的第一位,足见其’Bad Enough’^_^。’Duplicated Code’将给你带来诸多不便,诸如维护更多的代码、代码修正后可能的不一致性等,这里列举三种情况来具体说明How smelly the ‘Duplicated Code’ is!

(1) 太多的128、256…
[e.g.]
void func1(…) {
 char path[128];
 …
}

void func2(…) {
 char path[256];
 …
}

[solution & notice]
#define  MAX_PATH 256

void func1(…) {
 char path[MAX_PATH];
 …
}

void func2(…) {
 char path[MAX_PATH];
 …
}

避免在程序中多次出现magic number,特别是当多个magic number重复时,坏的味道就散发出来了,我们可以定义一些常量符号还替换这些magic number。

(2) 一山难容二虎
[e.g.1]
void func1() {
        time_t          in_tm;
        time_t          out_tm;
        char            in_str[100];
        char            out_str[100];
        struct tm       *ptm = NULL;

        memset(in_str, 0, sizeof(in_str));
        memset(out_str, 0, sizeof(out_str));

        in_tm   = time(NULL);
        out_tm  = time(NULL);

        ptm = localtime((time_t*)&(in_tm));
        sprintf(in_str, "%d%02d%02d%02d%02d%02d",
                ptm->tm_year+1900,
                ptm->tm_mon +1,
                ptm->tm_mday,
                ptm->tm_hour,
                ptm->tm_min,
                ptm->tm_sec);

        ptm = localtime((time_t*)&(out_tm));
        sprintf(out_str, "%d%02d%02d%02d%02d%02d",
                ptm->tm_year+1900,
                ptm->tm_mon +1,
                ptm->tm_mday,
                ptm->tm_hour,
                ptm->tm_min,
                ptm->tm_sec);
}

[e.g.2]
//in test.c
void func1() {
        time_t          in_tm;
        char            in_str[100];
        struct tm       *ptm = NULL;

        memset(in_str, 0, sizeof(in_str));

        in_tm   = time(NULL);

        ptm = localtime((time_t*)&(in_tm));
        sprintf(in_str, "%d%02d%02d%02d%02d%02d",
                ptm->tm_year+1900,
                ptm->tm_mon +1,
                ptm->tm_mday,
                ptm->tm_hour,
                ptm->tm_min,
                ptm->tm_sec);
}

// in test.c, too
void func2() {
        time_t          out_tm;
        char            out_str[100];
        struct tm       *ptm = NULL;

        memset(out_str, 0, sizeof(out_str));

        out_tm   = time(NULL);

        ptm = localtime((time_t*)&(out_tm));
        sprintf(out_str, "%d%02d%02d%02d%02d%02d",
                ptm->tm_year+1900,
                ptm->tm_mon +1,
                ptm->tm_mday,
                ptm->tm_hour,
                ptm->tm_min,
                ptm->tm_sec);
}

[solution & notice]
在一个函数中存在功能相同的’代码群落’(如e.g.1)或者是在同一个源文件中在不同的函数实现中存在功能相同的’代码群落’(如e.g.2),这是典型的’Duplicated Code’,解决方法很简单 — 析出函数(Extract Function)或者析出宏(Extract Macro, 这可是C语言的特色哟)。如解决上面两个例子中的问题我们便可以这么做:
a) 析出函数time2str
void time2str(char *s, const time_t t) {
        time_t          tmp     = t;
        struct tm       *ptm    = NULL;

        ptm = localtime((time_t*)&(tmp));

        sprintf(s, "%d%02d%02d%02d%02d%02d",
     
           ptm->tm_year+1900,
                ptm->tm_mon +1,
                ptm->tm_mday,
                ptm->tm_hour,
                ptm->tm_min,
                ptm->tm_sec);
}

b) 析出宏TIME_2_STR
#define TIME_2_STR(s, t)     do { \
                struct tm *ptm = localtime((time_t*)&t); \
                sprintf(s, "%d%02d%02d%02d%02d%02d", \
                        ptm->tm_year+1900, \
                        ptm->tm_mon +1, \
                        ptm->tm_mday, \
                        ptm->tm_hour, \
                        ptm->tm_min,  \
                        ptm->tm_sec); \
        } while(0)

用time2str或TIME_2_STR替换掉那些重复的代码后,就会发现原来世界可以这么简洁!

(3) 你中有我,我中有你
[e.g.]
/* foo1.c in project foo */
static int foo1_mkdir(const char *path) {
 …
}

/* foo2.c in project foo */
static int foo2_mkdir(const char *path) {
 …
}

[solution & notice]
/* foo_common.c */
static int foo_mkdir(const char *path) {
 …
}

/* foo1.c */
#include "foo_common.h"

/* foo2.c */
#include "foo_common.h"

在较大的工程中,多个程序员并行工作,不可避免的在各自的代码中实现了相同功能的一些方法,这就是’Bad Smell’,这样程序员要维护两份实现相同功能的代码,而且当fix bug时很有可能漏掉更新另一份。这种的散落在不同源文件中重复代码可以采用’Pull Up Method’方法将这个功能接口提取到一个公共的文件中去实现,而原先的两个文件只需包含接口头文件即可。

2、Long Function/Procedure/Method
事实证明越长的Function,理解起来越难,维护难度也越大。拆分函数意味着要把代码中一个功能相对独立的’代码群落’独立出去,这样其实带来了很多好处。首先你又给了自己一次重新阐述代码意图的机会,你可以起一个恰当的名字来表述这段被独立出去的’代码群落’的’工作性质’;其次一旦这段’代码群落’被独立出去了,它就可以被其他函数逻辑所共享;再者独立出去的’代码群落’与原先的代码的关系照比之前已经不那么紧密,所以一旦’代码群落’里发生什么’叛乱’,其不会蔓延到原先的代码中。在Martin书中曾专门引用Kent Beck的一段关于’Indirection and Refactoring’的论述来说明’拆分’的利弊,而在整个重构的理论中’拆分’的确也占据着’主流’地位。这里就不举例说明了,拆分也是我们平时经常做的事情,不是吗!:)

3、Long Parameter List
长长的参数列表虽然不是致命的,但却是最最不顺眼的。想想当你去调用一个接口时发现它有10个参数,为完成这个调用你需要来来回回看N遍那个接口的原型还不见得传入的实参全部符合原型要求。实际情况就是这样,而且遭遇这种情况的时候还是很多的。怎么办?重构!如何做?看下面例子。

[e.g]
int x_add_book(const char *title,
                const char *author,
                const char *isbn,
                const char *pub_date,
                int pages,
                int price,
                const char *publisher,
                const char *pub_addr,
                const char *pub_homepage);

[solution & notice]
哇,添加一本书要这么参数亚,晕倒!让它少些参数吧 — 合并!这个例子中的参数之间有相关性,可以尝试组合一下。组合完可能是这样的:
struct pub_info {
        char            name[MAX_NAME_LEN];
        char            addr[MAX_STR_LEN];
        char            homepage[MAX_STR_LEN];
};

struct book_info {
        char            title[MAX_NAME_LEN];
        char            author[MAX_NAME_LEN];
        char            isbn[MAX_STR_LEN];
        char            pub_date[MAX_STR_LEN];
        int             pages;
        int             price;
        struct pub_info pub;
};

int x_add_book(const struct book_info *book);
重构完了。不知道大家发现这样修改的好处没有,比如现在我想在添加一本书时,再加上这本书的位置信息,如放在那个书架上,这样如果我们使用e.g.中的做法,我们需要修改接口;而使用重构之后的方式我们只需要修改book_info结构体,x_add_book接口则无需修改!这也是增加一个间接层的好处之一!

那么是不是所有的长参数列表的函数都可以这么重构呢?有些时候参数列表中的参数都是毫无联系的,如果硬要把他们’组合’在一起反倒不顺眼,这也符合’强扭的瓜不甜’的道理:) 这时候可以考虑某个参数是否可以调用其他函数得到该值,如果可以的话,我们就可以在删除该参数而改在函数体中调用其他接口获取这个值,这种方法类似于Martin书中的’Replace Parameter with Method’;尽量不要用全局变量来代替某一参数,过多的全局变量也是Bad Smell!

如果还不行的话,没办法,保留这一长参数列表的接口,千万不要为了’哨子’而付出太多的代价!

4、Comments
我们不是反对注释,注释是很好的增强代码可读性和可维护性的手段,而且注释有时会帮助你发现代码中的味道!想象一下我们的注释一般会出现在什么样的位置呢?如下图:
/*
 * 注释1
 */
return_type func_name(params list…) {
 /*
  * 注释2
  */
 …

 /*
  * 注释3
  */
 …’代码群落’…
 
}

一般我们在’注释1′的位置描述函数的功能,有时候我们发现这段注释描述比函数名更加清楚,这也是一个不好的味道,这时我们可以使用’Rename Method’来重构之;在注释2的位置我们可能会描述一些’precondition’,这里不妨用’Introduce Assertion’来替换掉这些注释;注释3会详尽介绍’代码群落’干的是什么活,仔细考量,看看能否将这段’代码群落’用’Extract Method’方法析出,而这段注释恰恰可以为析出后的函数作命名之用。另外’Rename Method’和’Introduce Assertion’两种重构技巧较简单,望文即可生义,自然无需多说!

5、其他
Alejandra Garrido的’CRefactory project’中的一些条目还是对我很有启发性的,这里仅列出我认为必要的。
(1) contract variable scope
我将之理解为:’约束变量的作用范围’:一个在特定作用范围内定义的变量仅仅被该范围内的一个inner scope中的代码所使用,我们大可把这个变量声明移到这个inner scope中。

(2) Replace expression with variable
这其实也是个’Duplicated Code’问题,只是重复的’代码群落’是一个expression,如 (i < 10) ? 1 : 0条件表达式等,这样的话我们把它赋值给一个变量,然后在以后使用这个变量即可。

(3) Convert global variable into parameter
对于这个条目我们可以举个例子:
[e.g]
int g_cnt = 1;

void fun1() {
 if (g_cnt > 10) {
  g_cnt = 1;
 } else {
  g_cnt++;
 }
}

void func2() { /> g_cnt += 3;
}

void func() {
 if (g_cnt == 2) {
  func1();
 } else {
  func2();
 }
}

[solution & notice]
在这个例子中func、func1和func2都对全局变量g_cnt进行了访问,并且都有权对g_cnt进行修改,而实际func1和func2的设计意图可能就是func的辅助函数,负责对func需要操作的变量进行修改,而不一定都需要访问全局变量,这时我们可以这样修改:
int g_cnt = 1;

void fun1(int *cnt) {
 if (*cnt > 10) {
  (*cnt) = 1;
 } else {
  (*cnt)++;
 }
}

void func2(int *cnt) {
 (*cnt) += 3;
}

void func(int *cnt) {
 if (*cnt == 2) {
  func1(cnt);
 } else {
  func2(cnt);
 }
}

func(&g_cnt);

感觉这个条目的目的就是尽量减少有权访问全局变量的访问点个数。

OK! C语言并不复杂,所以重构的技巧、代码的smell也不如面向对象语言那么多,呵呵,这里我就想到这么多了!最后别忘了重构仅仅是一种有计划有规则的’代码整理’,它并不是那种不可逾越的技术高峰。在积累了一段时间的重构’经验’后,重构完全可能会成为你开发过程中的一个不可缺少的习惯了,而养成习惯的必经之路就是持续不断的练习、积累和总结!让自己从此走上’C语言重构之路’吧。

[注1]
关于重构重要性、重构时机等话题在Martin书中有详尽说明!

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats