Featured image of post すべての本を表示したい - forループとカプセル化の壁

すべての本を表示したい - forループとカプセル化の壁

本棚アプリの全ての本を表示する機能を追加。forループで配列に直接アクセスするとカプセル化が崩れる問題を体験し、より良い設計を考えます。

はじめに

@nqounetです。

シリーズ「本棚アプリで覚える集合体の巡回」の第2回です。

前回の振り返り

前回は、本棚アプリの基礎となる2つのクラスを作成しました。

  • Bookクラス — title(タイトル)とauthor(著者)の属性を持つ
  • BookShelfクラス — 複数の本を管理する集合体。add_bookget_book_atget_lengthメソッドを提供する

これらのクラスを使って、本棚に本を追加し、個別に取り出す機能を実装しました。

今回のお題:すべての本を一覧表示したい

本棚に登録したすべての本を一覧表示する機能を追加してみましょう。一見簡単そうに見えますが、この実装を通じて「カプセル化」について深く考える機会になります。

素朴なアプローチ:books配列に直接アクセスする

まず、最も素朴なアプローチを試してみます。BookShelfbooks属性に直接アクセスして、forループで回す方法です。

1
2
3
4
5
6
# Perl v5.36以降 / Moo
# 問題のあるコード例

for my $book ($shelf->books->@*) {
    say $book->title . " / " . $book->author;
}

このコードは動作します。しかし、ここには重大な問題が潜んでいます。

カプセル化の破壊

オブジェクト指向プログラミングにおける「カプセル化」とは、オブジェクトの内部構造を隠蔽し、外部には必要なインターフェースだけを公開する考え方です。

	flowchart LR
    subgraph 問題のあるアプローチ
        Client1[利用者コード] -->|"$shelf->books->@*"| Internal[内部配列に直接アクセス]
        Internal -->|カプセル化の破壊| BookShelf1[BookShelf]
    end

上記のコードの問題点を整理します。

  • BookShelfの内部が「配列」であることを外部から知っている必要がある
  • books属性に直接アクセスしている
  • 配列のデリファレンス記法(->@*)を使っている

これは、本棚の「引き出しを開けて中身を直接見ている」ようなものです。本棚の設計者は、本を配列で管理することを内部の実装詳細として隠したいはずです。しかし、外部のコードがこの実装詳細に依存してしまっています。

なぜこれが問題なのか

将来、BookShelfの内部構造が変更になった場合を考えてみます。

  • 本を配列ではなくハッシュで管理するようになった
  • データベースから都度取得するようになった
  • ページネーションを導入して一部の本だけをメモリに保持するようになった

このような変更が入ると、$shelf->books->@*と書いているすべての箇所を修正する必要があります。これは保守性の面で大きな問題です。

改善版:メソッド経由でアクセスする

前回作成したget_lengthget_book_atメソッドを使ってみましょう。

1
2
3
4
5
6
7
# Perl v5.36以降 / Moo
# 改善版だがまだ問題あり

for my $i (0 .. $shelf->get_length - 1) {
    my $book = $shelf->get_book_at($i);
    say $book->title . " / " . $book->author;
}

このコードは、books属性に直接アクセスしていません。BookShelfが提供するメソッドのみを使用しています。これは先ほどのコードより改善されています。

この改善版でも残る問題

しかし、このアプローチにもまだ問題があります。

  • 「インデックスでアクセスできる」という内部構造への依存が残っている
  • ループの書き方(0から始まる、lengthを使う)を利用側が意識している
  • 巡回のロジックが利用側に散らばる
	flowchart LR
    subgraph 改善版でも残る依存
        Client2[利用者コード] -->|"get_length() + get_book_at(i)"| Index[インデックス操作の知識が必要]
        Index --> BookShelf2[BookShelf]
    end

本棚の実装が「配列」から「連結リスト」や「データベース」に変わった場合、この書き方は使えなくなる可能性があります。

理想的な形

本当に欲しいのは、以下のような形です。

  • 「次の本を取得する」という操作だけを知っていればよい
  • 本棚の内部構造を一切意識しない
  • 本棚側が巡回方法を完全にコントロールする

これを実現する方法については、次回以降で詳しく見ていきます。

完成コード

今回の内容を反映した完成コードを以下に示します。2つのアプローチを比較できるようにしています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#!/usr/bin/env perl
# Perl v5.36以降 / Moo
use v5.36;

package Book {
    use Moo;

    has title  => (is => 'ro', required => 1);
    has author => (is => 'ro', required => 1);
}

package BookShelf {
    use Moo;

    has books => (
        is      => 'ro',
        default => sub { [] },
    );

    sub add_book ($self, $book) {
        push $self->books->@*, $book;
    }

    sub get_book_at ($self, $index) {
        return $self->books->[$index];
    }

    sub get_length ($self) {
        return scalar $self->books->@*;
    }
}

package main;

# 本棚を作成
my $shelf = BookShelf->new;

# 本を追加
$shelf->add_book(Book->new(title => 'すぐわかるPerl', author => '深沢千尋'));
$shelf->add_book(Book->new(title => '初めてのPerl', author => 'Randal L. Schwartz'));
$shelf->add_book(Book->new(title => 'プログラミングPerl', author => 'Larry Wall'));

say "=== 方法1: books配列に直接アクセス(問題あり) ===";
for my $book ($shelf->books->@*) {
    say $book->title . " / " . $book->author;
}

say "";
say "=== 方法2: メソッド経由でアクセス(改善版) ===";
for my $i (0 .. $shelf->get_length - 1) {
    my $book = $shelf->get_book_at($i);
    say $book->title . " / " . $book->author;
}

実行結果

1
2
3
4
5
6
7
8
9
=== 方法1: books配列に直接アクセス(問題あり) ===
すぐわかるPerl / 深沢千尋
初めてのPerl / Randal L. Schwartz
プログラミングPerl / Larry Wall

=== 方法2: メソッド経由でアクセス(改善版) ===
すぐわかるPerl / 深沢千尋
初めてのPerl / Randal L. Schwartz
プログラミングPerl / Larry Wall

まとめ

  • カプセル化とは、オブジェクトの内部構造を隠蔽し、外部には必要なインターフェースだけを公開する考え方である
  • $shelf->books->@*のように内部の配列に直接アクセスするとカプセル化が崩れる
  • get_lengthget_book_atを使う方法は改善だが、「インデックスでアクセスする」という前提への依存が残る
  • 内部構造に依存したコードは、将来の変更で修正が必要になるリスクがある

次回予告

今回、本棚のすべての本を一覧表示する際に「カプセル化」の問題に直面しました。メソッドを使う改善版でも、まだ「インデックスでアクセスする」という内部構造への依存が残っています。

次回は、この問題を解決するための新しい仕組みを導入します。本棚の内部構造を一切知らなくても、すべての本を順番に取り出せる方法を探ります。

お楽しみに。

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