perl-patterns

perl-patterns

熱門

現代 Perl 5.36+ 慣用語法、最佳實務與慣例,用於建構穩健、可維護的 Perl 應用程式。

23萬星標
3.5萬分支
更新於 2026/7/17
SKILL.md
readonlyread-only
name
perl-patterns
description

現代 Perl 5.36+ 慣用語法、最佳實務與慣例,用於建構穩健、可維護的 Perl 應用程式。

現代 Perl 開發模式

慣用的 Perl 5.36+ 模式與最佳實務,用於建構穩健、可維護的應用程式。

何時啟用

  • 撰寫新的 Perl 程式碼或模組
  • 審查 Perl 程式碼是否符合慣例
  • 重構舊版 Perl 至現代標準
  • 設計 Perl 模組架構
  • 將 5.36 之前的程式碼遷移至現代 Perl

運作方式

將這些模式作為傾向現代 Perl 5.36+ 預設值的偏誤:簽名、明確模組、聚焦的錯誤處理以及可測試的邊界。以下範例可作為起點複製,然後根據實際應用程式、依賴堆疊與部署模型進行調整。

核心原則

1. 使用 v5.36 編譯指示

單一 use v5.36 取代舊的樣板程式碼,並啟用 strict、warnings 與子程式簽名。

# 好:現代前置宣告
use v5.36;

sub greet($name) {
    say "Hello, $name!";
}

# 壞:舊式樣板
use strict;
use warnings;
use feature 'say', 'signatures';
no warnings 'experimental::signatures';

sub greet {
    my ($name) = @_;
    say "Hello, $name!";
}

2. 子程式簽名

使用簽名以獲得清晰度與自動參數數量檢查。

use v5.36;

# 好:帶預設值的簽名
sub connect_db($host, $port = 5432, $timeout = 30) {
    # $host 為必要,其他有預設值
    return DBI->connect("dbi:Pg:host=$host;port=$port", undef, undef, {
        RaiseError => 1,
        PrintError => 0,
    });
}

# 好:可變參數的 slurpy 參數
sub log_message($level, @details) {
    say "[$level] " . join(' ', @details);
}

# 壞:手動參數解包
sub connect_db {
    my ($host, $port, $timeout) = @_;
    $port    //= 5432;
    $timeout //= 30;
    # ...
}

3. 上下文敏感性

理解標量 vs 列表上下文 — 這是 Perl 的核心概念。

use v5.36;

my @items = (1, 2, 3, 4, 5);

my @copy  = @items;            # 列表上下文:所有元素
my $count = @items;            # 標量上下文:計數 (5)
say "Items: " . scalar @items; # 強制標量上下文

4. 後綴解參考

使用後綴解參考語法以提高巢狀結構的可讀性。

use v5.36;

my $data = {
    users => [
        { name => 'Alice', roles => ['admin', 'user'] },
        { name => 'Bob',   roles => ['user'] },
    ],
};

# 好:後綴解參考
my @users = $data->{users}->@*;
my @roles = $data->{users}[0]{roles}->@*;
my %first = $data->{users}[0]->%*;

# 壞:前綴解參考(在鏈式中較難閱讀)
my @users = @{ $data->{users} };
my @roles = @{ $data->{users}[0]{roles} };

5. isa 運算子 (5.32+)

中綴型別檢查 — 取代 blessed($o) && $o->isa('X')

use v5.36;
if ($obj isa 'My::Class') { $obj->do_something }

錯誤處理

eval/die 模式

use v5.36;

sub parse_config($path) {
    my $content = eval { path($path)->slurp_utf8 };
    die "Config error: $@" if $@;
    return decode_json($content);
}

Try::Tiny(可靠的例外處理)

use v5.36;
use Try::Tiny;

sub fetch_user($id) {
    my $user = try {
        $db->resultset('User')->find($id)
            // die "User $id not found\n";
    }
    catch {
        warn "Failed to fetch user $id: $_";
        undef;
    };
    return $user;
}

原生 try/catch (5.40+)

use v5.40;

sub divide($x, $y) {
    try {
        die "Division by zero" if $y == 0;
        return $x / $y;
    }
    catch ($e) {
        warn "Error: $e";
        return;
    }
}

使用 Moo 的現代 OO

