作者归档:admin

与代码的相处之道

与代码的相处之道 — 读《编程人生》一二章有感

最近在阅读《编程人生》,看了作者对Jamie Zawinski(Lisp黑客、XEmacs开发者、Netscape浏览器和Mozilla核心开发者)和Brad Fitzpatrick(80后程序员、LiveJournal和memcached开发者,Google员工)的采访,除了感慨作者采访的准备充分外,对于牛人的一些观点有一些共鸣,也有一些观点不太认同,尽信书不如无书,对于牛人也是如此吧。
一个程序员,大多数时间都在写代码,调试代码,阅读代码、评论他人的代码……林林总总,都是与代码相处。那如何与代码相处呢?首先我们需要认知到代码是什么,代码是一种语言,一种我们与计算机沟通的语言。我们需要通过代码告诉那个有点傻傻的计算机需要做点什么。

对于一些新增加的内容,我们可以从如下的方面与代码相处。

  • 第一,了解写这些代码是为了什么,或者说你的需求是什么,列出所有的功能点,估算你实现这些功能需要多长的时间,在估算的过程中,你不能将自己的工作时间完全算在开发中,大概50%差不多,毕竟我们不会一整天的写,还会有思考,发呆,梦游,开各种网页,看各种新闻,被各种im打断……
  • 第二,为你的新的功能搭出基本的架子,比如写好空类或空的函数,后面的工作就是填充这些空的地方了。当然在写这些空类或空函数的过程中,把注释写清楚,这是一个理顺自己代码结构和业务逻辑的过程。
  • 第三,这一步当然就是填充之前留空的内容了,在整个过程中你可能会写入一些调试的代码,没关系,先放着,因为在这个过程中你可能会再次需要这些调试的代码。
  • 第四,验证需求,重构代码。每个设计方案在开始的时候都是完美的,包括就在刚才你的代码结构设计。只是在细化需求时,可能部分细节没有考虑到,此时你就需要重构刚才的设计和代码,以适应这些变化。
  • 第五,打完收工,这是一个收尾的工作。此时,我们需要再次阅读你刚才写好的代码,清除这个过程中出现的调试代码,确认整个过程是否已经全部完成了所需要的功能点。
  • 第六,提交

如果你不是重新开始写一个功能,而是在别人(或你自己之前)的代码上修改并增加新的功能,此时当如何相处呢?

  • 第一,了解过去,分析影响范围,列出checklist。一段旧的代码,你最好先了解他的过去,看他与哪些其它模块有耦合,或者有哪些内容依赖于他,如果修改了这些内容,对其他模块是否有影响,如果要修改的是对外的接口,是否需要适配这些接口?
  • 第二,修改代码。此时你可能会重构之前的代码,那么在重构的过程中需要把握重构的度,不要轻易的将重构变成重写。毕竟之前的一些细节可能是你没有考虑到的。如果修改的代码中有对外的接口,此时可能需要保留这些接口,或者修改所有的调用这个接口的地方,这个就要权衡两者的机会成本了 。
  • 第三,根据checklist验证所修改的内容是否正确,验证是否影响了其它相关的内容
  • 第四,提交

如果是不是重新开始写一个功能,而是由于bug或其它原因需要调试代码呢?此时当如何相处?

  • 第一,了解它。我们需要先通读代码,审阅整个代码的结构,确认在整体的方向上没有问题。
  • 第二,在关键点打印信息,当然,你也可以使用调试工具与之交互,但是打印语句是一种绿化无污染的调试方式,不依赖于外部的环境,不过你需要通过第一步,先对代码有一定的了解才行。
  • 第三,确认问题,修改。此时可能需重复上面的修改流程
  • 第四,提交

前面三个都是通过代码告诉计算机怎么做,在某些时候我们也需要计算机告诉我们他做了什么。因此,在这里我们也需要通过代码告诉计算机如何将信息反馈给我们。一般来说,需要告诉他如何将整个代码执行过程中发生了什么告诉我们,比如日志,比如执行过程中的信息打印。

