squirrel_code @ ウィキ

Double dispatch がめんどくさい

最終更新:

squirrel_code

- view
管理者のみ編集可

Double dispatch がめんどくさい

last update: 2013/09/04 (Wed)

ヤブから棒ではあるが,

Rectangle rectangle;
Circle circle;

PenSVG pen_svg;
PenPNG pen_png;

rectangle.drawWith(pen_svg);
circle.drawWith(pen_svg);

rectangle.drawWith(pen_png);
circle.drawWith(pen_png);

…というようなことがしたい.要は四角形とか円とか,ひょっとしたら楕円とか菱形とかいろんな図形があって, PNG とか SVG とか,ひょっとしたら JPEG とかに出力するペンがあって,どの図形も,どのペンでも自在に組み合わせて描きたい,という意味である.

その1

簡単にプログラムを書くとこんな感じ.

// Shape.hpp

class PenToPNG;
class PenToSVG;

class Rectangle
{
    public:
    void
    drawWith(PenToPNG & pen);

    void
    drawWith(PenToSVG & pen);
};

class Circle
{
    public:
    void
    drawWith(PenToPNG & pen);

    void
    drawWith(PenToSVG & pen);
};

// Shape.cpp
#include "Shape.hpp"
#include "Pen.hpp"
#include <iostream>

void
Rectangle::drawWith(PenToPNG & pen)
{
    std::cout << "Rectangle: PNG" << std::endl;
}

void
Rectangle::drawWith(PenToSVG & pen)
{
    std::cout << "Rectangle: SVG" << std::endl;
}

void
Circle::drawWith(PenToPNG & pen)
{
    std::cout << "Circle: PNG" << std::endl;
}

void
Circle::drawWith(PenToSVG & pen)
{
    std::cout << "Circle: SVG" << std::endl;
}

// Pen.hpp
class PenToPNG
{
};

class PenToSVG
{
};

// Pen.cpp
// no line

画像フォーマット云々に踏み込むつもりはないので,関数の中身はテキスト出力で代用した.たった2種類の図形,2種類のペンではあるが,なかなかに長い.そして似たような行が延々と並ぶ.そして図形の種類やペンの種類を増やすときはとりあえず 4yy C-w l p C-v jjj :'<,'>s/Rectangle/Triangle/g ...

なんてことは間違ってもしてはいけない…のだが,それ以前に SVG とか PNG とか,特定の画像フォーマットに依存するコードが図形クラスの中で実装してあるのがなんとも気持ちが悪い.まずはここをなんとかしよう.

その2


// Shape.hpp
class PenToPNG;
class PenToSVG;

class Rectangle
{
    public:
    void
    drawWith(PenToPNG & pen);

    void
    drawWith(PenToSVG & pen);
};

class Circle
{
    public:
    void
    drawWith(PenToPNG & pen);

    void
    drawWith(PenToSVG & pen);
};

// Shape.cpp
#include "Shape.hpp"
#include "Pen.hpp"

void
Rectangle::drawWith(PenToPNG & pen)
{
    pen.draw(*this);
}

void
Rectangle::drawWith(PenToSVG & pen)
{
    pen.draw(*this);
}

void
Circle::drawWith(PenToPNG & pen)
{
    pen.draw(*this);
}

void
Circle::drawWith(PenToSVG & pen)
{
    pen.draw(*this);
}

// Pen.hpp
class Rectangle;
class Circle;

class PenToPNG
{
    public:
    void
    draw(Rectangle & rectangle);

    void
    draw(Circle & rectangle);
};

class PenToSVG
{
    public:
    void
    draw(Rectangle & rectangle);

    void
    draw(Circle & rectangle);
};

// Pen.cpp
#include "Pen.hpp"
#include "Shape.hpp"
#include <iostream>

void 
PenToPNG::draw(Rectangle & rectangle)
{
    std::cout << "Rectangle: PNG" << std::endl;
}

void 
PenToPNG::draw(Circle & rectangle)
{
    std::cout << "Circle: PNG" << std::endl;
}

void 
PenToSVG::draw(Rectangle & rectangle)
{
    std::cout << "Rectangle: SVG" << std::endl;
}

void 
PenToSVG::draw(Circle & rectangle)
{
    std::cout << "Circle: SVG" << std::endl;
}

