Entry

プログラミングメモ - 画像ファイル読み込みクラスの設計例

2009年07月11日

先日,DIB (ビットマップ)ファイルの読み込みクラスを書いたんですけれど,これはちと作りとしてマズいと思ったので,ちゃんと作った場合の設計例を簡単に。

前回作ったクラスを大まかに再現すると,こんなクラスだったのでした。

class Bitmap {
public:
    Bitmap();
    Bitmap(const std::string& filename) throw (std::exception);
    virtual ~Bitmap();
public:
    // ファイルを読み込む
    void read(const std::string& filename) throw (std::exception);
    // ファイルを書き込む
    void write(const std::string& filename) throw (std::exception);
public:
    // 点を描く
    void setPixel(int x, int y, unsigned long color);
    // 点を読む
    unsigned long getPixel(int x, int y) const;
    // 幅を取得
    int getWidth() const;
    // 高さを取得
    int getHeight() const;
private:
    int width_;
    int height_;
    unsigned long* pixels_;
};

Bitmap::read() の引数である filename には,DIB ファイルの名前を指定します。この関数は,与えられたファイル名のファイルが存在することや,それが DIB ファイルであることなんかを確認して,画像のメタ情報やピクセル情報をメンバに読み込むことになります。

しかし,これは汚い。どうしてかというと,データを生成する関数が大きくなりすぎてしまうからです。前回紹介したのは,かなり簡単な関数だったけれども,ちと本格的に作ると,かなり複雑になってしまいます。こゆのは,読み込み用のクラスを作った方がいい。

class BitmapReader {
    // (略)
    void read(const std::string& filename, Bitmap& bitmap)
        throw (std::exception);
};

class BitmapWriter {
    // (略)
    void write(const std::string& filename, Bitmap& bitmap)
        throw (std::exception);
};

ファイル名と先ほどの Bitmap オブジェクトを引数に与えると,Bitmap オブジェクトに読み込んだ結果が格納されます。ファイル名はオブジェクトの作成時に初期化してもいいかもしれません。しかし,これもマズい。どこがマズいのか。

まずこれ,Bitmap オブジェクトにアクセスする方法が限られちゃうんですね。Bitmap オブジェクトには setPixel() というアクセサがあるんですけれど,これは1ピクセル単位で値を設定するので,ファイルの情報を読み込む場合のように,いっぺんにデータを読み込む場合は効率が悪いです。それじゃ,メンバの pixels_ を返すアクセサを作ればいいじゃないか,ともなりそうなんですけど,そうすると外側で勝手に delete されちゃう危険がある。難儀なんですね。

この方法を使う場合,BitmapReader クラスに対する friend 関数を作ることもできますけど,やはりあまりスマートじゃない。

じゃどうするかというと,読み込みメソッドを Bitmap オブジェクト側に持たせて,BitmapReader オブジェクト側に読み込みメソッドの本体を持たせればいい。こんな感じになるんでしょうか。

class BitmapReader {
    // (略)
    void read(unsigned long** pixels) throw (std::exception) {
        // 読み込みメソッド本体
    }
};

class BitmapWriter {
    // (略)
    void write(unsigned long** pixels) throw (std::exception) {
        // 書き込みメソッド本体
    }
};

class Bitmap {
public:
    // (略)
public:
    // ファイルを読み込む
    void read(BitmapReader& reader) throw (std::exception) {
        reader.read(&pixels_);
    }
    // ファイルを書き込む
    void write(BitmapWriter& writer) throw (std::exception) {
        writer.write(&pixels_);
    }
public:
    // (略)
private:
    // (略)
};

しかし,これでもマズい。どうしてかというと,拡張性がないんですね。例えば,ファイルの種類を増やそうと思ったら,この設計だとどうなるか。

読み込み先の情報(width_,pixels_ など)は同じもので,異なるのは「読み込み方」ですから,クラスを増やせばよさそうです。例えば,DIB に加えて,JPEG ファイルと PNG ファイルを読ませるようにしてみましょう(*Writer は省略)。

// DIB ファイル用
class DIBReader {
    // (略)
public:
    void read(unsigned long** pixels) throw (std::exception) {
        // 読み込みメソッド本体
    }
};

// JPEG ファイル用
class JPEGReader {
    // (略)
public:
    void read(unsigned long** pixels) throw (std::exception) {
        // 読み込みメソッド本体
    }
};

// PNG ファイル用
class PNGReader {
    // (略)
public:
    void read(unsigned long** pixels) throw (std::exception) {
        // 読み込みメソッド本体
    }
};

class Bitmap {
public:
    // (略)
public:
    // DIB ファイルを読み込む
    void read(DIBReader& reader) throw (std::exception) {
        reader.read(&pixels_);
    }
    // JPEG ファイルを読み込む
    void read(JPEGReader& reader) throw (std::exception) {
        reader.read(&pixels_);
    }
    // PNG ファイルを読み込む
    void read(PNGReader& reader) throw (std::exception) {
        reader.read(&pixels_);
    }
public:
    // (略)
private:
    // (略)
};

しかし,これも汚い。どうしてかというと,ファイルの種類が増えるたびにメソッドを増やさないといけないからです。つか,中身同じだから作る意味がねい。

お気づきの通り,こゆ場合は,継承させた方がいいですよね。

// 親クラス
class BitmapReader {
protected:
    BitmapReader();
public:
    virtual ~BitmapReader();
public:
    // 読み込み用インターフェイス
    virtual void read(unsigned long** pixels) throw (std::exception) = 0;
};

// DIB ファイル用
class DIBReader : public BitmapReader {
    // (略)
public:
    virtual void read(unsigned long** pixels) throw (std::exception) {
        // 読み込みメソッド本体
    }
};

// JPEG ファイル用
class JPEGReader : public BitmapReader {
    // (略)
public:
    virtual void read(unsigned long** pixels) throw (std::exception) {
        // 読み込みメソッド本体
    }
};

// PNG ファイル用
class PNGReader : public BitmapReader {
    // (略)
public:
    virtual void read(unsigned long** pixels) throw (std::exception) {
        // 読み込みメソッド本体
    }
};

class Bitmap {
public:
    // (略)
public:
    // ファイルを読み込む
    void read(BitmapReader& reader) throw (std::exception) {
        reader.read(&pixels_);
    }
public:
    // (略)
private:
    // (略)
};

こうすれば,Bitmap クラスは何も変更しなくても,クラスを新しく作るだけで複数の種類の画像ファイルを読めるようになります。

最後に,メソッドを少し変えてみましょうか。これは好みにもよるんでしょうけど,読み込み用のオブジェクトを読み込み先のオブジェクトメソッドの引数にするのは,なんか変な感じ。operator=() にオーバーライドしてみます。

class Bitmap {
public:
    // (略)
public:
    // ファイルを読み込む
    Bitmap& operator=(BitmapReader& reader) throw (std::exception) {
        reader.read(&pixels_);
    }
public:
    // (略)
private:
    // (略)
};

とりあえず,こんなところだろうか。書き込みはどうすんだろう。どうすんだ?

Trackback
Trackback URL:
Ads
About
Search This Site
Ads
Categories
Recent Entries
Log Archive
Syndicate This Site
Info.
クリエイティブ・コモンズ・ライセンス
Movable Type 3.36
Valid XHTML 1.1!
Valid CSS!
ブログタイムズ

© 2003-2012 AIAN