偏好使用 Moo 以獲得輕量、現代的 OO。僅在需要其元協議時才使用 Moose。

# 好:Moo 類別
package User;
use Moo;
use Types::Standard qw(Str Int ArrayRef);
use namespace::autoclean;

has name  => (is => 'ro', isa => Str, required => 1);
has email => (is => 'ro', isa => Str, required => 1);
has age   => (is => 'ro', isa => Int, default  => sub { 0 });
has roles => (is => 'ro', isa => ArrayRef[Str], default => sub { [] });

sub is_admin($self) {
    return grep { $_ eq 'admin' } $self->roles->@*;
}

sub greet($self) {
    return "Hello, I'm " . $self->name;
}

1;

# 使用方式
my $user = User->new(
    name  => 'Alice',
    email => 'alice@example.com',
    roles => ['admin', 'user'],
);

# 壞:受祝福的 hashref(無驗證、無存取子)
package User;
sub new {
    my ($class, %args) = @_;
    return bless \%args, $class;
}
sub name { return $_[0]->{name} }
1;

Moo 角色

package Role::Serializable;
use Moo::Role;
use JSON::MaybeXS qw(encode_json);
requires 'TO_HASH';
sub to_json($self) { encode_json($self->TO_HASH) }
1;

package User;
use Moo;
with 'Role::Serializable';
has name  => (is => 'ro', required => 1);
has email => (is => 'ro', required => 1);
sub TO_HASH($self) { { name => $self->name, email => $self->email } }
1;

原生 class 關鍵字 (5.38+, Corinna)

use v5.38;
use feature 'class';
no warnings 'experimental::class';

class Point {
    field $x :param;
    field $y :param;
    method magnitude() { sqrt($x**2 + $y**2) }
}

my $p = Point->new(x => 3, y => 4);
say $p->magnitude;  # 5

正規表達式

命名捕獲與 /x 旗標

use v5.36;

# 好:使用 /x 的命名捕獲以提高可讀性
my $log_re = qr{
    ^ (?<timestamp> \d{4}-\d{2}-\d{2} \s \d{2}:\d{2}:\d{2} )
    \s+ \[ (?<level> \w+ ) \]
    \s+ (?<message> .+ ) $
}x;

if ($line =~ $log_re) {
    say "Time: $+{timestamp}, Level: $+{level}";
    say "Message: $+{message}";
}

# 壞:位置捕獲(難以維護)
if ($line =~ /^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+\[(\w+)\]\s+(.+)$/) {
    say "Time: $1, Level: $2";
}

預編譯模式

use v5.36;

# 好:編譯一次,多次使用
my $email_re = qr/^[A-Za-z0-9._%+-]+\@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;

sub validate_emails(@emails) {
    return grep { $_ =~ $email_re } @emails;
}

資料結構

參考與安全深層存取

use v5.36;

# 雜湊與陣列參考
my $config = {
    database => {
        host => 'localhost',
        port => 5432,
        options => ['utf8', 'sslmode=require'],
    },
};

# 安全深層存取(若任一層級缺失則回傳 undef)
my $port = $config->{database}{port};           # 5432
my $missing = $config->{cache}{host};           # undef,無錯誤

# 雜湊切片
my %subset;
@subset{qw(host port)} = @{$config->{database}}{qw(host port)};

# 陣列切片
my @first_two = $config->{database}{options}->@[0, 1];

# 多變數 for 迴圈(5.36 實驗性,5.40 穩定)
use feature 'for_list';
no warnings 'experimental::for_list';
for my ($key, $val) (%$config) {
    say "$key => $val";
}

檔案 I/O

三引數開啟

use v5.36;

# 好:使用 autodie 的三引數開啟(核心模組,消除 'or die')
use autodie;

sub read_file($path) {
    open my $fh, '<:encoding(UTF-8)', $path;
    local $/;
    my $content = <$fh>;
    close $fh;
    return $content;
}

# 壞:二引數開啟(shell 注入風險,參見 perl-security)
open FH, $path;            # 絕對不要這樣做
open FH, "< $path";        # 仍然不好 — 使用者資料在模式字串中

使用 Path::Tiny 進行檔案操作

use v5.36;
use Path::Tiny;

