【ゲーム開発のためのC#入門講座・応用拡張編】カプセル化で保守性を高めよう【#1】

5.0_C#応用拡張編

ようこそ、応用拡張編へ!

この応用拡張編ではオブジェクト指向の三大要素について学習します。

  • カプセル化
  • 継承
  • ポリモーフィズム(多態性)

オブジェクト指向を含めた現代プログラミングの知識・技術の習得はこの応用拡張編で実質完了します。この応用拡張編を乗り切れば、Unityの構造をきちんと理解しながら自分の思い描くプログラムを組んでいくことができるようになるでしょう。

今までおまじないといってきた内容もすべてここで明かされます。長かったですね。でもあと少しです。引き続き楽しくC#を学んでいきましょう!

それでは今回は三大要素のひとつ目、カプセル化について学びます。

ヒーラーが回復してくれるプログラムを作ろう

カプセル化を学ぶために、ヒーラーがMPの尽きるまでHPをヒール(MP消費4)してくれるプログラムを作ってみましょう。

MPや回復量はインスタンス生成時にランダムで算出されるものとします。

Healer healer = new Healer();
int playerHP = 1;
​
while (healer.MP > 4)
{
    playerHP += healer.Heal();
}
​
Console.WriteLine(playerHP);
​
public class Healer
{
    public int MP;
    public int HealPower;
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
}

データと処理をクラスにまとめ、コンストラクタでメンバー変数の初期設定を行っています。

応用学習編で学んだことが勢揃いって感じですね!

プログラマーY氏再び

オブジェクト指向にも少しずつ慣れてきたかなと悦に浸っていたところ、共にゲーム開発に向けて勉強しているY氏が不意にコードを覗き込んできました。

「ん、そのヒーラークラスって何?」

「こいつはMPが尽きるまでひたすらヒールしてくれるクラスだよ。MPや回復量はランダムで設定されるんだ」

「すげーじゃん。俺のゲームでも使えそうだな……そのクラスくれよ」

「いいよ」

「サンキュー!」

(……後日……)

「もらったヒーラークラス使ってみたんだけどさ」

「おお、どうだった? なかなか便利でしょ?」

「MPも回復量も全然ランダムにならねえぞ?」

「は? 何で?」

「こっちが聞きたいわ」

「ちょっとコード見せて」

Healer healer = new Healer();
// ↓ここが今回のお話のポイントだよ!
healer.MP = 20;
healer.HealPower = 10;
// ここまで
int playerHP = 1;
​
while (healer.MP > 4)
{
    playerHP += healer.Heal();
}
​
Console.WriteLine(playerHP);

「……いや、そりゃそうだよ。せっかくインスタンス生成時にランダムな数値で「MP」と「HealPower」設定しているのに、固定値で上書きしちゃってるじゃん」

「え、ここで設定した値をもとにランダムな数値を算出してくれるんじゃないの?」

「違うよ」

「マジかよ。つまり「MP」と「HealPower」ってこっちで設定する必要ない?」

「そう。インスタンス生成時に自動算出するから、むしろ設定しちゃだめなの」

「知らんわ、そんなの。だったらそもそもこっちからは設定できないようにしてくれ。そしたらこんな誤解しないで済んだのに」

「……まあ、一理ある」

このような会話がこの世界の片隅で行われたかどうかはこれまた定かではありませんが、確かに最初から設定できなければそもそも誤解のしようがないですよね。

改めてプログラムを見直してみよう

改めてプログラムを見直してみましょう。

メンバー変数「HealPower」の利用箇所に注目してみてください。

Healer healer = new Healer();
int playerHP = 1;
​
while (healer.MP > 4)
{
    playerHP += healer.Heal();
}
​
Console.WriteLine(playerHP);
​
public class Healer
{
    public int MP;
    // ↓定義している箇所
    public int HealPower;
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        // ↓設定している箇所
        HealPower = random.Next(10) + 1;
    }
    
    public int Heal()
    {
        MP -= 4;
        // ↓参照している箇所
        return HealPower * 2;
    }
}

