【C#】クラスを使ってカプセル化をする方法

当連載では現代のプログラミングにおいて重要となる「オブジェクト指向」について解説しています。オブジェクト指向は「オブジェクト」を頂点とした考え方を持つ開発手法であり、現代の多くの案件で使用されています。

今回はオブジェクト指向の中でも最も重要となる「カプセル化」を掘り下げていきます。C#でカプセル化を実現するには「クラス」を活用することが必要となりますので、カプセル化を実現するためのクラスの使い方を解説したいと思います。クラスを使用してカプセル化をするにはプロパティとメソッドが重要です。

クラスでまとめる理由

まずは「どうしてクラスでまとめるのか?」という点を解説していきたいと思います。オブジェクト指向における重要な点は「処理をまとめて分かりやすくすることだ」という趣旨の内容を「オブジェクト指向とは | プログラミングの重要な考え方」にて紹介しました。

オブジェクト指向は「機能をひとまとめにして再利用可能な部品として使えるようにする」ことを目的とします。

再利用可能とするためには、部品のパーツがあちらこちらに散らばっているわけにはいきません。組み立てに必要なものはまとめておいておいて欲しいですよね。何か製品を作る工場を想像してみてください。工場で使用されるすべての部品が製品関係なしに工場のあちこちに置かれているのと、製品を作るのに必要な部品がまとめておかれているのではどちらが効率がよいでしょうか。

製品関係なしに部品が工場内におかれている場合、必要な部品を探すために工場をあちこち歩かなくてはなりませんよね。それに対して、製品を作るのに部品が一か所にまとまっていれば探すのも簡単です。なんなら部品の近くに作業場を持っていくとより効率があがるのは想像できると思います。

プログラミングにおけるオブジェクト指向も似たような考え方をしているのです。特定の物事を成し遂げるために必要な処理・データは近くに持っておきたいという願望が根底にあることになります。それを実現するのに最適なのが「クラス」になるのです。クラスには以下の機能がメインとなります。

  • メソッド
  • プロパティ
  • フィールド

そしてそれらを制御するために必要なのが「アクセス修飾子」になります。クラスの基本的な機能、およびアクセス修飾子については「【C#】アクセス修飾子とは | アクセス修飾子の基本を解説」で解説しているので確認してください。

カプセル化をする方法

さて、議論をもう少し先に進めて実際にカプセル化をする方法を考えていきたいと思います。基本的な方針は「まとめる」です。ここからは簡単にコーディングをしながら進めていきたいと思います。コンソールアプリケーションを作成していきます。

オブジェクトの機能を決める

カプセル化をするには「何を閉じ込めるか」を考える必要があります。もう少し簡単に言ってしまうと「何をまとめるのか」ということであり、これは「どんな機能を作成するか」ということです。ここでは難しいことをせず「計算をする機能」を用意したいと思います。難しいモノを作っても分かりづらいので、「足し算のみ」を行うオブジェクトを作っていきます。

クラスを定義する

コンソールアプリケーション「App1」を作成して、ソリューションエクスプローラーの「App1」を右クリックし、「追加」を選択、「クラス」を続けて選び「Calculation.cs」を作成しましょう。

作成されたクラスのアクセス修飾子を「public」にします。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace App1
{
    public class Calculation
    {
    }
}

ここまで出来たら準備完了です。ここからクラスを作りこんでいく作業をしていきます。

属性・処理を内包する

計算するクラスですが、今回はよりスコープを狭めて作りたいので「足し算」のみができるクラスを作成していきます。まずは足し算をするために必要な機能を考えていきます。計算といっても微妙なので、電卓で足し算のみが行えるイメージにしましょう。最低限以下の処理は必要になると思います。

  • 足し算する処理

足し算をするといったので足し算をする処理は必要です。それ以外には「合計値を返す処理」も欲しいです。なぜかというと電卓のように連続実行をさせたいので、毎回呼び出し元に足し算の結果を返すよりも「現在の合計値を内包して、必要に応じて呼び出し元に教える」という方が「オブジェクト」としての振る舞いが正しいです。というわけでまずは2つの処理を内包します。

namespace App1
{
    public class Calculation
    {
        private int total;

        private void Add(int input)
        {
            total += input;
        }

        private int GetTotal()
        {
            return total;
        }
    }
}

アクセス修飾子がpublicではなく、privateになっていますが、今は気にしないことにします。まずは「足し算をする処理」と「合計値を返す処理」を持たせるようにしました。それ以外にも必要な処理があるかどうか考えてみます。

今回のGUIはコンソールアプリケーションですので、入力が文字列になりますよね。文字列の場合は数値型とは異なるため、正常に動作させるには「数値かどうかの判定」や「桁数のチェック」が必要になります。こうしたチェック処理も内包する様にします。なぜなら「GUIがコンソールアプリケーションから変更されても使用できるようにする」ためです。

オブジェクト指向は「再利用可能な部品」を作成するのも大きな方針の一つですので、これらを満たせるようなオブジェクト(クラス)を作っていく必要があります。コンソールアプリケーションから切り替えた際には、数値判定や桁数チェックを継続して使用するかは分かりませんが、どんなGUIになっても対応できるという意味で処理を持たせたいと思います。

