程序质量、程序BUG、程序的维护和扩展、多个程序员协同,一个好的程序需要平衡这一切。一个漂亮但是不解决问题的程序是毫无价值的,但一个有用却不可维护的程序也是一个定时炸弹。
为了把程序写好,你必须要理解该语言,还得培养对语言和设计的品位。要达到这个目标,需要不断地练习--不仅仅是写代码,还要维护代码和阅读优秀的代码。
这条路没有捷径,但有路标。
写能维护的Perl
可维护性的高低就是指理解、修改程序的难易程度。比如现在写的代码,6个月后再来看它,如果要修复一个BUG或者添加一个新功能,你需要花多长时间呢?这个就是可维护性的体现。
可维护性不是指你是否要去看内置语法和库函数,也不是指一个没有编程经验的人是否能读懂你程序。它着眼的是****一个称职合格的程序员是不是很容易就理解你的程序、修改你的程序,修复一个bug或增加一个功能时他会遇到哪些问题。****
(网上有很多关于Perl维护性的谣言)
要编写出能维护的软件,需要你具有解决实际问题的经验。开发过程中有这些原则:
- 删除重复
重复的代码(或相似的代码)容易有坑---当你修复这一段代码中的错误时,你会不会修复那一段呢;更新这一段时,会不会更新另一段呢?精心设计的系统中通常不会出现重复。通过使用函数,模块,对象,角色来提取出重复的代码形成单独的组件,这样就能精确的控制问题域。好的设计有时甚至可以通过删除代码来增加功能。
- 起个好名字
编写代码就是在讲一个故事。你为变量,函数,模块和类起的每一个名字都可以表明(或混淆)你的意图。仔细选择名字,如果你不知道取什么样的名字好,那么再考虑下你的设计或者再了解下问题的细节。
- 别耍小聪明
简洁的代码是好的,只要它能体现出来代码的意图。小聪明和华而不实的招数不利于展示你的意图。在Perl中,你总是可以选择更明显的的方式来解决问题。有些问题可能确实需要一些特别的解决方法,当遇到这种情况时,封装起来,对外提供简单的接口,并且提供文档(详细记录了你聪明才智的文档)。
- 保持简单
如果不考虑其他的,简单的程序总是更容易维护。简单意味着你知道什么是最重要的东西并且只做这个。
有时候你需要功能强大,可靠的代码;有时候只需要一个单行程序,****简单意味着你知道自己需要的什么。****错误检测、安全性验证这些都是必须要做的,简单的代码也可以使用高级特性,简单的代码可以使用CPAN模块,简单的代码也需要理解原理。简单的代码能有效的解决问题,并且不做多余的工作。
写地道的Perl
Perl借用了很多其他语言的特性,Perl允许你随心所欲的写代码。经常的,有C背景的程序员写成C风格,java背景的写出java风格,其实高效的Perl程序员会写地道的Perl。这里有几个小提示:
- 了解社区智慧
Perl程序员经常会就技巧和习惯进行激烈的辩论,同时也乐于分享他们的经验(不仅仅是在CPAN上)。你可以从中了解到不同的风格和想法,并从中获益。
- 遵循社区规范
Perl有很多工具帮助我们解决着各种各样的问题,比如静态代码分析(Perl::Critic),格式化(Perl::Tidy),私人分发系统(CPAN::Mini, Carton, Pinto)。利用CPAN来学习,按照CPAN的规范来书写文档、打包代码、测试代码和发布代码。
- 阅读代码
加入一个邮件列表,如 Perl Beginners(http://learn.perl.org/faq/beginners.html)浏览 PerlMonks (http:
//perlmonks.org/),积极参与社区。阅读代码,回答问题,这些都是很好的学习机会。
CPAN developers,,Perl mongers,还有各种邮件列表,里面包含有来之不易的经验和各种解决方案。与他们交谈,阅读他们的代码,问他们问题,向他们学习,同时,也让他们有机会来学习你。
写高效的Perl
编写能维护的代码意味着要去设计能维护的代码。好的设计源自好的习惯:
- 写可测试的代码
编写有效的测试用例其实也在训练你编写有效代码的能力。完善的测试会给你自信--自信你的代码总能正常运行。
- 模块化
抽象出边界、进行封装、找出组件之间的正确接口、起个合适名字并且将它们放在合适的地方。模块化迫使你去思考模型和抽象代码,更好的理解各个部分是如何组合起来的。修改不合理的组合方式,优化组合方式。
- 遵循合理的编码标准
优秀的编码指导会涉及到错误处理、安全性、封装、API设计、布局和可维护性相关的内容。优秀的编码指导有利于开发人员的互相沟通和理解。你用代码来解决问题,所以让你的代码清楚的表达含义--让你的代码会说话。
- 利用好CPAN
Perl程序员喜欢解决问题、共享解决方案。CPAN就是力量的聚集器,你可以利用它来开阔思路,解决问题,无需付费。如果你发现了BUG可以上报,能力有余也可自行修复。人人为我,我为人人。
异常
好的程序员总会考虑到异常情况的发生:应该存在的文件却不见了;超大容量的硬盘永远不会写满,但是写满了;从来没断过的网络,断了;牢不可破的数据库突然崩溃了。
异常总会发生,健壮的软件必须要能处理这些情况。如果能恢复,当然最好;如果不能恢复,起码也得记录必要的信息。
抛出异常
假设你要写一个日志文件,如果你无法打开这个文件,那么就发生错误,可以用die来抛出异常:
sub open_log_file
{
my $name = shift;
open my $fh, '>>', $name
or die "Can't open logging file '$name': $!";
return $fh;
}
die()会设置全局变量$@并且立即退出当前函数,并且不返回任何值。这个就是抛出异常,抛出的异常会留在调用堆栈顶端,等待某个东西来取;如果没有东西来取,程序就会报错并退出。
异常处理使用动态范围的局部变量。
捕获异常
有时候在异常发生时报错并退出程序是很实用的,但有时候却不希望退出程序,而是继续程序并做一些相应处理。比如错误日志的文件写满了,我们并不希望程序因此而退出,同时也希望能发个短信提醒下管理员。
使用eval操作符的程序块形式来捕获异常:
# log file may not open
my $fh = eval { open_log_file( 'monkeytown.log' ) };
如果文件打开成功,$fh就会包含一个文件句柄;如果打开文件失败,$fh就是未定义的,程序将会继续运行。程序块作为eval的参数,若发生异常,异常将会被eval捕获。
异常处理是一个迟钝的工具,它可以捕获程序块范围内的所有异常。要了解发生的是哪种异常,那就检查$@的值。一定要先本地化,因为$@是一个全局变量。
local $@;
# log file may not open
my $fh = eval { open_log_file( 'monkeytown.log' ) };
# caught exception
if (my $exception = $@) { ... }
将$@的值马上复制出来,避免后面的代码改变全局变量$@的值,因为你不知道还有哪些别的地方会设置该值。
$@通常包含了描述异常的字符串。
if (my $exception = $@)
{
die $exception unless $exception =~ /^Can't open logging/;
$fh = log_to_syslog();
}
再次调用die()抛出异常。这里使用了一个正则表达式来匹配异常内容,不过这个是不可靠的,因为异常的内容并不是固定的。你也可增加额外的信息,如行号、文件名或者是其他调试信息。
CPAN模块Exception::Class提供面向对象的用法:
package Zoo::Exceptions
{
use Exception::Class
'Zoo::AnimalEscaped',
'Zoo::HandlerEscaped';
}
sub cage_open
{
my $self = shift;
Zoo::AnimalEscaped->throw
unless $self->contains_animal;
...
}
sub breakroom_open
{
my $self = shift;
Zoo::HandlerEscaped->throw
unless $self->contains_handler;
...
}
注意事项
抛出异常很简单,捕获异常也很简单,但是$@有一些微妙的地方:
- 在动态范围本地化$@再使用,否则可能改变它(全局变量)
- $@可能包含了一个对象,这个对象在布尔语境下返回假值
- 信号处理程序可能改变$@的值
- 对象的析构函数可能会调用eval并改变$@值
Modern Perl已经修复了其中的一些问题。这些现象极其少见,但极难调试。建议使用Try::Tiny模块来更好的进行异常处理:
use Try::Tiny;
my $fh = try { open_log_file( 'monkeytown.log' ) }
catch { log_exception( $_ ) };
用try 代替了 eval,try捕获了异常后,catch语句块就会执行。异常会放在$_里面。
内置异常
Perl有自己的内置异常,如语法错误,这种异常会在编译时就会抛出;其他的异常可以在运行时进行处理。比如:
- 在锁定的哈希中使用了不被允许的键。
- bless一个不存在的引用
- 在一个无效的调用者上调用方法
- 调用不存在的方法
- 使用了被污染的值(污染模式)
- 修改只读的值
- 对引用执行了无效的操作
当然你也可以捕获autodie(编译指示)产生的异常,也可以动态地将警告提升为异常。
编译指示
大多数的Perl模块是提供新功能或者定义类,但是有些模块比如strict,warnings则只是影响语言自身的行为(它们不提供新功能),这类模块就是编译指示。按照惯例,编译指示使用小写以区别于其他模块。
编译指示和有效范围
编译指示在调用它的词法范围内生效,就像词法变量一样。
{
# $lexical不可见,strict未生效
{
use strict;
my $lexical = 'available here';
# $lexical可见,strict启用生效
}
# $lexical不可见,strict未生效
}
很像词法变量吧。
# 作用域持续整个文件
use strict;
{
# 嵌套作用域,strict生效
my $inner = 'another lexical';
}
使用编译指示
编译指示的使用和其他模块一样(本来就是模块),也接受参数:
# 要求变量声明,禁止裸字
use strict qw( subs vars );
# 使用2014的规则
use Modern::Perl '2014';
如果你想禁用特性使用关键字no:
use Modern::Perl;
# 或 use strict;
{
no strict 'refs';
# 该词法范围内不启用refs的限制,这样就能操作符号表了
}
有用的编译指示
这里有一些有用的编译指示:
- strict
让编译器启用以下检查:符号引用,裸字使用,变量声明。
- warnings
对不规范的行为发出警告。
- utf8
告诉解析器使用UTF-8编码来理解当前文件的源代码。
- autodie
启用对系统调用和内部函数的自动错误检查。
- constant
允许你使用编译时常量。
- vars
允许你声明包全局变量。
- feature
允许你启用新特性。如
use 5.14; #启用5.14版本的新特性,启用strict编译指示
use feature ':5.14'; #跟上面一样的
- less
演示如何写一个编译指示。
可查看并了解更多:perldoc perlpragma。
还有一些CPAN上的编译指示,可能还没有被广泛使用,有兴趣的你可以自己去体验。
- autovivification
- indirect
- autobox
- perl5i