データを中心に考える

1月 15, 2020

プログラムが複雑になるにつれ、多くのデータ、多くの処理、多くの画面に多くの入出力が複雑に絡んできます。

初めて大きめの規模のプログラムを作ったとき、内部が複雑になりすぎて破綻してしまい、お蔵入りになってしまうという残念な経験をしたことはないでしょうか。あるいは、現在進行形で破綻寸前のプログラムと向き合ってる方もいらっしゃるかもしれません。

破綻しないプログラムを作るためには、データを中心に考えるという発想が不可欠です。

スポンサーリンク

MVCデザインパターン

近年は、プログラミング言語とともに提供されるフレームワークが、MVCデザインパターンを採用しているものも多くあります。MVCとは、

  • Model・・・アプリケーションが扱うデータとロジック(処理)
  • View・・・アプリケーションの状態を目に見える形で提供するもの。画面表示など。
  • Controller・・・ユーザーからのキーボードやマウスによる入力を処理する機構

という3つの要素を明確に分離するプログラミング手法のことです。

このように分離されたプログラムは理路整然としていて、どれだけプログラムの規模が大きくなっても複雑度がそれほど大きくならず、破綻しにくいようになっています。

public class MyController : Controller
{
    //ControllerがWebアクセス(=ユーザーからの入力)を受け付ける
    [HttpGet]
    [Route("~/Result/{requestID}")]
    public ActionResult Result(string requestID)
    {
        var model = new MyModel();
        model.SomeAction(requestID); //Modelに対して何らかの処理
        return View(model); //Modelを表現するViewを生成する
    }
}

この例では、フレームワークがController(System.Web.Mvc.Controller)という基本クラスを持っていて、MVCデザインパターンに則ってプログラムを記述する方法をプログラマーに提供しています。

しかし、MVCを採用していないフレームワークをベースとする場合や、旧来のプログラミング言語を使うケースも多々あると思います。

そんなとき、ガッチガチにオレオレMVCを構築するのは難しいとしても、データを中心に考えるという発想を持っていれば、プログラムの破綻は未然に防げます。

画面表示はデータの像にすぎない

データ中心

3種類の手の形を表わすデータを扱うアプリケーションを考えてみます。1つは握りこぶし、1つは指を2本突き出した形、1つは手を開いた形。私の記憶が確かならば、古来よりこの3種類の手の形で勝負をする拳法を、じゃんけんと呼ぶらしいです。

さて、まずこのデータを表現するクラスを上図の中央のように定義します。MVCの用語で言うならModelになります。ここで重要なのは、データの表現の仕方を今使っているプログラミング言語で最も表現しやすい形にするということです。そして、最も根源的なデータの形にするということもまた重要です。

3種類の手の形は、人間界では「グー」「チョキ」「パー」などという文字列で表現されたりもしますが、プログラミング言語で最も表現しやすい根源的な形は

public enum HandEnum
{
    Rock,
    Scissors,
    Paper
}

という列挙型でしょう。仮に「グー」を「石」と呼んだり表示したりする場合があっても、このようにHandEnum.Rockという根源的に定義された値で識別するようにしておけば、呼び方や表示が変わっても影響を受けません。

また、画面にどんな方法で表示するか(画像を使うとか動画を使うとか)や、データファイル上にどんな値で保存するか(1,2,3などの数字を保存するか)に関しても影響を受けません。

常に、データが最も中心で根源的だと考えます。

そして、画面に表示されている手の画像は、HandEnum.Scissorsというデータの像にすぎません。HandEnum.Scissorsという最も根源的なデータの表現方法の1つが、この手の画像というわけです。

入出力は常にデータを経由する

さて、画面上のチョキの絵がクリックされたことによって、自分が選択している画像をチョキに変えたいとしましょう。まずはダメな例から。

//ダメな例
private void ButtonScissors_Click(object sender, EventArgs e)
{
    myPlayerHandImage = Resource("Scissors.png");
}

チョキがクリックされたとき、画面に表示されている自分の手の画像をチョキに変えています。しかしこれはデータ(Model)を経由しておらず、データと画面表示の状態に不整合が起きてしまいます。

データを中心に考えるなら、必ずデータを経由するようにします。