如果你是想优化一些代码,此时当如何相处呢?

  • 第一,确认需要优化的内容。可能你需要优化时间,也可能是需要优化空间,确认优化的内容。嗯,Knuth说过:过早优化是万恶之源。
  • 第二,找出瓶颈。优化并不是优化所有的地方,而是要找到优化的关键点,比如循环调用许多次的函数,或者花费时间或空间较多的地方等等。
  • 第三,寻找优化方案。可能你需要的仅仅是调换一下代码的执行顺序,或者释放执行过程中的某些变量所占的内存,当然也有可能需要优化整个数据结构,或者换台机器也是一个不错的主意。
  • 第四,验证优化结果。
  • 第五,提交

优化是一个持续的过程,在写代码的过程中能够随手优化的就优化吧,比如局部变量的使用等。

代码是我们与计算机沟通的语言,也是我们与其它程序员沟通的语言,因此除了让计算机了解代码外,我们也需要为其它程序员(或一段时间后的自己)了解这些代码做点什么,除了团队内部构建良好的代码规范,统一代码风格这些老生常谈(老生常谈其实挺好)外,书中的牛人提出关于注释的观点也非常认同:关于注释我们得写点不一目了然的东西。至少不要出现类似于循环结束,某某值加1的注释,一般来说注释应该是写点与业务相关的东西,至少在第一眼看代码无法看出来的东西。虽然现在敏捷提倡“代码即文档”,不要注释,提高代码的可读性,但是一些注释还是必要的。

锁机制概述

本文主要回答如下问题:

  • 什么是锁机制?
  • 锁机制的作用是什么?
  • 锁机制的类型有哪几种?

关键字:并发、并发控制、乐观锁、悲观锁

在我们常见的程序设计、操作系统和数据库等领域,并发是非常棘手的问题之一。而并发在操作系统、数据库等领域,最终还是会体现在软件开发(代码实现)上。并发的主要纠结点在于对共享资源的处理,虽然现在有事务来处理并发,但是这只是在一定程序上缓解了并发的问题,并没有彻底解决,比如跨事务的并发。

在软件开发过程中,并发控制是确保及时纠正由并发操作导致的错误的一种机制。并发控制主要采用时间戳、乐观并发控制和悲观并发控制等技术手段来实现。而今天我们要说的锁机制主要是指后面的两种技术手段:乐观并发控制和悲观并发控制,他们分别对应乐观锁和悲观锁。锁机制是管理对共享资源的并发访问机制。

乐观锁并不是纯粹意义上的锁,它可以理解为冲突检测,属于事后的操作,其中一种实现是依赖数据版本记录机制。在数据源增加一个版本标记,当请求方读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据源中对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据源当前版本号,则予以更新,否则认为是过期数据。比如某个共享数据被并发访问修改,在各个请求方获取了数据后,请求方都会被分配一个相同版本,如果有一个请求方提交了修改,则将原始数据版本增加1,则其它请求方再次提交修改时,由于提交的版本小于当前版本则显示冲突。除了基于数据版本的控制外,还有包含对更新时间的控制,对不同字段的对比控制等。

悲观锁可以理解为冲突避免,属于事前的操作,即不让并发修改的操作发生,减少并发出现。当一个用户访问一个共享对象时锁住它,即先获得锁,此时其它用户无法访问此对象,当对共享对象的操作完成以后要为被它封锁的对象解锁,此时其它对象才能访问此共享对象。如果此时一个用户一直占着一个资源不放,其它所有用户都只能永远等待,此时可能需要引入其它机制来防止这种情况的发生。

悲观锁减少了并发的程序,而乐观锁在一定程度上会更加自由一些,其在获取资源时是不受限制的,仅在提交的时候才会有限制。当需要在悲观锁与乐观锁之间抉择时,可以考虑如下两个点:

  • 冲突的频率 如果冲突少,通常可以选择乐观锁,这样可以获得更多的并发性,但是此时也需要考虑冲突的严重性,如果系统不能容忍冲突的出现,则需要考虑牺牲并发性,使用悲观锁
  • 冲突的严重性 如果冲突所产生的后果比较严重或者为用户不能容忍,需要使用悲观锁

