Menu
Coddy logo textTech

パッケージレベルのカプセル化

CoddyのGOジャーニー「オブジェクト指向プログラミング」セクションの一部 — レッスン 48/107。

前のレッスンではフィールドレベルの可視性について説明しましたが、Goのカプセル化は実際にはパッケージレベルで動作します。同じパッケージ内にあるものはすべて、どのファイルにあるかに関係なく、エクスポートされていないフィールドや関数にアクセスできます。

複数のファイルを持つパッケージを考えてみましょう:

// account/account.go
package account

type Account struct {
    balance int  // エクスポートされていない
}

func (a *Account) Deposit(amount int) {
    a.balance += amount
}
// account/helper.go
package account

func ResetAccount(a *Account) {
    a.balance = 0  // 動作します - 同じパッケージ内
}

両方のファイルは account パッケージに属しているため、helper.go はエクスポートされていない balance フィールドに直接アクセスできます。これは意図的なもので、内部へのアクセスを維持しながら、パッケージのロジックを複数のファイルに分割することができます。

しかし、パッケージ外のコードは、エクスポートされていないメンバにアクセスすることはできません:

// main.go
package main

import "account"

func main() {
    acc := &account.Account{}
    acc.Deposit(100)        // works - exported method
    // acc.balance = 50     // error - unexported field
    // account.ResetAccount // works if ResetAccount were exported
}

このパッケージレベルの境界は、Goの主要なカプセル化メカニズムです。パッケージを、内部コードが自由に連携し、外部コードはエクスポートされたAPIを通じてのみ相互作用する、凝集度の高いユニットとして設計します。各パッケージを、明確な公開インターフェースを持つ自己完結型のモジュールと考えてください。

challenge icon

チャレンジ

簡単

同じパッケージ内の複数のファイルが、非公開フィールドに直接アクセスすることで連携しつつ、そのデータを外部コードから隠蔽する方法を示す在庫管理システムを構築しましょう。

main パッケージ内で連携して動作する3つのファイルを作成します:

  • product.go: 公開フィールド Name と非公開フィールド quantity (int) を持つ Product 構造体を定義します。[Name]: [quantity] in stock という形式を返す公開メソッド Info() string を追加してください。
  • inventory.go: 非公開の quantity フィールドに直接アクセスして製品を操作するヘルパー関数を作成します(同じパッケージ内にあるため可能です)。以下を実装してください:
    • Restock(p *Product, amount int) - 製品の数量に指定された量を追加します。
    • Sell(p *Product, amount int) bool - 十分な在庫がある場合に数量から指定された量を差し引き、成功した場合は true、在庫不足の場合は false を返します。
    • NewProduct(name string, initialQty int) *Product - 指定された名前と初期数量で製品を作成するコンストラクタです。
  • main.go: 入力から製品の詳細を読み取り、コンストラクタを使用して製品を作成し、在庫操作を実行して結果を表示します。各操作の後、製品情報を出力して更新された状態を表示してください。

以下の入力が提供されます:

  • 1行目: 製品名
  • 2行目: 初期数量 (整数)
  • 3行目: 補充量 (整数)
  • 4行目: 販売量 (整数)

製品を作成した後、その初期情報を出力します。次に補充を行い、再度情報を出力します。最後に販売を試み、更新された情報(成功した場合)または "Sale failed: insufficient stock"(数量が不足している場合)のいずれかを出力します。

例えば、Laptop1058 が与えられた場合、出力は以下のようになります:

Laptop: 10 in stock
Laptop: 15 in stock
Laptop: 7 in stock

また、Phone3210 が与えられた場合、出力は以下のようになります:

Phone: 3 in stock
Phone: 5 in stock
Sale failed: insufficient stock

ここでの重要なポイントは、inventory.goproduct.go と同じパッケージ内にあるため、非公開の quantity フィールドを直接読み書きできることです。これはパッケージレベルのカプセル化の実践です。内部の連携は自由に行われる一方で、データは外部パッケージから保護されたままになります。

チートシート

Goのカプセル化は、ファイルレベルではなく**パッケージレベル**で動作します。同じパッケージ内のすべてのファイルは、どのファイルで定義されているかに関わらず、エクスポートされていないフィールドや関数にアクセスできます。

パッケージレベルのアクセス

エクスポートされていないメンバは、同じパッケージ内のすべてのファイルからアクセス可能です:

// account/account.go
package account

type Account struct {
    balance int  // エクスポートされていない
}

func (a *Account) Deposit(amount int) {
    a.balance += amount
}
// account/helper.go
package account

func ResetAccount(a *Account) {
    a.balance = 0  // 動作する - 同じパッケージ
}

外部パッケージの制限

パッケージ外のコードは、エクスポートされていないメンバにアクセスできません:

// main.go
package main

import "account"

func main() {
    acc := &account.Account{}
    acc.Deposit(100)        // 動作する - エクスポートされたメソッド
    // acc.balance = 50     // エラー - エクスポートされていないフィールド
}

この設計により、内部アクセスを維持しながらパッケージのロジックを複数のファイルに分割することができ、明確なパブリックインターフェースを持つまとまりのあるユニットを作成できます。

自分で試してみよう

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// 商品名を読み込む
	name, _ := reader.ReadString('\n')
	name = strings.TrimSpace(name)

	// 初期数量を読み込む
	initialQtyStr, _ := reader.ReadString('\n')
	initialQty, _ := strconv.Atoi(strings.TrimSpace(initialQtyStr))

	// 補充量を読み込む
	restockStr, _ := reader.ReadString('\n')
	restockAmount, _ := strconv.Atoi(strings.TrimSpace(restockStr))

	// 販売数量を読み込む
	sellStr, _ := reader.ReadString('\n')
	sellAmount, _ := strconv.Atoi(strings.TrimSpace(sellStr))

	// TODO: NewProduct コンストラクタを使用して新しい商品を作成する

	// TODO: 初期の商品の情報を表示する

	// TODO: 商品を補充し、情報を表示する

	// TODO: 販売を試行し、結果を表示する
	// 販売が成功した場合は、更新された情報を表示する
	// 販売が失敗した場合は、"Sale failed: insufficient stock" と表示する

	fmt.Println() // 実際の出力に置き換える
}
quiz icon腕試し

このレッスンには短いクイズがあります。レッスンを始めて解答し、進捗を記録しましょう。

オブジェクト指向プログラミングのすべてのレッスン