//良い例
private void ButtonScissors_Click(object sender, EventArgs e)
{
    myPlayer.State = HandEnum.Scissors;
    ReDrawMyPlayer();
}
private void ReDrawMyPlayer()
{
    switch(myPlayer.State)
    {
        case HandEnum.Rock:
            myPlayerHandImage = Resource("Rock.png");
            break;
        case HandEnum.Scissors:
            myPlayerHandImage = Resource("Scissors.png");
            break;
        case HandEnum.Paper:
            myPlayerHandImage = Resource("Paper.png");
            break;
        default:
            myPlayerHandImage = Resource("Blank.png");
            break;
    }
}

ボタンクリックによって、myPlayer.Stateというデータを書き換えます。そして、現在のデータの状態を画面表示(再描画)するよう、ReDrawMyPlayer()に委ねます。一旦データを経由して、画面(View)に画像表示を指示する形です。このようにしていればいつも、データと画面表示の状態は必ず一致しています。

フレームワークによっては、ReDrawMyPlayerのように明示的に再描画を指示しなくていいものもあります。例えばSwiftUIの場合、

struct ContentView: View {
    @State var user = User()
    var body: some View {
        Button(action : {
            self.user.hand = .scissors
            //@Stateで定義されたuserに値を代入するとき、
            //内部では"setter"という処理が呼ばれ、
            //self.viewGraph.reander()のような
            //再描画処理が内部で自動的に実行される
        })
        //以下略・・・
    }
}

このようにViewに何も指示を与えなくても、自動的に現在のModelの状態が再描画されるようになっています。MVCに則ったフレームワークでは、プログラマーがMVCをあまり意識しなくていいようにうまく内部にMVCの機構を隠蔽しています。このような機構が存在しないフレームワークや言語でプログラムを書くときは、自前でReDrawMyPlayerのような処理を行いましょう。自前であっても、立派なMVCです。

 

現在のデータを取得するときも同様です。

//ダメな例
if (myPlayerHandImage == Resource("Scissors.png"))
    MessageBox.Show("チョキ");
//良い例
if (myPlayer.State == HandEnum.Scissors)
    MessageBox.Show("チョキ");

現在の状態を画面から直接取得するのはよろしくありません。「画面に表示されている画像がScissors.pngという画像ファイルと同じならば」という判断方法は、「画像」という2次的で複雑なものを判断するという非常にコストがかかる方法なので、よろしくありません。上に別の画像がかぶさっているなどの理由で画像が表示されていないとき、現在状態がチョキであるにもかかわらず、チョキであると判断されない可能性もあります。

そうではなく、必ず現在のデータの状態は、データそのものから取得するようにします。この例の場合、現在の手の状態はmyPlayer.Stateというデータ(Model)に格納されています。これが今画面上にどのように表示されているかを気にする必要はありません。常にデータこそが中心で、データこそが1次的な真の現在状態であるからです。

シリアライズ

アプリケーションのデータの状態は、ファイルやデータベースに保存することがあります。これをシリアライズ(永続化)といいます。逆にファイルやデータベースから読み込んで復元することをデシリアライズといいます。

ファイルなどにデータを保存しておけば、アプリケーションを一旦終了しても、次回起動時に同じ状態から再開できます。複数のデータの状態をファイル名を変えて保存する、というようなこともできます。

ファイルやデータベースの中身は文字や数字の羅列になっていて、「何バイト目に何のデータ」などのような取り決めがある場合もあります。

一方、アプリケーションで扱うデータというのは、使用しているプログラミング言語で最も扱いやすいデータ表現方法になっているべきです。なので、シリアライズとデシリアライズの段階で必ずデータ形式の変換が行われます。少し面倒かもしれませんが、これを怠らないようにしましょう。

//データ形式の変換を怠ったダメな例
class MyClass
{
    private byte[] Data;
    public void Init(string fileName)
    {
        this.Data = File.Read(fileName);
    }
}

シリアライズされたファイルから読み込んだバイト配列を、そのままデータとして保持するという暴挙。こんなバイト配列、扱いにくいったらありゃしない。

考え方次第でMVCは実現できる

フレームワークがMVCデザインパターンを用意していなくても、データを中心に考えるという発想を忘れずにいれば、MVCが持つ利点と同じようなものは実現できます。

プログラミングの最初のチュートリアルを終えてイザ中規模なアプリケーションを作ろうと思い立ったとき、少し記述する行数は増えるかもしれませんが、データを中心に考える発想を忘れないようにしましょう。そうすれば、プログラムが破綻してお蔵入りになるという残念な事態は未然に防げると思います。

スポンサーリンク