无论是乐观锁还是悲观锁都存在其优点和缺点,只使用某一种机制都会产生其它问题,可以考虑将这两种锁放一起使用,或者提供两种机制,供用户选择,默认使用乐观锁。

Yii框架的组件行为管理机制和Mix-in

Yii框架的组件行为管理机制和Mix-in

本文包括以下内容:

  • Yii框架的组件行为管理机制介绍
  • Ruby、PHP5.4和Mix-in

在Yii框架的官网,我们可以看到关于Behaviors & events的介绍: Behaviors are simply a way of adding methods to an object.

我们看官网上的使用示例:

class SomeClass extends CBehavior{
    public function add($x, $y) { return $x + $y; }
}

class TestComponent extends CComponent {
}

$test_comp = new TestComponent();
$test_comp->attachbehavior('blah', new SomeClass);
$test_comp->add(2, 5);

在TestComponent类的对象创建的后,我们可以通过调用attachbehavior给对象添加新的方法。

通过其源码(在base/CComponent.php)可以知道它是通过在组件类内部以私有变量的方式存储这些添加的方法所在的对象, 通过魔术方法__call,当调用一个未定义的方法时需要调用__call方法的特性,遍历所有通过attachbehavior方法添加进来的对象, 并判断此对象是否禁用并且此对象是否存在需要调用的方法,如果存在则调用。

此种实现方式存在如下一些问题:

  • 如果多个对象存在相同的方法,则程序调用时永远会调用第一次添加进去的方法
  • 如果我们只是需要某个对象中的某个方法,但是在存储上需要将整个对象添加到列表中

也许你会觉得这些都是一些如果,都是一些假设,可能不会出现,这有些像众所周知的goto语句问题,如果用得好,这是一个利器,如果用得不好,可能会给你带来痛苦。 Yii框架中的这种机制实现运行时的方法绑定,虽然类的属性和实例参数仍然归属于其它类和对象。

在官方说明中也提到了这是一种类似于ruby语言的实现方式,如果我们用Ruby实现上面的方法该如何写呢?如下:

module SomeClass
    def add(x, y)
        return x + y
    end
end

class TestComponet
    include SomeClass
end

test = TestComponet.new
puts test.add(10, 20)

非常简单的实现了类的重用,我们知道在PHP中,接口是可以多继承的,但是接口只是形态上的多继承,是一种对于类实现的约束,是一种规格。 如果要实现这种类的重用,Ruby受Lisp的影响引入了Mix-in,在PHP5.4引入了trait关键字。

在Ruby中Mix-in的关键字是module,而在即将推出的PHP5.4,其对应的关键字是trait; 如果要复用这个定义的类,在Ruby中使用include,而在PHP5.4中使用use。如下PHP代码:

<?PHP
trait SomeClass {
    public function add($x, $y) {
        return $x + $y;
    }
}

class TestComponent {
    use SomeClass;
}

$obj = new TestComponent();
echo $obj->add(10, 20);

对于Mix-in类,有两个约束:

  • 不能单独生成实例
  • 不能继承其它的普通类

如果实例这个类程序执行会显示:

Fatal error: Cannot instantiate trait SomeClass...

如果从其它普通类继承会显示:

Fatal error: A trait (SomeClass) cannot extend a class ...

如果要查找这两个约束的源码实现,可以直接在源码中搜索Cannot instantiate trait和cannot extend a class。 从搜索可以看出:

  • 不能单独生成实例的检测是在new关键字的中间代码执行时执行的,在Zend/zend_vm_execture.h文件
  • 不能继承的约束是在编译成中间代码的过程中实现的,在 Zend/zend_compile.c文件

在面向对象编程语言,Mix-in是一个提供了一些被用于继承或在子类中重用的功能的类,它类似于一种多继承, 但是实际上它是一种中小粒度的代码复用单元,而不直接用于实例化。 虽然这不是一种专业的方式进行功能复用,这在实现多继承的同时,在一定程序上避免了多继承的明显问题。 一如Yii的组件行为管理机制,也是另外一种取巧的Mix-in实现。