よくよく見てみると、クラスの中ですべて完結していますね。

ようはクラスの外側で読み書きする必要のないデータなのです。むしろY氏のように勝手に上書きされると、想定通りに動かなくなってしまいます。

このように、クラスの中でしか使わないものをクラスの中からしか操作できないようにする方法があります。

それがアクセス修飾子です。

アクセス修飾子で情報を隠蔽しよう

今回はふたつのアクセス修飾子を見てみましょう。

アクセス修飾子アクセス可能な範囲
publicどこからでも可
privateクラス内のみ可

やっとこのおまじないの意味を解説できる時がきました。

そう、publicとは実は、どこからでも(クラスの外からでも)アクセスOKという意味のアクセス修飾子だったのです。

そう言われてみると、今回キーとなっているメンバー変数「HealPower」もアクセス修飾子はpublicになっていますね。

public class Healer
{
    public int MP;
    // ↓アクセス修飾子がpublicになっている
    public int HealPower;
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
}

これをprivateに変更すると、クラス内からしかアクセスできないようにすることができます。

public class Healer
{
    public int MP;
    // ↓private(クラス内のみ可)に変更したよ!
    private int HealPower;
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
}

Y氏のようにクラスの外側からアクセスしようとしてもエラーになるので、変に誤解されることもなくなる訳ですね。

Healer healer = new Healer();
healer.MP = 20;
// ↓クラスの外側からアクセスしようとしても、エラーになるよ!
healer.HealPower = 10;
int playerHP = 1;
​
while (healer.MP > 4)
{
    playerHP += healer.Heal();
}
​
Console.WriteLine(playerHP);

これなら安全に使ってもらえそうです。

なお、他にもあちこちアクセス修飾子がついていることからわかるように、メンバー変数に限らずクラスに紐づく様々なもののアクセスを制御することができます。

// ↓クラスにもついている(変更可能)
public class Healer
{
    public int MP;
    private int HealPower;
 
    // ↓コンストラクタにもついている(変更可能)
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    // ↓メソッドにもついている(変更可能)
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
}

カプセル化って?

このように、クラス内でしか使わないものをクラスの中からしか操作できないようにすること、すなわちクラスという入れ物(カプセル)に閉じ込めて外から見えないように隠してしまうことをカプセル化といいます。

適切なアクセス修飾子を設定し、上手にカプセル化を行うことで、想定外のデータ設定や処理実行によるバグを防ぐことができます。

また、使う側にとっても、読み書きしていいデータ、利用してよいメソッドにだけアクセスできるようになっている方が誤解なく利用できて使いやすいですよね。

安全性が高く使いやすいクラスを作れるようになる――まさにオブジェクト指向の目的である「理解しやすく、再利用しやすいプログラムを作る」を実現する機能ですね。

実は他にもアクセス修飾子はあるのですが、それはその知識が必要になったタイミングでご紹介させていただきます。

まずはクラスの外に公開するpublicと、クラスの内に隠すprivateのふたつを使い分けていくことに少しずつ慣れていきましょう。

Unityでの活躍ポイント

Unityでは、publicに設定したメンバー変数の値を、インスペクターという画面から直接変更することができるようになっています。値を直接入力することもできますし、範囲を定義するとスライダー形式で設定することも可能になります。

こんな風に色んなものを視覚的に設定可能!

とても便利ですね!

そのため、Unityでは何をprivateにして隠蔽し、何をpublicにして公開するかが開発効率にも影響を及ぼします。

非常に重要なポイントなので、ぜひマスターしておきましょう。

実践演習

それでは実際にアクセスを制御してみましょう。

仕様

下記のプログラムのうち、クラスの外で設定する必要のないデータやメソッドはすべてprivateに変更してください。

