14
10

More than 1 year has passed since last update.

ソフト設計に疎いMATLABユーザーがクラスについてイメージを掴んだ後に読む記事

Last updated at Posted at 2023-01-03

まえがき

前回はオブジェクト指向について、イメージを掴むだけの記事を投稿しました。

今回はそこから少し発展して、プログラムを本職としない人にもオブジェクト指向設計を少し体感してもらおうと思います。

そのためにクラスでもうひとつ覚えるべき機能があります。
それが継承です。

継承とは

継承は言葉で説明すると、既存のクラスの変数やメソッドを流用することです。
イメージを掴まないといけないので、具体例で説明します。

まず、あなたの手元にりんごがあり、そのリンゴは質量をもっています。これがリンゴクラスです。

あなたはふとりんごを運動させたいと考えます。その時にはりんごの質量では情報が不十分です。従って、りんごクラスに位置・速度・加速度・力の変数を追加したいです。しかし、りんごクラスは運動を表すものではなく、単にりんごの属性を示すクラスなため、そこに運動に係る変数を追加するのははばかられます。

そこで、りんごクラスの情報を流用したりんご運動クラスをつくります。このクラスは、りんごクラスの機能をまるまるもっており、その上追加の変数やメソッドを追加できますが、りんごクラスとは別物になるため、レイヤーの整理も完璧ですね。

基本の設計思想

なんとなくイメージできたでしょうか?
しっかり説明するとこうなります。

まずりんごクラスはりんご運動クラスよりも抽象的なクラスで、質量という運動以外にも使えそうな汎用的な変数を持っています。りんご運動クラスはそれよりは機能を絞って、運動を記述するためのクラスですので、抽象概念のりんごクラスを引き継ぎ、運動記述に必要な変数やメソッドを持っています。

このような形で、抽象オブジェクトを継承して具体的なオブジェクトを作る。この思想で継承は設計されます。

ですので、全てのソフトにおいて継承が必要かというとそうではありません。

例えばプログラム設計の最中に、りんごジャム作成クラスと、りんご運動クラスが混在した時、おそらくどちらのクラスにも質量がいるでしょう。そんな時、同じりんごを表すクラスの共通部分(ここでいう質量)は切り離して抽象オブジェクトにして、ジャムと運動はそれぞれ継承を使ってクラスを作成すれば、同じメソッドを何回も組む手間がなくなりますし、レイヤーも整います。

しかし、そういった事情がない段階では、無理に継承を使った設計をする必要はないです。必要になったらその時に作ればいいのです。

MATLABで継承クラス設計

実践です。まずはりんごクラスを作ってみましょう。メンバーは質量と、その初期化をする関数のみです。
正直これくらいならわざわざ抽象クラスは作らなくてもいい気がしますが、説明のために簡単なクラスにしてます。

Apple.m
classdef Apple
    properties (SetAccess = private)
        might double = 1;
    end

    methods
        function obj = SetMight(obj,newMight)
            obj.might = newMight;
        end
    end
end

そして、りんごクラスを継承して作ったりんご運動クラスはこんな感じです。

AppleMove.m
classdef AppleMove < Apple
    properties (SetAccess = private)
        dt = 1e-3; % シミュレーションの時間ステップ
    end

    properties (SetAccess = private)
        % 位置・力・速度・加速度をメンバーに追加。
        pos (2,1) double = [0;0];
        forces (2,1) double = [0;0];
        velocity (2,1) double = [0;0];
        accel (2,1) double = [0;0];
    end
    
    % クラス内でしか使わない関数はprivateメソッドにしておく。
    methods (Access = private)
        function obj = UpdateAccel(obj)
            obj.accel = obj.forces / obj.might;
        end

        function obj = UpdateVelocity(obj)
            obj.velocity = obj.velocity + obj.accel * obj.dt;
        end

        function obj = UpdatePos(obj)
            obj.pos = obj.pos + obj.velocity * obj.dt;
        end

        function obj = Update(obj)
            obj = obj.UpdateAccel();
            obj = obj.UpdateVelocity();
            obj = obj.UpdatePos();
        end
    end

    methods
        function obj = SetInitPos(obj, initPos)
            obj.pos = initPos;
        end

        function obj = SetInitVelocity(obj, initVel)
            obj.velocity = initVel;
        end

        function obj = SetDt(obj, newDt)
            obj.dt = newDt;
        end

        function obj = SetForce(obj, newForce)
            obj.forces = newForce;
            obj = obj.Update();
        end
    end