private bool IsNumber(string input)
{
    return int.TryParse(input, out var num);
}

private bool LessThanMaxLength(string input)
{
    return input.Length > 0 && input.Length <= 5;  
}

とりあえずこんな感じで数値チェック、および桁数チェックを入れてみました。桁数チェックは0より大きく、5桁より小さくしています。あまり複雑なものではなく、単純に文字列のサイズを見るようにしています。なので「000001」とか入力されたら計算できないエラーを返します。本来なら数値に変換した上で桁数チェックをするべきですね。

というわけで実際の計算式、およびチェック処理などを入れ込んだ「Calculationオブジェクト」が完成しました。コメントなどは入れていませんが、クラスの全文はこんな感じになっているかと思います。

namespace App1
{
    public class Calculation
    {
        private int total;

        private void Add(int input)
        {
            total += input;
        }

        private int GetTotal()
        {
            return total;
        }

        private bool IsNumber(string input)
        {
            return int.TryParse(input, out var num);
        }

        private bool LessThanMaxLength(string input)
        {
            return input.Length > 0 && input.Length <= 5;  
        }
    }
}

これで必要な機能は完成しました。ここから「どれを公開するか」を考えていく必要があります。

公開範囲を決める

「Calculationオブジェクト」が完成しましたが、完全に決定した訳ではありません。今のところクラス内に定義したメソッドは、すべてprivateとなっており、外部に公開している機能がありません。ここから「外側に後悔しても良い範囲」を決めていきます。現在は以下のような要素を持っています。

  • 現在の合計値
  • 足し算する処理
  • 合計を返却する処理
  • 数値かどうかを判定する処理
  • 5桁以下かを判定する処理

さて、ここからどれをクラスの外側から操作させてもよいかを考えます。まず、少なくとも「足し算する処理」は公開してよいでしょう。そうでなければ計算オブジェクトの意味がなくなってしまいます。そう考えると「合計を返却する処理」「数値かどうかを判定する処理」「5桁以下かを判定する処理」も外から呼ばれること前提とした処理ですね。

「現在の合計値」はどうかというと、別に外側に公開する必要はない内容です。「合計を返却する処理」があるので内部で持っている「private int total」の部分はクラスの外側に公開する「public int total」とする必要はありません。

publicにして公開してしまうと、計算オブジェクトの外側からtotalを操作できるようになってしまうのでカプセル化の理想からは外れます。というわけで公開範囲を考えたうえでクラスは以下のように定義することになります。今回は簡単な処理をクラスとしたので、あまり面白みはありませんでしたね。

namespace App1
{
    public class Calculation
    {
        private int total;

        public void Add(int input)
        {
            total += input;
        }

        public int GetTotal()
        {
            return total;
        }

        public bool IsNumber(string input)
        {
            return int.TryParse(input, out var num);
        }

        public bool LessThanMaxLength(string input)
        {
            return input.Length > 0 && input.Length <= 5;  
        }
    }
}

メイン部分を記述する

最後に作成したCalculationオブジェクトを操作するためのメインルーチン部分を書いていきます。これについてはC#の基本文法が分かっていれば問題にはなりませんので、ソースコードの全文のみを記載して終了にしたいと思います。

using System;

namespace App1
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("5桁以下の数値を入力してください。「=」の入力で合計が表示されます。 ");

            var calc = new Calculation();
            while(true)
            {
                var input = Console.ReadLine();
                if (!calc.IsNumber(input))
                {
                    if (input == "=") { break; } //処理終了
                    Console.WriteLine("数値を入力してください。");
                    continue;
                }
                if (!calc.LessThanMaxLength(input))
                {
                    Console.WriteLine("1~5桁以下で入力してください。");
                    continue;
                }
                calc.Add(Convert.ToInt32(input));
            }
            Console.WriteLine($"合計値:{ calc.GetTotal()}");
            Console.ReadLine();
        }
    }
}

「=」の入力であるかの判定を「計算オブジェクト」に入れ込まなかった理由は、ここでは計算に関係ない判定だからです。桁数や数値チェックは「計算」する上で必要になる処理なので、Calculationクラスの処理として記述しましたが、「=」の入力で計算結果を表示するのはアプリケーションの仕様であり、純粋な「計算」としての要素になりえないため書いていません。

オブジェクト指向の入門としてはこれで十分かと思います。カプセル化の原則は「必要な処理・関連する処理はまとめて記述する」ことであり「どこに何が書いてあるかが一目瞭然であること」が重要になります。そのためCalculationクラスには「計算」に必要な処理しか定義しませんでした。

また公開範囲を決める上で必要な処理にしかpublicに設定しませんでした。それにはtotalの値はクラス内部でしか操作させないという必要があったからです。もしpublicにしてしまうと、calculationクラスの外側で計算して結果を格納するような処理が記述されてしまう可能性があるためです。「操作されたくないものは公開しない」はカプセル化の原則になります。