テンプレート
BattleCharacter link = new BattleCharacter(10, 25, 10);
link.Name = "リンク";
​
BattleCharacter goblin = new BattleCharacter(5, 10, 7);
goblin.Name = "ゴブリン";
​
while (true)
{
    goblin.HP -= link.Attack();
    if (goblin.HP <= 0)
    {
        break;
    }
    link.HP -= goblin.Attack();
    if (link.HP <= 0)
    {
        break;
    }
}
​
if (goblin.HP <= 0)
{
    Console.WriteLine(link.Name + "勝利!");
}
else
{
    Console.WriteLine("ゲームオーバー!");
}
​
public class BattleCharacter
{
    public string Name = "";
    public int Level = 0;
    public int HP = 0;
    public int ATK = 0;
    public Random Random = new Random();
​
    public BattleCharacter(int level, int hp, int atk)
    {
        Level = level;
        HP = hp;
        ATK = atk;
    }
    
    public int Attack()
    {
        return ATK + Level * 2 / 5 + CalcRandomDamage();
    }
    
    public int CalcRandomDamage()
    {
        return Random.Next(5) + 1;
    }
}

答え合わせ

BattleCharacter link = new BattleCharacter(10, 25, 10);
link.Name = "リンク";
​
BattleCharacter goblin = new BattleCharacter(5, 10, 7);
goblin.Name = "ゴブリン";
​
while (true)
{
    goblin.HP -= link.Attack();
    if (goblin.HP <= 0)
    {
        break;
    }
    link.HP -= goblin.Attack();
    if (link.HP <= 0)
    {
        break;
    }
}
​
if (goblin.HP <= 0)
{
    Console.WriteLine(link.Name + "勝利!");
}
else
{
    Console.WriteLine("ゲームオーバー!");
}
​
public class BattleCharacter
{
    public string Name = "";
    private int Level = 0;
    public int HP = 0;
    private int ATK = 0;
    private Random Random = new Random();
​
    public BattleCharacter(int level, int hp, int atk)
    {
        Level = level;
        HP = hp;
        ATK = atk;
    }
​
    public int Attack()
    {
        return ATK + Level * 2 / 5 + CalcRandomDamage();
    }
​
    private int CalcRandomDamage()
    {
        return Random.Next(5) + 1;
    }
}

まとめ

  • アクセス修飾子はデータやメソッドなどのアクセス許可範囲を設定することができる機能
  • アクセス修飾子を活用し、クラスという入れ物(カプセル)に閉じ込めたデータやメソッドを外から見えないように隠してしまうことをカプセル化という
  • 上手にカプセル化を行うことで、安全性が高く使いやすいクラスにすることができる

「なるほど……って、HealPowerは確かに隠せたけどさ、MPはそのままになってない? MPはクラスの外でも参照してるから、privateにする訳にはいかないよね?」

Healer healer = new Healer();
// ↓本当はMPも勝手に設定して欲しくないんだけど、
healer.MP = 20;
int playerHP = 1;
​
// ↓クラスの外で参照してるから、
while (healer.MP > 4)
{
    healer.MP -= 4;
    playerHP += healer.Heal();
}
​
Console.WriteLine(playerHP);
​
public class Healer
{
    // ↓privateにする訳にはいかない
    public int MP;
    private int HealPower;
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
}

次回はとある技術を使ってこの問題を解決し、より強固なカプセル化を達成します。お楽しみに!

それでは、今回もお疲れ様でした!

また次の記事でお会いしましょう!

おまけ

アクセス修飾子を省略した場合はprivate扱いになります。

わかりやすく明示的に記述するもよし、省略して入力の手間を省くもよし、お好みで!

public class Healer
{
    public int MP;
    // ↓省略した場合はprivate扱い
    int HealPower;
    
    public Healer()
    {
        Random random = new Random();
        MP = random.Next(20) + 1;
        HealPower = random.Next(10) + 1;
    }
    
    public int Heal()
    {
        MP -= 4;
        return HealPower * 2;
    }
}

Posted by yuumekou