end

クラス定義のclassdef AppleMove < Apple、これでAppleをAppleMoveが継承する定義ができます。
メインコードではAppleMoveを作ればAppleクラスのメンバである質量や関数も扱えます

ちなみにですが、クラスのメンバーのアクセス権にはprivate, public, protectedがあります。これらはそれぞれこんな意味です。

アクセス権 意味
private そのクラス自身にしか編集・呼び出し不可能
protected そのクラスと継承先のクラスは編集・呼び出し可能
public 誰でも編集・呼び出し可能

オブジェクト指向の考え的にはできるだけprivateで作っておいて、仕方ないものだけprotectedやpublicに設定するといった作り方を通常はします。

テスト

試しに自由落下運動のメインコードを書いてみます。

FreeFall.m
g = [0;-9.8];
m = 2;
force = m * g;
dt = 1e-3;

ag = AppleGravity;
ag = ag.SetMight(m);
ag = ag.SetInitPos([0;10]);
ag = ag.SetInitVelocity([2;3]);
ag = ag.SetDt(dt);

tend = 1;
x = [];
y = [];
for t = 0:dt:tend
    ag = ag.SetForce(force);
    x = [x, ag.pos(1)];
    y = [y, ag.pos(2)];
end

これでx,yについて確認するとこんな感じです。

image.png

メインコードは非常にシンプルですが、落下運動のシミュレーションができてしまいました。
クラスを作ってあるので、他のコードでりんごを使いたい時も、同じようにクラスを呼び出してコードを組めば、りんごの再設計の必要なくプログラムできます。

※雑な計算の割にはしっかり落下運動させられました。

神は我らに自我を与えた。

大規模なプログラムの場合、大抵は意味のまとまりごとに、コードのほとんどをクラスにします。

前回記事で、オブジェクト指向とは、変数や関数たちを神の管理からオブジェクトの管理にすることで、神気取りのプログラマの負担を減らすことだ、とは言ってませんが大体そんなことを主張しました。

いわば神の手を離れて個々のオブジェクトに自我を与えたようなものです。(違う気もする)

これを極めると、神が管理しなければならないことなんてほんの少しになります。結果、ほとんどのものがクラス化されるのです。

正直ここまでくると、多くの研究者や開発者には必要ない考えかもしれませんが、何かしらのアプリを作るときなどは必ず持っておいた方がいい考え方なので、少し体験しましょう。

万有引力空間クラスを作る

りんごを空間に浮かべて動きをシミュレーションしてみましょう。

そのために、りんごをいくつかもつ万有引力のクラスを作成します。こんな感じにしてみました。

