🥲

オブジェクト指向の話 #1

に公開

はじめに

今回はオブジェクト指向について考えていきます。 というのも、自分はロボコンをしており、ロボコンを行っていくうえで後輩に技術の伝承?的な事を行っています。そのときにこのようなことがありました。

悲しい事件

自分と後輩Aとの会話...

自分 「cppプログラムではclassを使うから今から講習をします。」
講習後...
後輩A「classって必要あります? 結局、関数と構造体で大丈夫じゃないですか?
自分 「いや、classを使うと簡潔に書けるし、理解しやすいから...」
後輩A「先輩はいいかもしれないですけど、正直classの必要性感じないです。

自分はオブジェクト指向が大好きですし、classが非常に便利という信念を持っているので、今までclassを使うことに疑問や不満を持つことはなかったのですが、初心者にオブジェクト指向を教えてもあまりいい反応をしてくれません。

オブジェクト指向の意義

ぶっちゃけオブジェクト指向なくてもかける

正直オブジェクト指向を使うことができなくてもプログラムは書けるし、動きます。 最近は関数型プログラミングが流行っているし、工学的にはMATLABのような関数を記述することでシミュレーションをすることの方が多くオブジェクト指向が必須というわけではありません。(何ならオブジェクト指向で書くと逆に複雑になったりすることも...)

じゃあ時間の無駄じゃね?

自分的にはオブジェクト指向を学ぶということは一手段が増えることに過ぎません。
しかし、実際オブジェクト指向を使ったコードは星の数ほどあり、オブジェクト指向が分からないとコードを全く持って読めないなんてこともあります。 それらが読めるようになることでインプットできる情報量が数倍になることもあります。
オブジェクト指向を学ぶ、使うことはプログラミング能力を大幅に向上させます。

どのぐらい違うのか

オブジェクト指向の便利さを率直に見せることが一番なので、今回はラジコンカーの制御についての仮想のmbedプログラムを書いてみます。(mbedについてはこちら ,実際のプログラムではないので動きません)
ラジコンカーはエンコーダー付き独立二輪、無線モジュールはi2cでデータを取得するものとします。

オブジェクト指向なし

#include "mbed.h"

#define ppr 200

I2C Controller(D4,D5);

struct Encoder{
    int rAState = 0;
    int rB = 0;
    int lAState = 0;
    int lB = 0;
}typedef Encoder;

Encoder encoder;

PwmOut rightSpeed(D2);//motor right speed
PwmOut leftSpeed(D3); //motor left speed
InterruptIn rightEncoderA(D6);
InterruptIn rightEncoderB(D7);
InterruptIn leftEncoderA(D8);
InterruptIn leftEncoderB(D9);

Timer T;

void rightArise(){encoder.rAState = 1;}
void rightAfall(){encoder.rAState = 0;}
void rightBrise(){
    if(encoder.rAState)
        encoder.rB++;
    else
        encoder.rB--;
}
void leftArise(){encoder.lAState = 1;}
void leftAfall(){encoder.lAState = 0;}
void leftBrise(){
    if(encoder.lAState)
        encoder.lB++;
    else
        encoder.lB--;
}

int main(void){
    rightEncoderA.rise(&rightArise);
    rightEncoderA.fall(&rightAfall);
    rightEncoderB.rise(&rightBrise);
    leftEncoderA.rise(&leftArise);
    leftEncoderA.fall(&leftAfall);
    leftEncoderB.rise(&leftBrise);
    double gains[3] = {0.01,0.0001,0.01};//pidgains {p,i,d};
    double Proportal[2] = {0.0,0.0};//right left
    double Integral[2] = {0.0,0.0};
    int target[2] = {0,0};
    while(1){//main loop
        T.stop();
        double dt = T.read_us();
        T.reset();
        T.start();
        //read encoder
        int SpeedRight = encoder.rB/ppr/dt;
        int SpeedLeft = encoder.lB/ppr/dt;
        encoder.rB = 0;
        encoder.lB = 0;
        //read controller and set target speed
        char order[2];
        Controller.read(0x08<<1,order,2);
        target[0] = order[0]*(255+order[1]);
        target[1] = order[0]*(255-order[1]);
        //pid
        double diff[2] = {Proportal[0]-(target[0]-SpeedRight),Proportal[1]-(target[1]-SpeedLeft)};
        Integral[0] += (Proportal[0]+(target[0]-SpeedRight))*dt/2;
        Integral[1] += (Proportal[1]+(target[1]-SpeedLeft))*dt/2;
        Proportal[0] = target[0]-SpeedRight;
        Proportal[1] = target[1]-SpeedLeft;
        rightSpeed = gains[0]*Proportal[0]+gains[1]*Integral[0]+gains[2]*diff[0];
        leftSpeed = gains[0]*Proportal[1]+gains[1]*Integral[1]+gains[2]*diff[1];

        wait_us(1000);
    }
}

