PHP
オブジェクト指向
ソースコードリーディング
オブジェクト指向設計
More than 1 year has passed since last update.

巨大なソースコードを読むとき、その巨大さに圧倒されたことがあるかもしれない。オブジェクト指向設計、オブジェクト指向のコードを読むときは、スレッドを追っていくのはあんまり能率的な方法ではないかもしれない。
僕は、「単一責任の原則」を使って読むのがいいんじゃないかと思っている。
ちなみに、「単一責任の原則」は、綺麗に設計されたオブジェクト指向のコードでないと、適応できないかもしれない。Linuxカーネルとかで適応できるのかどうはははっきりはわからない。(Linuxのコードの設計が汚いという意味ではなく)
では、「単一責任の原則」でソースコードを読んでいくとは何か。
「単一責任の原則」とは、一つのクラスは一つの責任を持っているべきであるというオブジェクト指向の思想である。オープンソースのオブジェクト指向のコードでは、これがちゃんと守られていることが多い。
僕がソースコード読みでやっている戦略は、そのクラスの「責任」に想いを馳せるということである。
どんな巨大で、たくさんのクラスで構成されているコードでも、たった一つのクラスと、その使用状況を調べるだけなら、簡単にできる。
そのクラスは、何をやっているクラスなのか。それを知ることにより、ソースコード読みを楽しくできるのではと思うのである。
意外と、Linuxのカーネルでもうまくいく戦略かもしれない。
では、理屈ばっかり言っても仕方ないので、ちょっと読んでいこう。
CakePHPのコードより引用した。

https://github.com/cakephp/cakephp/blob/master/src/Database/Driver/Sqlserver.php

ソースコードが変わる可能性があるので、以下にソースコードを貼り付けてみる。

<?php
/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.0.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\Database\Driver;

use Cake\Database\Dialect\SqlserverDialectTrait;
use Cake\Database\Driver;
use Cake\Database\Query;
use Cake\Database\Statement\SqlserverStatement;
use PDO;

/**
 * SQLServer driver.
 */
class Sqlserver extends Driver
{

    use PDODriverTrait;
    use SqlserverDialectTrait;

    /**
     * Base configuration settings for Sqlserver driver
     *
     * @var array
     */
    protected $_baseConfig = [
        'persistent' => false,
        'host' => 'localhost\SQLEXPRESS',
        'username' => '',
        'password' => '',
        'database' => 'cake',
        // PDO::SQLSRV_ENCODING_UTF8
        'encoding' => 65001,
        'flags' => [],
        'init' => [],
        'settings' => [],
        'attributes' => [],
        'app' => null,
        'connectionPooling' => null,
        'failoverPartner' => null,
        'loginTimeout' => null,
        'multiSubnetFailover' => null,
    ];

    /**
     * Establishes a connection to the database server
     *
     * @return bool true on success
     */
    public function connect()
    {
        if ($this->_connection) {
            return true;
        }
        $config = $this->_config;
        $config['flags'] += [
            PDO::ATTR_PERSISTENT => $config['persistent'],
            PDO::ATTR_EMULATE_PREPARES => false,
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
        ];

        if (!empty($config['encoding'])) {
            $config['flags'][PDO::SQLSRV_ATTR_ENCODING] = $config['encoding'];
        }

        $dsn = "sqlsrv:Server={$config['host']};Database={$config['database']};MultipleActiveResultSets=false";
        if ($config['app'] !== null) {
            $dsn .= ";APP={$config['app']}";
        }
        if ($config['connectionPooling'] !== null) {
            $dsn .= ";ConnectionPooling={$config['connectionPooling']}";
        }
        if ($config['failoverPartner'] !== null) {
            $dsn .= ";Failover_Partner={$config['failoverPartner']}";
        }
        if ($config['loginTimeout'] !== null) {
            $dsn .= ";LoginTimeout={$config['loginTimeout']}";
        }
        if ($config['multiSubnetFailover'] !== null) {
            $dsn .= ";MultiSubnetFailover={$config['multiSubnetFailover']}";
        }
        $this->_connect($dsn, $config);

        $connection = $this->connection();
        if (!empty($config['init'])) {
            foreach ((array)$config['init'] as $command) {
                $connection->exec($command);
            }
        }
        if (!empty($config['settings']) && is_array($config['settings'])) {
            foreach ($config['settings'] as $key => $value) {
                $connection->exec("SET {$key} {$value}");
            }
        }
        if (!empty($config['attributes']) && is_array($config['attributes'])) {
            foreach ($config['attributes'] as $key => $value) {
                $connection->setAttribute($key, $value);
            }
        }

        return true;
    }