UniversalGravitation.m
classdef UniversalGravitation
    properties (SetAccess = private)
        apples (1,:) AppleMove
    end

    methods (Static)
        % 万有引力
        function force = GetForce(pos1, might1, pos2, might2)
            dif = pos2 - pos1;
            r = vecnorm(dif,2,1);
            G = 6.67259e-11;
            force = (dif ./ r) * G * (might1 * might2) / r^2;
        end
    end

    methods (Access = public)
        % 指定した数のりんごを空間に召喚
        function obj = UniversalGravitation(numApple)
            obj.apples(1,numApple) = AppleGravity;
        end

        % りんごの質量を設定する
        function obj = SetInitAppleMight(obj, newMights)
            if numel(obj.apples) ~= numel(newMights)
                error("りんごの数と質量ベクトルの要素数が一致しません。")
            end

            for idx = 1:numel(obj.apples)
                obj.apples(idx) = obj.apples(idx).SetMight(newMights(idx));
            end
        end

        % りんごの位置を設定する
        function obj = SetInitApplePos(obj, newPoses)
            arguments
                obj UniversalGravitation
                newPoses (2,:) double
            end

            if numel(obj.apples) ~= size(newPoses,2)
                error("りんごの数と位置ベクトルの要素数が一致しません。")
            end

            for idx = 1:numel(obj.apples)
                obj.apples(idx) = obj.apples(idx).SetInitPos(newPoses(:,idx));
            end
        end

        % りんごの初期速度を設定する。
        function obj = SetInitAppleVelocity(obj, newVelocity)
            arguments
                obj UniversalGravitation
                newVelocity (2,:) double
            end

            if numel(obj.apples) ~= size(newVelocity,2)
                error("りんごの数と速度ベクトルの要素数が一致しません。")
            end

            for idx = 1:numel(obj.apples)
                obj.apples(idx) = obj.apples(idx).SetInitVelocity(newVelocity(:,idx));
            end
        end

        % 時間ステップ設定
        function obj = SetDt(obj, dt)
            for idx = 1:numel(obj.apples)
                obj.apples(idx) = obj.apples(idx).SetDt(dt);
            end
        end

        % りんごを更新する
        function obj = Update(obj)
            positions = [obj.apples.pos];
            mights = [obj.apples.might];
            forces = 0 .* positions;
            
            for idx = 1:numel(obj.apples)
                for idy = 1:numel(obj.apples)
                    if idx == idy
                        continue;
                    end

                    forces(:,idx) = forces(:,idx) + obj.GetForce(positions(:,idx),mights(idx),positions(:,idy),mights(idy));
                end
            end

            for idx = 1:numel(obj.apples)
                obj.apples(idx) = obj.apples(idx).SetForce(forces(:,idx));
            end
        end
    end
end

methods (Static)とは、オブジェクトを引数や出力に持たないただの関数のことです。引力の計算はここにおいてます。

このクラスでは、りんご間の引力を計算して、個々のりんご運動クラスにその引力によるりんごの運動を計算させます。

メインコード

メインでは万有引力空間の設定をして、決まったサイクル数ループさせればシミュレーション完了です。

Newton.m
num = 2;
ug = UniversalGravitation(num);
ug = ug.SetInitAppleMight(rand(1,num)*1e8 + 1e8);
ug = ug.SetInitApplePos((rand(2,num)-0.5)*10);
ug = ug.SetInitAppleVelocity((rand(2,num)-0.5)*0.1);

dt = 0.5;
ug = ug.SetDt(dt);

for t = 0:dt:1000
    ug = ug.Update();
end

いろんなパラメータ設定が適当ですが、とりあえずこんな運動をシミュレーションできました。
黒線が速度ベクトルで、緑線が加速度ベクトルです。位置と速度・加速度のプロットは1サイクルズレたものなので、加速度が整体してないのはそのせいです。

newton.gif

image.png

まとめ

今回は、クラスを使ったオブジェクト指向設計について、初心者から一歩進むために継承を解説し、ついでにソフト設計について少し触れました。

りんご運動クラスを作って使いまわせば、りんごのコーディングを改めてしなくても、自由落下のシミュレーションから宇宙での軌道の計算までできてしまいました。異なるシステム間で共通のオブジェクトを使いまわせること、これがオブジェクト指向の利点です。

ここまでの機能を使わないといけない人は正直そんなに多くないのではと思いますが、重要なことは

  • なんでも自分で管理しようとするな。あなたはただの人間であり、能力的にも大したことないことを知ろう。
  • りんごの再設計をしないためにクラスを作ろう。
  • 継承は必要に迫られたら作れば良い。なんでも継承がいいわけではない。

です。

そしてMATLABのクラスにはまだ知らない機能もあります。フル活用したい人は公式ページをチェックしてください。

14
10
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
10