LoginSignup
88
98

More than 5 years have passed since last update.

俺流オブジェクト指向の入門

Last updated at Posted at 2014-05-07

何が言いたいか

オブジェクト指向怖くないよ ってことだけ。

注意

ここで書かれているコードに関して、一切の動作保証はいたしません。また、コードを用いて何かしら不具合等生じても責任を追わないのであしからず(当たり前ですが、念のため書きました。)

初心者的なプログラムの何が欠点か?

例えば、phpでDataBase(MySQL)をあつかうコードを見てみましょうか。

phpでDatabaseサーバーとやりとりするには一般的に次のようなプロセスを踏みます。

  1. DBサーバーのリンクを開く
  2. DBを選択する
  3. DBへクエリを送っていちゃいちゃする
  4. DBサーバーとのリンクを閉じる

旧来の方法で書いてみる

Moduleは旧来のMySQLのやつ。(MySQLiじゃないよ!)


<?php

//各種DBの接続情報
$db_host = "example.com";
$db_user = "u_hoge";
$db_pass = "password";
$db_name = "d_foo";

//DBサーバーへリンク
$db_link = mysql_connect($db_host, $db_user, $db_pass);

//リンクが正常なのかを判断
if(!$db_link){
    die("cannot connect Database: " . mysql_error());
}

//使用するDBを選択
$db_select_result = mysql_select_db($db_name, $db_link);

//使用するDBが選択出来たか判断
if(!$db_select_result){
    die("cannot select" . $db_name . ": " . mysql_error());
}

//実際にクエリを発行
$query = "SELECT * from table_hoge;";
$query_result = mysql_query($query, $db_link);
if($query_result){
    while($target_result_row = mysql_fetch_assoc()){
        print_r($target_result_row);
    }
}else{
    die("Query error: " . mysql_error());
}

//DBサーバーのリンクのクローズ(ちょっと横着)
if(!mysql_close($db_link)){
    die("cannot close Database link: " . mysql_error());
}

?>

ね、簡単でしょ。

何が問題か?

普通にこれで何か致命的なエラーとか起こるわけじゃないけど、次のような要求があった時どうコーディングしましょうか?

複数のリンクやDBをあつかう場合どう書く?

例えば別のホストにあるDBを扱いたい場合、どうすればいいでしょうか?まぁまともに考えれば、$db_hostとかで定義されている情報を書き換えれば大丈夫ですね。問題は、同時に複数の接続をオープンしなければならない場合です。同じコードを2つ書かなければいけませんね。一部の処理、ここではmysql_connect()とかmysql_select_db()などのリンク処理やそれに付随するエラー処理はfunctionでひとまとめにすれば楽ですが、それでもやはりそのfunctionから帰ってくる$db_linkの管理とかめんどくさくなります。

試しに処理をもっと使いやすいようにメソッド化してみましょう。


<?php

function database_connection($host, $user, $pass, $name){
    //DBに接続するメソッド
    $link = mysql_connect($host, $user, $pass);

    //リンクが正常なのかを判断
    if(!$link){
        die("cannot connect Database: " . mysql_error());
    }

    //使用するDBを選択
    $db_select_result = mysql_select_db($name, $link);

    //使用するDBが選択出来たか判断
    if(!$db_select_result){
        die("cannot select" . $db_name . ": " . mysql_error());
    }

    return $link;
}

function database_query($query, $link){
    //DBにQueryを実行させるメソッド
    $query_result = mysql_query($query, $link);
    if(!$query_result){
        die("Query error: " . mysql_error());
    }

    result $query_result;
}

function database_close($link){
    //DBとのリンクをクローズするメソッド
    if(!mysql_close($link)){
        die("cannot close Database link: " . mysql_error());
    }
}

?>

実際あつかうには…


<?php

//各種DBの接続情報
$db_host = "example.com";
$db_user = "u_hoge";
$db_pass = "password";
$db_name = "d_foo";

$db_link = database_connection($db_host, $db_user, $db_pass, $db_name);

$query = "SELECT * from table_hoge;";
$query_result = database_query($query, $db_link);
if($query_result){
while($target_result_row = mysql_fetch_assoc()){
    print_r($target_result_row);
    }
}

database_close($db_link);

?>

2つのDBのリンクをひらくためにはまぁ適当に書くならこんなかんじでしょうか。


<?php

