缺少这个思维,你很难成为大牛

我们常常遇到这样的场景,在项目A中开发了很多功能,这时在项目B中遇到要开发和A中类似的功能,这时会怎么做?

很多人会不假思索的找到原来A项目中相关的功能代码,拷贝到B项目中,然后修改。 
几乎人人都这么做过,这样做有些什么问题呢?

首先,由于代码常常混杂在一起,并不那么容易拷贝。特别是当代码不是自己写的,往往会一下拷贝了过多的无关内容到新项目中。 
其次,需要把功能中的非通用部分(只适用于项目A的部分)修改掉,以与当前项目匹配,这对于复杂的功能,特别是通用逻辑与专用逻辑交错在一起的模块,也是挺费时的工作。

这种代码难以复用的问题屡见不鲜,我们于是希望能把这些功能做成开箱即用的的可复用模块。 
所谓可复用模块,就是当你在别的项目中使用它时,不需要改它内部的任何东西,只需要按接口文档配置它的选项、实现它要求的接口,再调用它的接口就可以了。 
对照这个标准,看看你写的东西有多少可以复用的呢?

那么是不是从一开始设计或编码的时候,就要以可复用的标准来设计函数或模块呢? 
如果你认为这样才是优秀的设计和良好的习惯,那么你一定要小心别犯过度设计的毛病。 
很多人读过设计模式,于是在项目中看到大量的factory/strategy/adapter等等。多数设计模式都是为了降低模块耦合,其实就是在设计可复用模块。 
然而模块是不是需要复用,更多的是看经验,而不是去套用模式。滥用模式必然导致过度设计。 
多数时候,一开始并不知道哪些东西需要复用,很多自以为是的拆分往往都被证明并不必要。 
过度设计和设计不足都会导致代码难以理解,而且过度设计还会在一开始耗费更多的时间。 
因此,不必试图从一开始就设计可复用模块,因为你常常并不知道哪些是真正需要被复用的。

运用重构思维,按需创建可复用模块

那么如何生成可复用的模块呢? 
很简单,当你发现某个功能和已有的一个功能很像,却又不完全一样,这就给你制作可复用模块的信号了;如果这种情况再出现第三次第四次,显然这块功能就是有复用价值的。 
这就是常见的lazy-loading机制的翻版,不在一开始设计可复用模块,而是在需要复用的时刻,通过重构代码来复用。 
而到底是在出现第一次复用机会时就重构,还是第二次或第三次时才重构,就得取决于重构的代价与获得好处的权衡了。如果难度不太大,风险也可控,我们一般都是鼓励尽早重构的。

很多人天天做类似的编码,工作输出却还是线性的,为什么做第二次第三次仍然要花费差不多的精力? 
古话说的好,磨刀不误砍柴功,我们需要的是立即停止重复,立即开始重构。

重构并不都是大刀阔斧的把项目大卸八块,更常见的是微重构,往往就是当你发现一块逻辑可以复用,就把它独立成了一个函数,然后函数多了,把它们分类放在一起,便于理解和修改,就形成了可复用模块的雏形。 
优秀的程序员会如同有洁癖般憎恨重复,遇到重复就会忍不住做代码优化,这其实就是以重构思维,动态地渐近地演化的过程,而这一过程往往就会生产出很多有用的轮子。

如何面对重构的挑战

前面谈到复用的好处,以及鼓励通过重构而不是一开始庞大的设计来演化出可复用模块。作为代价,它不仅带来额外的开发时间,而且对软件质量的挑战是极大的,尤其是使用无须编译的弱类型的动态语言做开发。 
简单的修改个函数名或加个参数,都需要人工仔细检查所有项目代码,即使这样也不牢靠。

只能靠测试用例来覆盖受影响的代码,所以,创建回归测试,覆盖主要业务逻辑,是敢于对项目进行重构的前提条件。

可复用模块的演化之路

说了这么多,下面举个例子吧,看看可复用模块是怎样演化出来的。 
就以javascript来举例吧。假设一开始进入某H5应用,需要尝试自动登录到系统,面条式代码如下:

function initApp()
{
    // 初始化环境,20行
    // 尝试自动登录,50行
    // 登录成功做些操作,10行
    // 自动登录不成功跳转到登录页
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这时并没有什么问题,运行很好。当写到登录页时,发现登录成功后也要做某些操作,和initApp里那些代码类似,为避免重复,这时就做个微重构,抽出一个共用函数handleLogin来:

function handleLogin(data)
{
    // 登录成功做些操作,10行
}

function initApp()
{
    ...
    handleLogin(data);
    ...
}

function login()
{
    callSvr("login", function (data) {
        handleLogin(data);
        // ...
    });
    // ...
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

之后,又遇到一个逻辑,如果调用后台接口返回“未登录”错误时,可以先尝试一下自动登录,如果能登录成功就重新调用接口。 
做这个逻辑时,想到“自动登录”代码在initApp里面有,那么也借机把它抽出来吧:

// ----- login相关函数 -------
function handleLogin(data)
{
    // 登录成功做些操作,10行
}
function tryAutoLogin()
{
    // 尝试自动登录,50行
}
// ---------------------------

function initApp()
{
    ...
    tryAutoLogin();
    handleLogin(data);
    ...
}

function callSvr(action)
{
    $.ajax({
        ...
        success: function (data) {
            if (errorCode == E_NOLOGIN) {
                if (tryAutoLogin() == OK) {
                    // 重新调用接口
                }
                else {
                    // 跳转登录页
                }
            }
        }
    });
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35

像上面,我们把login相关函数放到一起,就已经初步形成了一个模块。这时,另一个项目也需要类似功能时,复制代码就变的容易了。 
实际代码中这个模块会有很多函数以及相关变量,有些会被外部使用,有些只在模块内部使用,还有些会依赖外部的变量或函数。 
使用者需要了解模块内大量的细节才能集成到新的项目中。

这为构建可复用模块提供了一个好机会。 
为了隐藏复杂的模块内细节,我们把模块提供的公共接口,以及模块依赖的接口明确列举出来,形成文档,把其它内部实现隐藏起来。

这样,tryAutoLogin和handleLogin就成为模块的公共接口,会被外面调用;而模块需要外部提供一个callSvr函数,用于调用服务端接口,有了它模块才能工作。 
我们用@module, @fn, @var这些标记直接在代码里通过注释来描述公共接口,便于使用者查看文档,也可以通过工具生成漂亮的文档。

/**
@module Login

(模块文档)
登录模块。使用示例:

    $.extends(Login.options, {
        callSvr: function (ac) {
            $.ajax(...)
        }
    });

    Login.tryAutoLogin();
    Login.handleLogin(data);

*/
var Login = new LoginModule();
function LoginModule()
{
    var self = this;

    /**
    @var Login.options = {callSvr(ac)}
    */
    self.options = {
        callSvr: function (ac) {
            throw new Exception("callSvr is NOT implemented");
        }
    };

    /**
    @fn Login.handleLogin(data)

    (公共函数文档)
    */
    self.handleLogin = handleLogin;
    function handleLogin(data)
    {
        // 登录成功做些操作,10行
    }

    /**
    @fn Login.tryAutoLogin()

    */
    self.tryAutoLogin = tryAutoLogin;
    function tryAutoLogin()
    {
        // 尝试自动登录,50行
    }

    // 某内部函数,使用者无须关注,外部也无法调用
    function privateFn()
    {
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

上面就是一个典型的代码演化过程,通过不断遇到重复逻辑时重构代码,产生了模块,而当其它项目中又需要这块功能时,索性把这个模块独立出来,生成了一个有着良好封装和使用文档的可复用模块。 
如果条件允许,把它贡献到开源社区是个不错的选择。

之后,如果遇到两个项目中处理不一样的情况,比如在项目B中handleLogin需要额外再做某操作,而项目A中不要此操作,常常在options中添加配置项就行了,比如

self.options = {
    ...,
    doA: false
};

function handleLogin(data)
{
    // 登录成功做些操作,10if (self.options.doA) {
        doA();
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值