    /**
     * Returns whether PHP is able to use this driver for connecting to database
     *
     * @return bool true if it is valid to use this driver
     */
    public function enabled()
    {
        return in_array('sqlsrv', PDO::getAvailableDrivers());
    }

    /**
     * Prepares a sql statement to be executed
     *
     * @param string|\Cake\Database\Query $query The query to prepare.
     * @return \Cake\Database\StatementInterface
     */
    public function prepare($query)
    {
        $this->connect();
        $options = [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL];
        $isObject = $query instanceof Query;
        if ($isObject && $query->isBufferedResultsEnabled() === false) {
            $options = [];
        }
        $statement = $this->_connection->prepare($isObject ? $query->sql() : $query, $options);

        return new SqlserverStatement($statement, $this);
    }

    /**
     * {@inheritDoc}
     */
    public function supportsDynamicConstraints()
    {
        return true;
    }
}

見ての通り、Driverクラスを継承している。Driverを継承しているのは、Mysql.php、Postgres.php、SQlite.php、Sqlserver.phpである
このことから何がわかるか、Driverとは、SQLデータベースのベンダーごとの差異を吸収するものであることがわかる。そして、このソースコードにおいては、Sqlserverに特異な部分を記述するものであることがわかる。
非常にうまい設計をしていることがわかる。例えば、ベンダーが増えたり減ったりしても、一部分を書き換え、新しくクラスを作成することによって、既存のコードに破壊的な影響を及ぼすことがない。
connect関数により、データベースサーバーへの、接続の確立をサポートしている。connect関数は、Mysql.php、Postgres.php、SQlite.php、Sqlserver.phpそれぞれに違う実装をしている。
ここら辺は、オブジェクト指向への理解が必要になると思う。

    public function enabled()
    {
        return in_array('sqlsrv', PDO::getAvailableDrivers());
    }

上の関数により、ドライバが使用可能かどうか、さらにいうと、SQL Serverが使用可能であるかということを判定しているように思われる。

prepare関数、以下の関数により、SQLのステートメントの実行準備をしている。これも、クラスにより実装が違う。

    public function prepare($query)
    {
        $this->connect();
        $options = [PDO::ATTR_CURSOR => PDO::CURSOR_SCROLL];
        $isObject = $query instanceof Query;
        if ($isObject && $query->isBufferedResultsEnabled() === false) {
            $options = [];
        }
        $statement = $this->_connection->prepare($isObject ? $query->sql() : $query, $options);

        return new SqlserverStatement($statement, $this);
    }

これもDriverを継承するクラスによって、微妙に違う。
以下のメソッドは、「動的束縛をサポートしているか」を返すメソッドである。

    public function supportsDynamicConstraints()
    {
        return true;
    }

興味深いのは、Mysqlはtrue、Postgresは、trueを返し、SQLiteはfalseを返し、SQLServerはtrueを返すということである。
これは、書いてあることそのままに、解釈していけば、仕様が丸わかりということで、興味深い。クラス名、メソッド名は、ちゃんと論理的に、読む人のことを考えてつける必要があるということである。
では、supportsDynamicConstraints()が、SDC()とか、DynamicConstraint()、Constraint()とか、ちょっとやばいクラス名をつけると、コードは動くが、誰も読めなくなり、だんだんコードが腐敗していき、地獄になるのである。
しかも、そのメソッドは、Mysql.php、Postgres.php、SQlite.php、Sqlserver.phpのそれぞれに実装されている。つまり、supportsDynamicConstraints() などのメソッドは、それぞれ違うベンダーの人が実装する可能性があるのである。なので、違う人の間で戦略を共有するためには、クラス名、メソッド名、プロパティ名は、それが何を示しているか、どういう動作を示しているかを、疑問の余地なく、明確に理解できるようにしなければならない。
ちなみに、「単一責任原則」を使って読むときは、そもそもそのソースコードが「単一責任原則」に従っている必要がある。それがされていない場合は、リファクタリングの必要があるが、そもそも作った人がリファクタリングをしてくれないと、あとで他の人が読んでリファクタリングとか、ちょっと地獄なのである。
つまり言い換えれば、コードを書いたときはリファクタリングの必要がある。
あと、プロジェクトの構成ファイルを眺めて、アプリの構造が理解できるのが理想的だなと思う。僕は自作アプリではあんまりできてないけど。