Javaで学ぶカプセル化:日記アプリを例に

スポンサーリンク
スポンサーリンク

はじめに

オブジェクト指向プログラミングにおいて、クラスのカプセル化は非常に重要だと言われています。しかし、カプセル化の概念を自分の理解に落とし込み、実際に実装することは、私も含め、初心者にとっては難しいと感じることが多いです。

カプセル化とは、オブジェクトの内部状態を隠蔽し、外部から直接アクセスできないようにすることです。これにより、データの一貫性を保ち、不正な操作や予期しない変更から、オブジェクトを保護することができます。

しかし、初学者が「クラスのメンバ変数をprivateにする」「むやみにsetterを持たせない」など表面的な内容だけを聞いても、どのようにカプセル化を実現すれば良いか、具体的なイメージは湧きにくいものです。

本記事では、Javaでの日記アプリを例に、カプセル化の例とその重要性について解説します。年、月、日を表すクラスを設計し、それぞれのクラスでカプセル化をどのように実現するかを具体的に示します。また、カプセル化を行わない場合のデメリットについても触れ、なぜ必要なのかを考えてみます。

スポンサーリンク

カプセル化をしない例

public class Year {
    public Month[] months;
 
    public Year() {
        months = new Month[12];
        for (int i = 0; i < 12; i++) {
            months[i] = new Month();
        }
    }
}

 

public class Month {
    public Day[] days;

    public Month() {
        days = new Day[30]; // 簡略化のため30日と仮定
        for (int i = 0; i < 30; i++) {
            days[i] = new Day();
        }
    }
}

 

public class Day {
    public int date;
    public String diaryEntry;
}

このように、すべての属性がpublicになっている場合、外部から直接アクセスしてデータを変更することができます。

public class Main {
    public static void main(String[] args) {
        Year year = new Year();

        // 1月1日の日記を更新する
        year.months[0].days[0].diaryEntry = "新年の抱負を書く";

        // 1月1日の日記を取得する
        String entry = year.months[0].days[0].diaryEntry;
        System.out.println("1月1日の日記: " + entry);
    }
}

この例では、Yearクラスのmonths配列に直接アクセスし、その中のMonthクラスのdays配列にアクセスして、DayオブジェクトのdiaryEntryを直接変更しています。これにより、以下のデメリットが生じます。

  1. データの一貫性の欠如: 外部から直接データにアクセスできるため、データの一貫性が保たれなくなる可能性がある。
  2. 予期しない副作用: 外部からの直接操作により、クラスの状態が予期しない形で変更されることがある。
  3. メンテナンスの困難さ: 内部実装が外部に露出しているため、変更が困難になり、バグの原因となることがある。

カプセル化を行わない場合、クラスの内部データが外部から直接操作されることで、データの一貫性や安全性が損なわれるリスクがあります。これを防ぐために、クラスの属性をprivateにし、必要なメソッドを通じてアクセスするように設計することが重要です。

スポンサーリンク

カプセル化を行った場合

public class Year {
    private Month[] months;

    public Year() {
        months = new Month[12];
        for (int i = 0; i < 12; i++) {
            months[i] = new Month();
        }
    }
 
    public Month getMonth(int index) {
        return months[index];
    }

    public void updateDiaryEntry(int monthIndex, int dayIndex, String diaryEntry) {
        if (monthIndex >= 0 && monthIndex < 12) {
            months[monthIndex].updateDiaryEntry(dayIndex, diaryEntry);
        }
    }
}


public class Month {
    private Day[] days;

    public Month() {
        days = new Day[30]; // 簡略化のため30日と仮定
        for (int i = 0; i < 30; i++) {
            days[i] = new Day();
        }
    }

    public Day getDay(int index) {
        return days[index];
    }

    public void updateDiaryEntry(int dayIndex, String diaryEntry) {
        if (dayIndex >= 0 && dayIndex < 30) {
            days[dayIndex].setDiaryEntry(diaryEntry);
        }
    }
}


public class Day {
    private int date;
    private String diaryEntry;

    public int getDate() {
        return date;
    }

    public void setDate(int date) {
        this.date = date;

    }

    public String getDiaryEntry() {
        return diaryEntry;
    }

    public void setDiaryEntry(String diaryEntry) {
        this.diaryEntry = diaryEntry;
    }
}

日記を更新する方法

public class Main {
    public static void main(String[] args) {
        Year year = new Year();

        // 1月1日の日記を更新する
        year.updateDiaryEntry(0, 0, "新年の抱負を書く");

        // 1月1日の日記を取得する
        String entry = year.getMonth(0).getDay(0).getDiaryEntry();
        System.out.println("1月1日の日記: " + entry);
    }
}

この例では、YearクラスにupdateDiaryEntryメソッドを追加し、指定された月と日のdiaryEntryを更新できるようにしています。これにより、外部から直接Dayオブジェクトにアクセスすることなく、安全に日記を更新することができます。

カプセル化の落とし穴

上記の場合、一見、日記の更新は Year::updateDiaryEntry() でしか更新できないと思われます。しかし、getMonth().getDay()で取得したDayオブジェクトを直接更新できてしまいます。これでは、カプセル化の目的が完全には達成されません。