これで画像の詳細に関わる実装がペンクラスに移動した.さて,Rectangle も Circle も drawWith メソッドを持ち,PenToSVG も PenToPNG も draw メソッドを持っている.同じ操作を受け付けるということは,基底クラスを作ってやることで,ぽりもーふぃっく(ひらがな)できる可能性があるわけだ.そこで「一般の図形」を表すクラス Shape と一般のペンを表すクラス Pen をつくり, Rectangle と Circle を Shape の, PenToSVG と PenToPNG を Pen の派生クラスにしてみよう.ついでにリファレンスもポインタに変えておく.

その3


// Shape.hpp
class PenToPNG;
class PenToSVG;

class Shape
{
    public:
    virtual
    ~Shape()
        { }

    virtual void
    drawWith(PenToPNG * pen) = 0;

    virtual void
    drawWith(PenToSVG * pen) = 0;
};

class Rectangle
    : public Shape
{
    public:
    virtual void
    drawWith(PenToPNG * pen) final override;

    virtual void
    drawWith(PenToSVG * pen) final override;
};

class Circle
    : public Shape
{
    public:
    virtual void
    drawWith(PenToPNG * pen) final override;

    virtual void
    drawWith(PenToSVG * pen) final override;
};

// Shape.cpp
#include "Shape.hpp"
#include "Pen.hpp"

void
Rectangle::drawWith(PenToPNG * pen)
{
    pen->draw(this);
}

void
Rectangle::drawWith(PenToSVG * pen)
{
    pen->draw(this);
}

void
Circle::drawWith(PenToPNG * pen)
{
    pen->draw(this);
}

void
Circle::drawWith(PenToSVG * pen)
{
    pen->draw(this);
}

// Pen.hpp
class Rectangle;
class Circle;

class Pen
{
    public:
    virtual
    ~Pen()
        { }

    virtual void
    draw(Rectangle * rectangle) = 0;

    virtual void
    draw(Circle * rectangle) = 0;
};

class PenToPNG
    : public Pen
{
    public:
    virtual void
    draw(Rectangle * rectangle) final override;

    virtual void
    draw(Circle * rectangle) final override;
};

class PenToSVG
    : public Pen
{
    public:
    virtual void
    draw(Rectangle * rectangle) final override;

    virtual void
    draw(Circle * rectangle) final override;
};

// Pen.cpp
#include "Pen.hpp"
#include "Shape.hpp"
#include <iostream>

void 
PenToPNG::draw(Rectangle * rectangle)
{
    std::cout << "Rectangle: PNG" << std::endl;
}

void 
PenToPNG::draw(Circle * rectangle)
{
    std::cout << "Circle: PNG" << std::endl;
}

void 
PenToSVG::draw(Rectangle * rectangle)
{
    std::cout << "Rectangle: SVG" << std::endl;
}

void 
PenToSVG::draw(Circle * rectangle)
{
    std::cout << "Circle: SVG" << std::endl;
}

見ればわかるが, Shape クラスは多少簡単にすることができる.

// Shape.hpp
class Pen;

class Shape
{
    public:
    virtual void
    drawWith(Pen * pen) = 0;
};

class Rectangle
    : public Shape
{
    public:
    virtual void
    drawWith(Pen * pen) final override;
};

class Circle
    : public Shape
{
    public:
    virtual void
    drawWith(Pen * pen) final override;
};

// Shape.cpp
#include "Shape.hpp"
#include "Pen.hpp"

void
Rectangle::drawWith(Pen * pen)
{
    pen->draw(this);
}

void
Circle::drawWith(Pen * pen)
{
    pen->draw(this);
}

使うときはこんな感じ.

Shape * rectangle = new Rectangle();
Shape * circle = new Circle();

Pen * pen_svg = new PenToSVG();
Pen * pen_png = new PenToPNG();

rectangle->drawWith(pen_svg);
circle->drawWith(pen_svg);
rectangle->drawWith(pen_png);
circle->drawWith(pen_png);

これで Shape::drawWith の呼び出しで自動的にそれぞれの図形やペンに対応した処理がなされる.これを条件分岐で実装すると if 文がネストして大変なことになるが,クラス間をたらいまわしにするこの手のテクを使えば美しく実装することができる.これを「ダブルディスパッチ」と呼ぶ.

つづく.

&trackback()

参考



コメント

  • コメントの投稿テスト -- (tossy_squirrel) 2010-12-29 03:35:18
名前:
コメント:

すべてのコメントを見る


関連ページ



関連ブログ

#blogsearch
記事メニュー
目安箱バナー