//各種DBの接続情報
$connection_info["host1"] = array(
    "host" => "host1.com",
    "user" => "u_hoge1",
    "pass" => "password1",
    "name" => "d_foo1"
);
$connection_info["host2"] = array(
    "host" => "host2.com",
    "user" => "u_hoge2",
    "pass" => "password2",
    "name" => "d_foo2"
);

foreach($connection_info as $host_name => $host_info){
    //$host_nameってなんか間違えやすい名前ですね…
    $host_info["link"] = database_connection(
        $host_info["host"],
        $host_info["user"],
        $host_info["pass"],
        $host_info["name"]
    );
}

$query = "SELECT * from table_hoge;";

$query_result1 = database_query($query, $connection_info["host1"]["link"]);
if($query_result1){
while($target_result_row = mysql_fetch_assoc()){
    print_r($target_result_row);
    }
}

$query_result1 = database_query($query, $connection_info["host2"]["link"]);
if($query_result2){
    while($target_result_row = mysql_fetch_assoc()){
        print_r($target_result_row);
    }
}

database_close($connection_info["host1"]["link"]);
database_close($connection_info["host2"]["link"]);

?>

うわ、めんどくさい。2つのリンクならまだしもこれが4つとかになると…

オブジェクト指向とは

さて、本題のオブジェクト指向について考えていきましょう。Wikipedia(2014/04/26)の概要的なところにはこんなふうに書いてあります。

オブジェクト指向プログラミング(オブジェクトしこうプログラミング、英: object-oriented programming, OOP)とは相互にメッセージ (message) を送りあうオブジェクト (object) の集まりとしてプログラムを構成する技法である。

はい、まず初心者はここでつまづきますね。意味がわからないです。(編集者さんすいません)

もう少しわかりやすい説明がオブジェクト指向とは 【 OO 】 【 object oriented 】 - 意味/解説/説明/定義 : IT用語辞典にありました。

関連するデータの集合と、それに対する手続き(メソッド)を「オブジェクト」と呼ばれる一つのまとまりとして管理し、その組み合わせによってソフトウェアを構築する。

オブジェクト指向を実際あつかった経験ある人には「あーそうそう、こんな感じ」ってなると思います。とはいえ、全く知らない人には「何じゃそりゃ」っていう印象です。

考えるよりもまず、実際にコードを見てみましょう。phpをあつかう皆さんにはお馴染みのphp.netのManualの中からPHP: クラスの基礎 - Manualからコードを拝借します。


<?php
class SimpleClass
{
    // プロパティの宣言
    public $var = 'a default value';

    // メソッドの宣言
    public function displayVar() {
        echo $this->var;
    }
}
?>

このコードはclass定義と呼ばれます。classはよく「設計図」と呼ばれます。例えば、家の設計図はそれ自体は家では無いですが、「出来る家の完成形」を表現しています。classも同じで、class定義をしても、それ自体は何も意味がありません。もう少し具体的に言うと、class定義中でdisplayVar()の宣言をしていますが、このままではこのあとに続くコードでdisplayVar()を使うことが出来ません。displayVar()を使うには、先の家の例に例えれば、設計図を元に実際に家を建てなければなりません。この操作を「インスタンス化する(実体化する)」といいます。実際には次のように書きます。


$instance_class = new SimpleClass;

displayVar()を実際使うには、

$instance_class->displayVar();

こんな風に使います。出力としては

a default value

が出てきます。ちなみに、


echo $instance_class->var;

でも全く同じ出力が出てきます。

因みに->は簡単に言うと、インスタンスの中の何かを選択するときに使います。

さぁ、ここでさっきのオブジェクト指向とは 【 OO 】 【 object oriented 】 - 意味/解説/説明/定義 : IT用語辞典をこのclassを交えて説明してみます。