見にくいコードになってしまいました。(書くの大変でした...)
このコードを実際に動かす際にはPIDのゲインの調整を行う必要がありますが、このコードではゲインがプログラム上のどの位置にあるのかが非常に分かりにくくなっています。
また、グローバル変数の多さも問題です。なぜならば、グローバル変数が大量に存在するコードは非常に管理が難しいからです。
なぜ管理のしやすさにこだわるかというと、コード管理を行えていない状況では誤ったピンをHIGHにしてしまうなどのコード不良でロボットが想定外の動作をして壊れてしまう可能性があるためです。

オブジェクト指向あり

#include "mbed.h"

#define ppr 200

class RC_Car
{
    public:
    I2C Controller;
    PwmOut rightSpeed;
    PwmOut leftSpeed;
    InterruptIn rightEncoderA;
    InterruptIn rightEncoderB;
    InterruptIn leftEncoderA;
    InterruptIn leftEncoderB;
    Timer T;
    int rAState,rB,lAState,lB;
    double gains[3];//pidgains {p,i,d};
    double Proportal[2];//right left
    double Integral[2];
    int target[2];

    RC_Car():
        Controller(D4, D5),T(),
        rightSpeed(D2),leftSpeed(D3),rightEncoderA(D6),rightEncoderB(D7),leftEncoderA(D8),leftEncoderB(D9),
        rAState(0),rB(0),lAState(0),lB(0),
        gains{0.0,0.0,0.0},Proportal{0.0,0.0},Integral{0.0,0.0},target{0,0}
    {
        rightEncoderA.rise(callback(this,&RC_Car::rightArise));
        rightEncoderA.fall(callback(this,&RC_Car::rightAfall));
        rightEncoderB.rise(callback(this,&RC_Car::rightBrise));

        leftEncoderA.rise(callback(this,&RC_Car::leftArise));
        leftEncoderA.fall(callback(this,&RC_Car::leftAfall));
        leftEncoderB.rise(callback(this,&RC_Car::leftBrise));
    }
    void rightArise(){rAState = 1;};
    void rightAfall(){rAState = 0;};
    void rightBrise(){
        if(rAState)
            rB++;
        else
            rB--;
    }
    void leftArise(){lAState = 1;};
    void leftAfall(){lAState = 0;};
    void leftBrise(){
        if(lAState)
            lB++;
        else
            lB--;
    }
    void setGains(double p,double i,double d){
        gains[0] = p;
        gains[1] = i;
        gains[2] = d;
    }
    void setSpeed(){
        T.stop();
        double dt = T.read_us();
        T.reset();
        T.start();
        //read encoder
        int SpeedRight = (double)rB/ppr/dt;
        int SpeedLeft = (double)lB/ppr/dt;
        rB = 0;
        lB = 0;
        //pid
        double diff[2] = {Proportal[0]-(target[0]-SpeedRight),Proportal[1]-(target[1]-SpeedLeft)};
        Integral[0] += (Proportal[0]+(target[0]-SpeedRight))*dt/2;
        Integral[1] += (Proportal[1]+(target[1]-SpeedLeft))*dt/2;
        Proportal[0] = target[0]-SpeedRight;
        Proportal[1] = target[1]-SpeedLeft;
        rightSpeed = gains[0]*Proportal[0]+gains[1]*Integral[0]+gains[2]*diff[0];
        leftSpeed = gains[0]*Proportal[1]+gains[1]*Integral[1]+gains[2]*diff[1];
    }
    void getController(){//read controller and set target speed
        char order[2];
        Controller.read(0x08<<1,order,2);
        target[0] = order[0]*(255+order[1]);
        target[1] = order[0]*(255-order[1]);
    }
};

RC_Car Car;

int main(void){
    Car.setGains(0.01,0.0001,0.01);
    while (1) {
        Car.getController();
        Car.setSpeed();
        wait_us(1000);
    }
    return 0;
}

プログラム自体は長くなっていますが、main文がシンプルになっています。 また、class内の関数で分割しているためプログラムの管理がしやすくなっています。
今回は簡単なラジコンカーでしたが、実際にコード実装を行うロボットは複雑な動きが求められるためプログラムの管理のしやすさはとても重要です。

ここまで見てもclassの必要性に疑問がある方へ

悪いことは言わないのでclassを習得しておきましょう。今後rosを使う予定のある方は特にです。
なぜなら、rosの多くの入門書、記事ははじめにros上のNodeというclassの継承から始めることが多いです。(自分は他の書き方をしている物を見たことがないです)classの継承はclassの概念が分かっていないと難しくclassの知識が必須です。

Discussion