Featured image of post Strategyパターンで複数のバックアップ戦略を切り替える

Strategyパターンで複数のバックアップ戦略を切り替える

Strategyパターンでフル・差分・増分バックアップを実装。3つの戦略を切り替え可能にし、パフォーマンスをベンチマーク比較します。

前回は Template Method パターンで処理の骨格を作りました。今回は、その骨格の中で差し替え可能な部品として、バックアップの「戦略(アルゴリズム)」を Strategy パターン で実装します。

前回: 処理フローを整理する骨格 | 目次 | 次回: 2パターンの協調動作

Strategy パターンとは

アルゴリズムをクラスとしてカプセル化し、実行時に交換可能にするパターンです。「戦術(Strategy)を切り替える」イメージです。今回は「フルバックアップ」「差分バックアップ」という異なる戦術を切り替えます。

実装:戦略クラスの実装

前回は Backup::Full というクラス自体が戦略を持っていました(サブクラス化によるTemplate Methodパターンの利用)。それを Strategy パターンに移行するには、戦略部分を別クラスに委譲します。

まず、戦略のインターフェース的なもの(Role)を定義します。

1
2
3
4
5
6
package Backup::Strategy::Role;
use Moo::Role;

requires 'execute'; # このメソッドの実装を強制

1;

次に、具体的な戦略クラスを作ります。

	classDiagram
    class Strategy_Role {
        <<interface>>
        +execute()
    }
    class Strategy_Full {
        +execute()
    }
    class Strategy_Diff {
        +execute()
    }
    Strategy_Role <|.. Strategy_Full
    Strategy_Role <|.. Strategy_Diff
  1. フルバックアップ戦略
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
package Backup::Strategy::Full;
use Moo;
use File::Copy qw(copy);
with 'Backup::Strategy::Role';

sub execute {
    my ($self, $context, $file, $dest_path) = @_;
    # 無条件コピー
    copy($file, $dest_path);
    return 1; # コピーした
}

1;
  1. 差分バックアップ戦略(mtime比較)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
package Backup::Strategy::Diff;
use Moo;
use File::Copy qw(copy);
with 'Backup::Strategy::Role';

sub execute {
    my ($self, $context, $file, $dest_path) = @_;
    
    if (-e $dest_path && (stat($file))[9] <= (stat($dest_path))[9]) {
        return 0; # スキップ(更新日時が古くないなら)
    }

    copy($file, $dest_path);
    utime((stat($file))[8], (stat($file))[9], $dest_path); # mtime同期
    return 1;
}

1;

コンテキスト側の変更

呼び出し側(前回でいう Backup::Base に近いもの)は、戦略オブジェクトを持ち、それを実行時に呼び出します。これが次回の「Template MethodとStrategyの融合」への布石になりますが、一旦ここでは単純に Strategy を使うクラスを示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package Backup::Manager;
use Moo;
use Path::Tiny;

has strategy => (is => 'ro', required => 1); # Strategyオブジェクトを持つ

sub run_backup {
    my ($self, $source, $dest) = @_;
    my $iter = path($source)->iterator({recurse => 1});
    
    while (my $file = $iter->()) {
        next if $file->is_dir;
        my $dest_path = path($dest)->child($file->relative($source));
        $dest_path->parent->mkpath;

        # 戦略に委譲する!
        if ($self->strategy->execute($self, $file, $dest_path)) {
            print "Copied: $file\n";
        } else {
            print "Skipped: $file\n";
        }
    }
}
1;

利用イメージ

コマンドライン引数などで戦略を切り替えることができます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use Backup::Manager;
use Backup::Strategy::Full;
use Backup::Strategy::Diff;

my $mode = shift || 'diff';
my $strategy;

if ($mode eq 'full') {
    $strategy = Backup::Strategy::Full->new;
} elsif ($mode eq 'diff') {
    $strategy = Backup::Strategy::Diff->new;
}

my $manager = Backup::Manager->new(strategy => $strategy);
$manager->run_backup('./source', './dest');

パフォーマンス比較

実際にファイル数10,000件のディレクトリで、2回目以降(差分なし)の実行速度を比較してみましょう。

1
2
3
4
5
6
7
use Benchmark qw(cmpthese);
# ... (準備コード省略) ...

cmpthese(10, {
    'Full' => sub { $full_strategy_manager->run_backup(...) },
    'Diff' => sub { $diff_strategy_manager->run_backup(...) },
});

実行結果:

1
2
3
       Rate Full Diff
Full 0.15/s   -- -99%
Diff 18.5/s 12233%   --

Strategyを切り替えるだけで、これだけの性能差が出ることが確認できました。重要なのは、この性能差を 「呼び出し側のコードを一行も変えずに」 実現できたことです。

メリット

  • アルゴリズムの独立: 「差分判定ロジック」だけが Strategy::Diff に閉じ込められ、Backup::Manager はいかにしてファイルをコピーするかを知る必要がなくなりました。
  • 動的な切り替え: 実行時に if 文でクラスを差し替えるだけで、挙動をガラリと変えられます。

次回は、第3回の Template Method と、今回の Strategy を組み合わせて、最強のバックアップツールアーキテクチャを完成させます。

comments powered by Disqus
Hugo で構築されています。
テーマ StackJimmy によって設計されています。