関連するデータの集合 (ここでは $var と、それに対する手続き(メソッド) (ここではdisplayVar() を「オブジェクト」と呼ばれる一つのまとまりとして管理し、その組み合わせによってソフトウェアを構築する。

すこし、具体的になって来ましたでしょうか?ちょうざっくり言うと、

変数、定数、functionをひとまとめにしようぜ

って個人的には理解してます。因みに、上記のphp manualのコードには既に書いてありますが、class定義の中の変数や定数は「プロパティ」と呼ばれ、関数は「メソッド」と呼ばれます。

通常の関数定義と何が違うのか

聡い読者様の中には、「関数定義中に変数が定義できるからわざわざclassを作る必要が無い」と感じられる方もいます。先ほどのphp manualの例で言えば、


<?php
function displayVar() {
    $var = 'a default value';
    echo $var;
}
displayVar();
?>

こんなかんじでしょうか。

classを作る必要がないってのは除いて、全く仰るとおりです。では、この$varはどこか別のところで再利用出来るでしょうか?$varのスコープ(変数が扱える範囲)はfunctionの中のみなので、$varfunction外で使うことは出来ません。では、グローバル変数として宣言すればいいじゃないか!って話になります。


<?php
function displayVar() {
    global $var = 'a default value';
    echo $var;
}
displayVar();
echo $var;
?>

さて、なんでもかんでもグローバル変数として宣言していいものでしょうか?私はよくないと考えます。いつまでも変数が生きているのでメモリリークが発生しますし、コーディングしている内に$varという存在を忘れていて、また同じ変数名で宣言してしまった場合、情報が上書きされてそれまでの情報が使えなくなってしまいます。使わなくなったら自動的に捨てて欲しいし、保持しておくならなるべく上書きしにくくしたいものです。

オブジェクト指向における変数の扱い

クラス間に関して

さて、関数内での変数定義に関して先のような問題点が明らかになりました。オブジェクト指向なプログラムではどのようになるでしょうか。

先ほどのphp manualのコードをインスタンス化します。


class SimpleClass
{
    // プロパティの宣言
    public $var = 'a default value';

    // メソッドの宣言
    public function displayVar() {
        echo $this->var;
    }
}
$instance_class = new SimpleClass;

インスタンス内には$varが宣言されていますが、次のようなコードを書いてもエラーになります。


echo $var;

おそらくnot defined的な、変数が定義されてないよーってエラーが出てきます。何度も書きますが、


echo $instance_class->var;

であればちゃんとa default valueが出力されます。
では次のようなコードの場合、いかがでしょうか。


<?php

class simple_class_1 {
    public $var = "value 1";
}

class simple_class_2 {
    public $var = "value 2";
}

$instance_1 = new simple_class_1;
$instance_2 = new simple_class_2;

echo $instance_1->var;
echo $instance_2->var;

?>

この場合の出力は

value 1
value 2

になります。

内部的な変数名は$varですが、内容はしっかり分けられていますね。変数名varの前に、インスタンス名のプレフィックスが自動的に付けられていると考えても良さそうです。クラス内の変数名重複はもちろん回避しないと上書きされますが、クラス間の変数名に関しては意識しないでプログラミングが出来るという利点があります。

インスタンス間

同じクラスでも、インスタンス間で変数を分けることができます。


class simple_class {
    public $var = "value 1";
}

$instance_1 = new simple_class;
$instance_2 = new simple_class;

$instance_2->var = "change value";

echo $instance_1->var;
echo $instance_2->var;

この場合の出力は

value 1
change value

になります。

これのどこが良いのでしょうか?

例えば、こんなClassがあればどうでしょうか。


<?php

class time_now{
    public $time_format = "Y-m-d h:i:s";

    public function now(){
        return date($this->time_format);
    }
}

?>

呼び出してみます。


<?php

$time_format = new time_now;
echo $time_format->now();

?>

これの出力の例としては

2014-05-02 21:34:12

のように、年月日と時分秒が表示されます。
次のように呼び出されたらどうでしょうか?


<?php

$time_format_1 = new time_now;
$time_format_2 = new time_now;

$time_format_2->time_format = "y-n-j g:i:s";

echo $time_format_1->now();
echo $time_format_2->now();

?>

これの出力は

2014-05-02 21:34:12
14-5-2 9:34:12

このようになります。つまり、同じ情報でも、インスタンスが保持しているプロパティの値を変更できることで、ここでは、年月日時分秒のフォーマットを変更することが出来るのに、呼び出し方を全く同じようにすることが出来るのです!なんて便利なんだ!!!!!!

ではDBのコードをオブジェクト指向的に書いてみよう

やっと話が戻ってきました。今一度、一番最初に例として上げた通常のDBの操作をするためのコードの問題点を整理しましょう(一つだけですけど…)。

  • 複数のDBリンクをあつかう場合にはどのように書くと良いのか?
    具体的なコードは上を見ていただくとして、$db_linkや、接続の情報を複数持つことによって、変数の管理が大変になることは、接続先のDBが多くなるほどに難しさが増していくことが容易に想像できますね。

ここまで読んでくださった皆さんは、オブジェクト指向がクラス内のプロパティの内容がインスタンス間ではおなじプロパティ名であっても、相互に影響を与えないことを理解していただけたと思います(じゃなければ、このエントリはそこで破綻していますね…)。この性質を先の$db_linkや、接続先情報の管理に利用できるとしたら、 とてもステキなことだと思いませんか?

そんなとってもステキなオブジェクト指向的なコードを書いてみましょう。


<?php

class Database{
    //接続情報を格納するプロパティを宣言
    public $connection_info;
    private $link;
    public $recently_result;

    function __contract($host, $user, $pass, $name){
        $this->connection_info = array(
            "host" => $host,
            "user" => $user,
            "pass" => $pass
        );

        //DBに接続する
        $this->link = mysql_connect($this->connection_info["host"], $this->connection_info["user"], $this->connection_info["pass"]);

        //リンクが正常なのかを判断
        if(!$this->link){
            die("cannot connect Database: " . mysql_error());
        }

        $this->select_db($name);
    }

    public function send_query($query){
        //DBにQueryを実行させるメソッド
        $query_result = mysql_query($query, $this->link);
        if(!$query_result){
            die("Query error: " . mysql_error());
        }
        $this->recently_result = $query_result;
        result $query_result;
    }

    public function fetch_assoc($query_result){
        //send_query()の結果の行を連想配列として、取り出す。連想配列のkeyは列名で、呼びたす度に内部のデータポインタを一行進める。
        return mysql_fetch_assoc($query_result);
    }

    public function select_db($name){
        //使用するDBを選択するメソッド
        $this->connection_info["name"] = $name;
        $db_select_result = mysql_select_db($this->connection_info["name"], $this->link);

        //使用するDBが選択出来たか判断
        if(!$db_select_result){
            die("cannot select" . $db_name . ": " . mysql_error());
        }
    }

    public function close(){
        //DBとのリンクをクローズするメソッド
        if(!mysql_close($this->link)){
            die("cannot close Database link: " . mysql_error());
        }
    }
}

?>

実際に使ってみましょう


<?php

$db1 = new Database(
    "host1.com",
    "u_hoge1",
    "password1",
    "d_foo1"
);

$db2 = new Database(
    "host2.com",
    "u_hoge2",
    "password2",
    "d_foo2"
);

$query  = "SELECT * from table_hoge;";

$db1->send_query($query);
$db2->send_query($query);

while($target_result_row = $db1->fetch_assoc($db1->recently_result)){
    print_r($target_result_row);
}

while($target_result_row = $db2->fetch_assoc($db2->recently_result)){
    print_r($target_result_row);
}

$db1->close();
$db2->close();

?>

いかがでしょうか?一番最初のほうで書いたオブジェクト指向を用いないコードよりもなかなか簡略化して、スッキリした印象があります(と個人的には思ってます)。DBの接続情報はインスタンス間でしっかりと保持してますし、query_sendするときに最初の方では$db_linkなどを引数に指定しなければいけないところを、$queryだけ渡せば、そのDBでしっかりQueryが実行できることがわかります。プログラムがシンプルになりますね!その他にも、何かコードに問題があればclass自体を修正すれば、$db1$db2もその修正が反映されます。

おまけに、ちょっとセキュアに

プロパティのアクセス制限

先程からClass内にprivatepublicというキーワードがあります。これはそれぞれのメソッドやプロパティのアクセスを制限するために書かれています。

privateは、インスタンス内(クラス内)からの参照は受け付けますが、インスタンス外(クラス外)からの参照を受け付けないことを宣言しています。publicは、インスタンス外(クラス外)からも参照を受け付けます。例えば、


<?php

class sample_calss{
    private $var_private = "private";
    public $var_public = "public";

    public echo_var(){
        echo $this->var_private;
        echo $this->var_public;
    }
}

$sample_instance = new samole_class;

//インスタンス内のメソッドでechoする
$sample_instance->echo_var(); // "private"と"public"という文字列が出力されます

//インスタンス外からインスタンス内のデータを参照してでechoする
echo $sample_instance->var_private; // Fatal エラー
echo $sample_instance->var_public; // "public"という文字列が出力されます

?>

アクセス権はprivatepublicの他に、protectedがあります。こちらの方はPHP: アクセス権 - Manualを参照して下さい。

実際に活用してみよう

ではアクセス制限をセキュリティの分野で活用してみましょう。

上記のクラスDatabaseでは、外部から$connection_infoが簡単に読み取れるようになっていますね。万が一、XSS(Webの攻撃手法の一つです)でDBの接続情報がもれないように、$connection_infoをprivateなプロパティとして外部からの参照を制限すると、なんとなく安心です。それを実現するとしたら、こんなかんじでしょうか?


<?php

class Database{
    //接続情報を格納するプロパティを宣言
    private $connection_info; //接続情報をprivateにすることで、XSSでも情報が漏れにくくなる
    private $link;
    public $recently_result;

    function __contract($db_host){
        $db_ini_array = parse_ini_file("database_info.ini", true); //接続情報を直接書かず、iniファイルに予め記録して、そこから呼び出す

        //指定のhost情報が存在するかチェック
        if(!$db_ini_array[$db_host]){
            die("not defined hostname:". $db_host);
        }

        $this->connection_info = array(
            "host" => $db_ini_array[$db_host]["host"],
            "user" => $db_ini_array[$db_host]["user"],
            "pass" => $db_ini_array[$db_host]["pass"]
        );

        //DBに接続する
        $this->link = mysql_connect($this->connection_info["host"], $this->connection_info["user"], $this->connection_info["pass"]);

        //リンクが正常なのかを判断
        if(!$this->link){
            die("cannot connect Database: " . mysql_error());
        }

        $this->select_db($db_ini_array[$db_host]["name"]);
    }

    public function send_query($query){
        //DBにQueryを実行させるメソッド
        $query_result = mysql_query($query, $this->link);
        if(!$query_result){
            die("Query error: " . mysql_error());
        }
        $this->recently_result = $query_result;
        result $query_result;
    }

    public function fetch_assoc($query_result){
        //send_query()の結果の行を連想配列として、取り出す。連想配列のkeyは列名で、呼びたす度に内部のデータポインタを一行進める。
        return mysql_fetch_assoc($query_result);
    }

    private function select_db($name){
        //使用するDBを選択するメソッド
        $this->connection_info["name"] = $name;
        $db_select_result = mysql_select_db($this->connection_info["name"], $this->link);

        //使用するDBが選択出来たか判断
        if(!$db_select_result){
            die("cannot select" . $db_name . ": " . mysql_error());
        }
    }

    public function close(){
        //DBとのリンクをクローズするメソッド
        if(!mysql_close($this->link)){
            die("cannot close Database link: " . mysql_error());
        }
    }
}

?>

コード中の記述で、iniファイルを利用しています。次のようにDBの接続情報をdatabase_info.iniに書きます。


[host1.com]
host = "host1.com"
user = "u_hoge1"
pass = "password1"
name = "d_foo1"

[host2.com]
host = "host2.com"
user = "u_hoge2"
pass = "password2"
name = "d_foo2"

phpコードと、iniを同じディレクトリに配置し、WebサーバーがAppacheならばdatabase_info.ini.htaccessで外部からのアクセスを制限するように設定すると安全でしょう。まだ抜け穴があるかもしれませんが、わかりやすくセキュアにするところだけ変更してみました。実際に使ってみましょう。


<?php

$db = new Database("host1.com");

$query  = "SELECT * from table_hoge;";
$db->send_query($query);

while($target_result_row = $db->fetch_assoc($db->recently_result)){
    print_r($target_result_row);
}

$db->close();

?>

これである程度はセキュアにDBを扱えると思いますね。もちろん、Queryの組み立て方などに不備がある場合は、テーブル内のデータが読み書きできてしまうので、そこには別の注意が必要ですが、少なくとも、接続情報を守る分にはこれで良いのではないかなーって思います。

そもそもprivateはセキュリティとしての手段ではない

コメントでもご指摘がありましたが、privateでアクセス権を設定することによるセキュリティの確保は、本来あるべきprivateの目的ではありません。privateはあくまで クラスを利用する立場からみて 隠蔽されるべき情報(利用者が考慮しなくてい良い情報)をクラスが持ちたいときに使われる手段です。これを「カプセル化」といい、オブジェクト指向の重要な特徴の一つと言われています。

終わりに

DBをあつかう場面を例にオブジェクト指向を選択する理由を中心に書いてきました。長々と記述しているので、何がなんだか、私が何言ってるのか分からなくなってきているのでさらに皆さんはわからないかもしれません。いろんな人がオブジェクト指向についてわかりやすい説明を試みていると思うので、そちらも調べて参照して勉強できればと思います。

ここが悪いなど、ありましたら適宜コメントを下さると、加筆修正できますのでよろしくお願いします。

88
98
3

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
88
98