程序员的上帝视角(5)——失控

编码规范中有一条:不要使用魔数。很多程序员对这种规范嗤之以鼻,认为是最没有技术含量的代码规范。那么,如果是否可以从另外一个角度来理解魔数呢?

魔数引起的失控

系统中判断魔数,并且拒绝魔数的本质到底是什么呢?一个系统中,魔数之所以称为魔数,并非是因为它是一个固定的常数。而是因为一个常数,没有任何业务意义。我们可以用漂移、无意义等词语来形容魔数。但是,真正让魔数成为禁忌的,是因为魔数会造成失控。

正是因为魔术的无具体的业务意义。而我们的程序总是时刻与业务逻辑息息相关。当无任何业务意义的魔数出现在业务代码中,你几乎不可能找到有多少地方使用了相同的魔数来表示相同的业务值。这是典型的失控。

我们再来思考为什么会失控,如果将魔数当做被使用者,使用者看到了这个值本身,本来使用者需要面对的是业务概念,而不是业务概念的真实内容,相当于说,魔数将细节和真实内容暴露给了使用者。

将魔数抽象为一个常量符号,使用者调用符号,是解决之道。所以,解决失控的一个方案,就是隐藏细节和真实内容。只对外暴露业务符号。

重载引起的失控

你可能会对重载为什么会引起失控有所疑问。但是,方法级的重载带来方便的同时,往往容易走向另外一个极端——若干方法名相同,而参数列表不同的方法带来的迷惑。

public void saveUser2Es(String field) {
    this.saveUser2Es(field, 0L, 0L);
}

public void saveUser2Es(String field, Long epId, Long appId) {
    this.saveUser2Es(field, epId, appId, 0L);
}

public void saveUser2Es(String field, Long epId, Long appId, Long crowdOrTagId) {
    this.saveUser2Es(field, epId, appId, crowdOrTagId, 0L);
}

public void saveUser2Es(String field, Long epId, Long appId, Long crowdOrTagId, Long userNum) {
    // 保存用户数据到ES中
}

类似上面的代码,在我们的系统中并不少见。甚至很多程序员还略显骄傲的宣称利用了重载的特性。但是,对于维护这段代码的程序员,如果工作在没有强大IDE支持的工作环境下,甚至很难确定每个方法中调用的saveUsers2Es是哪个。对于调用者来说,看到这一堆眼花缭乱的参数,同样很难决定使用哪一个。

这是否也是重载使用场景的失控呢?对于这种失控的场景,我们是否将其收拢为一个参数对象更加有效呢?例如:

public void saveUser2Es(User user) {
    // 保存用户数据到ES中
}

对于维护者来说,只需要关注User中的属性即可。对于调用者,只需要组装好User对象,直接调用这个确定的saveUser2Es方法即可。

同样的,如果有新的参数,只需要为User类增加一个新的属性,然后再在saveUser2Es方法增加对新属性的处理逻辑即可。而按照重载方式处理,不得不增加新的方法,还得纠结到底这个新的方法中,调用哪个现有的同名方法。

这种失控场景的根源在于,总是尝试将参数拆解为编程语言内置的数据类型。误认为这样可能会产生最好的复用性。其实不然,事实上,一旦出现这种场景,较多的参数之间并非毫无关联,更大的可能在于,它们可能从属于某一个特定业务对象——如同上例中的User。

事实上,有的编程语言已经明确禁止了方法的重载,例如,Golang。权衡之后,Go语言的设计者觉得重载特性弊大于利,因此,Go语言很明确的抛弃了重载这一特性。

公共权限带来的失控

一个对象一旦被全局暴露,可能会带来其状态的失控。全局暴露,通常意味着可以被多处代码或多个线程同时访问。更危险的是,如果该对象提供了设置属性的public方法,那么,该对象的状态随时会被修改。而这种不稳定状态并不是开发者的本意。

如下是一个动态代理的实现代码:

public class DynaProxyHello implements InvocationHandler{

    private Object target;//目标对象
    /**
     * 通过反射来实例化目标对象
     * @param object
     * @return
     */
    public Object bind(Object object){
        this.target = object;
        return Proxy.newProxyInstance(this.target.getClass().getClassLoader(), this.target.getClass().getInterfaces(), this);
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        Object result = null;
        Logger.start();//添加额外的方法
        //通过反射机制来运行目标对象的方法
        result = method.invoke(this.target, args);
        Logger.end();
        return result;
    }

}

这几乎可以被认定为一段自作聪明的代码。问题出现在bind()函数。在该函数中

this.target = object;

可将被代理对象target利用bind进行设置。然后将Proxy.newProxyInstance()的操作封装在bind方法中。这样做的目的当然是为了简化代码操作。

但是,很明显,被代理对象target可以通过bind()方法被不停修改。而且每次返回的代理对象都不同。单纯从代码执行的角度来说,一旦DynaProxyHello被暴露在全局,target将随时可能被重新绑定。并且每次生成代理对象都会不同。第一次调动bind()方法获得代理对象的使用者,根本不会意识到程序为什么会突然出错。

最常规写法应该是:

public DynaProxyHello(Object object) {
    this.target = object;
}

对于每次new一个DynaProxyHello而言,该对象每次都是一个全新的对象。而屏蔽public bind()方法,防止其他人无意识的修改已绑定的被代理对象。

总结失控

我们总是从现象抽象本质。无论是魔数、方法重载,还是不恰当的公共权限,几乎很难想到一个词语来概括这背后的逻辑。或许,只有“失控”一词,才能让我们对日常如此不起眼的代码也饱含敬畏之心。

从另外一个角度来说,有时候,【了解】的程度与【失控】的程度成反比。这意味着,你越是了解对方的实现细节,就越可能造成使用时失控的局面。如果你对对方一无所知,就越是能够盲从对方,对对方提供的功能的认知,纯粹到一无所知——不知道实现细节,也无关实现结果的对错,是否合理等等。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值