public class Main {
    public static void main(String[] args) {
        Year year = new Year();

        // 1月1日の日記を更新する
        year.getMonth(0).getDay(0).setDiaryEntry("新年の抱負を書く");
 
        // 1月1日の日記を取得する
        String entry = year.getMonth(0).getDay(0).getDiaryEntry();
        System.out.println("1月1日の日記: " + entry);
    
}

これを防ぐためには、MonthやDayオブジェクトを外部に公開せず、Yearクラス内で必要な操作をすべて行うように設計する必要があります。

カプセル化をさらに強化した設計

年クラス(Year)

public class Year {
    private Month[] months;

    public Year() {
        months = new Month[12];
        for (int i = 0; i < 12; i++) {
            months[i] = new Month();
        }
    }

    // 月の日記を更新するメソッド
    public void updateDiaryEntry(int monthIndex, int dayIndex, String diaryEntry) {
        if (monthIndex >= 0 && monthIndex < 12) {
            months[monthIndex].updateDiaryEntry(dayIndex, diaryEntry);
        }
    }

    // 月の日記を取得するメソッド
    public String getDiaryEntry(int monthIndex, int dayIndex) {
        if (monthIndex >= 0 && monthIndex < 12) {
            return months[monthIndex].getDiaryEntry(dayIndex);
        }
        return null;
    }
}

月クラス(Month)

public class Month {
    private Day[] days;

    public Month() {
        days = new Day[30]; // 簡略化のため30日と仮定
        for (int i = 0; i < 30; i++) {
            days[i] = new Day();
        }
    }
 
    // 日の日記を更新するメソッド
    public void updateDiaryEntry(int dayIndex, String diaryEntry) {
        if (dayIndex >= 0 && dayIndex < 30) {
            days[dayIndex].setDiaryEntry(diaryEntry);
        }
    }

    // 日の日記を取得するメソッド
    public String getDiaryEntry(int dayIndex) {
        if (dayIndex >= 0 && dayIndex < 30) {
            return days[dayIndex].getDiaryEntry();
        }
        return null;
    }
}

日クラス(Day)

public class Day {
    private int date;
    private String diaryEntry;

    public int getDate() {
        return date;
    }

    public void setDate(int date) {
        this.date = date;
    }
 
    public String getDiaryEntry() {
        return diaryEntry;
    }

    public void setDiaryEntry(String diaryEntry) {
        this.diaryEntry = diaryEntry;
    }
}

日記を更新・取得する方法

public class Main {
    public static void main(String[] args) {
        Year year = new Year();

        // 1月1日の日記を更新する
        year.updateDiaryEntry(0, 0, "新年の抱負を書く");

        // 1月1日の日記を取得する
        String entry = year.getDiaryEntry(0, 0);
        System.out.println("1月1日の日記: " + entry);
    }
}

この設計では、MonthやDayオブジェクトを外部に公開せず、Yearクラス内で日記の更新や取得を行うようにしています。これにより、外部から直接Dayオブジェクトを操作することができなくなり、カプセル化が強化されます。

この方法で、データの一貫性と安全性を保ちながら、必要な操作を行うことができます。

機能を拡張する場合

日記アプリを作るとしたら、複数のYearオブジェクトを扱えるようにする必要がありますね。新たにDiaryクラスを作成し、その中でYearオブジェクトを管理する方法を考えてみます。

Diaryクラス

import java.util.HashMap;
import java.util.Map;

public class Diary {
    private Map<Integer, Year> years;

    public Diary() {
        years = new HashMap<>();
    }
 
    // 年を追加するメソッド
    public void addYear(int year) {
        if (!years.containsKey(year)) {
            years.put(year, new Year());
        }
    }
 
    // 日記を更新するメソッド
    public void updateDiaryEntry(int year, int monthIndex, int dayIndex, String diaryEntry) {
        if (years.containsKey(year)) {
            years.get(year).updateDiaryEntry(monthIndex, dayIndex, diaryEntry);
        }
    }

    // 日記を取得するメソッド
    public String getDiaryEntry(int year, int monthIndex, int dayIndex) {
        if (years.containsKey(year)) {
            return years.get(year).getDiaryEntry(monthIndex, dayIndex);
        }
        return null;
    }
}

この設計では、Diaryクラスが複数のYearオブジェクトを管理し、日記の更新や取得を行います。これにより、複数の年にわたる日記を一元的に管理することができます。

やはり、外部から必要な情報を更新・取得するためのpublicメソッドを持たせています。Yearオブジェクトを直接外部に提供することもありません。

さいごに

今回の例では、年、月、日を表すクラスを設計し、それぞれのクラスでカプセル化を実現し、強化する方法を具体的に考えてみました。また、複数の念を管理するためのDiaryクラスを導入した場合も、配下のクラスが適切にカプセル化されていれば、簡単に、より複雑なデータ構造を扱えるようになることが分かりました。

カプセル化の概念を理解し、実際のコードに適用し、試行錯誤を繰り返していくことで、よりメンテナンス性の高いソフトウエアを開発することができます。

コメント