my $file = path('config', 'app.json');
my $content = $file->slurp_utf8;
$file->spew_utf8($new_content);

# 迭代目錄
for my $child (path('src')->children(qr/\.pl$/)) {
    say $child->basename;
}

模組組織

標準專案結構

MyApp/
├── lib/
│   └── MyApp/
│       ├── App.pm           # 主模組
│       ├── Config.pm        # 設定
│       ├── DB.pm            # 資料庫層
│       └── Util.pm          # 工具
├── bin/
│   └── myapp                # 進入點腳本
├── t/
│   ├── 00-load.t            # 編譯測試
│   ├── unit/                # 單元測試
│   └── integration/         # 整合測試
├── cpanfile                 # 依賴
├── Makefile.PL              # 建置系統
└── .perlcriticrc            # 程式碼檢查設定

Exporter 模式

package MyApp::Util;
use v5.36;
use Exporter 'import';

our @EXPORT_OK   = qw(trim);
our %EXPORT_TAGS = (all => \@EXPORT_OK);

sub trim($str) { $str =~ s/^\s+|\s+$//gr }

1;

工具

perltidy 設定 (.perltidyrc)

-i=4        # 4 空格縮排
-l=100      # 100 字元行長
-ci=4       # 接續縮排
-ce         # 擁抱 else
-bar        # 左大括號在同一行
-nolq       # 不要外縮長引號字串

perlcritic 設定 (.perlcriticrc)

severity = 3
theme = core + pbp + security

[InputOutput::RequireCheckedSyscalls]
functions = :builtins
exclude_functions = say print

[Subroutines::ProhibitExplicitReturnUndef]
severity = 4

[ValuesAndExpressions::ProhibitMagicNumbers]
allowed_values = 0 1 2 -1

依賴管理 (cpanfile + carton)

cpanm App::cpanminus Carton   # 安裝工具
carton install                 # 從 cpanfile 安裝依賴
carton exec -- perl bin/myapp  # 使用本地依賴執行
# cpanfile
requires 'Moo', '>= 2.005';
requires 'Path::Tiny';
requires 'JSON::MaybeXS';
requires 'Try::Tiny';

on test => sub {
    requires 'Test2::V0';
    requires 'Test::MockModule';
};

快速參考:現代 Perl 慣用語法

舊模式 現代替代
use strict; use warnings; use v5.36;
my ($x, $y) = @_; sub foo($x, $y) { ... }
@{ $ref } $ref->@*
%{ $ref } $ref->%*
open FH, "< $file" open my $fh, '<:encoding(UTF-8)', $file
blessed hashref 使用型別的 Moo 類別
$1, $2, $3 $+{name}(命名捕獲)
eval { }; if ($@) Try::Tiny 或原生 try/catch (5.40+)
BEGIN { require Exporter; } use Exporter 'import';
手動檔案操作 Path::Tiny
blessed($o) && $o->isa('X') $o isa 'X' (5.32+)
builtin::true / false use builtin 'true', 'false'; (5.36+, 實驗性)

反模式

# 1. 二引數開啟(安全風險)
open FH, $filename;                     # 絕對不要

# 2. 間接物件語法(解析歧義)
my $obj = new Foo(bar => 1);            # 壞
my $obj = Foo->new(bar => 1);           # 好

# 3. 過度依賴 $_
map { process($_) } grep { validate($_) } @items;  # 難以理解
my @valid = grep { validate($_) } @items;           # 更好:拆開
my @results = map { process($_) } @valid;

# 4. 停用 strict refs
no strict 'refs';                        # 幾乎總是錯的
${"My::Package::$var"} = $value;         # 改用雜湊

# 5. 全域變數作為設定
our $TIMEOUT = 30;                       # 壞:可變的全域
use constant TIMEOUT => 30;              # 更好:常數
# 最好:帶預設值的 Moo 屬性

# 6. 字串 eval 載入模組
eval "require $module";                  # 壞:程式碼注入風險
eval "use $module";                      # 壞
use Module::Runtime 'require_module';    # 好:安全載入模組
require_module($module);

切記:現代 Perl 是乾淨、可讀且安全的。讓 use v5.36 處理樣板,使用 Moo 處理物件,並偏好 CPAN 上經過實戰考驗的模組,而非自行實作。