#!/usr/bin/env perlusestrict;usewarnings;useutf8;useJSON;# === Taskクラスの定義 ===packageTask{useMoo;hasid=>(is=>'rw',default=>sub{0},);hastitle=>(is=>'ro',required=>1,);hasis_done=>(is=>'rw',default=>sub{0},);submark_done{my$self=shift;$self->is_done(1);}}# === TaskRepository::Role ===packageTaskRepository::Role{useMoo::Role;requires'save';requires'find';requires'all';requires'remove';}# === TaskRepository::File ===packageTaskRepository::File{useMoo;useJSON;with'TaskRepository::Role';hasfilepath=>(is=>'ro',default=>sub{'tasks.json'},);sub_load{my$self=shift;my@tasks;return@tasksunless-e$self->filepath;openmy$fh,'<:encoding(UTF-8)',$self->filepathordie$!;my$json=do{local$/;<$fh>};close$fh;my$data=decode_json($json);formy$item(@$data){push@tasks,Task->new(id=>$item->{id},title=>$item->{title},is_done=>$item->{is_done}?1:0,);}return@tasks;}sub_save_all{my($self,@tasks)=@_;my@data=map{{id=>$_->id,title=>$_->title,is_done=>$_->is_done?\1:\0,}}@tasks;openmy$fh,'>:encoding(UTF-8)',$self->filepathordie$!;print$fhencode_json(\@data);close$fh;}subsave{my($self,$task)=@_;my@tasks=$self->_load;if($task->id&&$task->id>0){my$found=0;formy$t(@tasks){if($t->id==$task->id){$t->is_done($task->is_done);$found=1;last;}}push@tasks,$taskunless$found;}else{my$max_id=0;formy$t(@tasks){$max_id=$t->idif$t->id>$max_id;}$task->id($max_id+1);push@tasks,$task;}$self->_save_all(@tasks);return$task;}subfind{my($self,$id)=@_;my@tasks=$self->_load;formy$task(@tasks){return$taskif$task->id==$id;}return;}suball{my$self=shift;return$self->_load;}subremove{my($self,$id)=@_;my@tasks=$self->_load;my$original_count=@tasks;@tasks=grep{$_->id!=$id}@tasks;if(@tasks<$original_count){$self->_save_all(@tasks);return1;}return0;}}# === TaskRepository::InMemory ===packageTaskRepository::InMemory{useMoo;with'TaskRepository::Role';hasstorage=>(is=>'rw',default=>sub{{}},);hasnext_id=>(is=>'rw',default=>sub{1},);subsave{my($self,$task)=@_;if(!$task->id||$task->id==0){$task->id($self->next_id);$self->next_id($self->next_id+1);}$self->storage->{$task->id}=$task;return$task;}subfind{my($self,$id)=@_;return$self->storage->{$id};}suball{my$self=shift;returnvalues%{$self->storage};}subremove{my($self,$id)=@_;if(exists$self->storage->{$id}){delete$self->storage->{$id};return1;}return0;}}# === メイン処理 ===packagemain;my$repository;if($ENV{TODO_TEST_MODE}){$repository=TaskRepository::InMemory->new;}else{$repository=TaskRepository::File->new(filepath=>'tasks.json');}my$command=shift@ARGV//'help';if($commandeq'add'){my$title=shift@ARGV;die"Usage: $0 add <task>\n"unlessdefined$title&&$titlene'';my$task=Task->new(title=>$title);$repository->save($task);print"Added: $title (ID: ".$task->id.")\n";}elsif($commandeq'list'){my@tasks=$repository->all;if(@tasks==0){print"No tasks.\n";exit;}formy$task(sort{$a->id<=>$b->id}@tasks){my$status=$task->is_done?'[x]':'[ ]';printf"%d. %s %s\n",$task->id,$status,$task->title;}}elsif($commandeq'complete'){my$id=shift@ARGV;die"Usage: $0 complete <id>\n"unlessdefined$id&&$id=~ /^\d+$/;my$task=$repository->find($id);die"Task $id not found.\n"unless$task;$task->mark_done();$repository->save($task);print"Completed: ".$task->title."\n";}else{print"Usage: $0 <command> [args]\n";print"Commands:\n";print" add <task> - Add a new task\n";print" list - List all tasks\n";print" complete <id> - Complete a task by ID\n";}
Repositoryパターンの真価
リポジトリ切り替えの概念を図で確認しましょう。
flowchart TB
subgraph メイン処理
A[Command]
end
subgraph Repository選択
B{環境変数<br/>TODO_TEST_MODE?}
end
subgraph Repository実装
C[TaskRepository::InMemory]
D[TaskRepository::File]
end
A --> B
B -->|設定あり| C
B -->|設定なし| D
C --> E[メモリ上のハッシュ]
D --> F[tasks.json ファイル]