RAKUS Developers Blog | ラクス エンジニアブログ

株式会社ラクスのITエンジニアによる技術ブログです。

手続き型プログラミングで発生した問題とオブジェクト指向への入門

こんにちは!新卒1年目のos188です。

私が担当する商材は、リリースから10年以上が経過し、膨大な量のソースコードが存在します。
大部分はオブジェクト指向プログラミングで書かれていますが、
コードを読んで勉強しているとき、古い部分で手続き型プログラミングによって書かれているところを見つけました。
新しい部分と比較すると「読みづらいな、処理を追いかけにくいな」と感じることが多く、
大規模なソースコードだとこんなにも差が出るのかと感心しました。
今回は、手続き型プログラミングを大きなプロジェクトや複雑な処理に適用した際のやりづらさと、オブジェクト指向プログラミングによる解決策について説明します。

手続き型のやりづらさ

  1. データの変更が処理に影響を与えやすい
  2. コードの重複が発生しやすい
  3. データと処理が分離される

サンプルコードを用いてそれぞれ具体的に説明していきます。

1. データの変更が処理に影響を与えやすい

手続き型プログラミングでは、データの変更が処理に影響を与える可能性が高いです。 関数によるデータ参照がバグを生みやすく、特に大規模なプログラムでは変数名の衝突や意図しない変更が起こりやすいです。
以下の例では、グローバル変数が別の関数によって予期せず書き換えられています。

<?php
$tax_rate = 0.10; // 税率がグローバル変数

function calculate_total_price($price) {
    global $tax_rate; // 税率を取得する
    return $price + ($price * $tax_rate); // 税込み価格を返す
}

function calculate_reduced_tax_price($price) {
    global $tax_rate; // 税率を取得する
    $tax_rate = 0.08; // 税率が上書きされた…!
    return $price + ($price * $tax_rate); // 税込み価格を返す
}

echo calculate_total_price(100); // "110"
echo calculate_reduced_tax_price(100); // "108"
echo calculate_total_price(100); // "108" (税率10%のつもりが…)

カプセル化する

オブジェクト指向では、カプセル化によってこの問題を解決できます。
クラス内のデータが隠蔽されており外部からアクセスできないため、書き換えられることがありません。

<?php
    private $taxRate; // 税率が外部からアクセスできない!

    public function __construct($taxRate) {
        $this->taxRate = $taxRate;
    }

    public function calculateTotalPrice($price) {
        return $price + ($price * $this->taxRate); // 税込み価格を返す
    }
}

$taxCalculator = new TaxCalculator(0.10); // 税率10%のインスタンスを作成
echo $taxCalculator->calculateTotalPrice(100); // "110"

$reducedTaxCalculator = new TaxCalculator(0.08); // 軽減税率用のインスタンスを作成
echo $reducedTaxCalculator->calculateTotalPrice(100); // "108"

echo $taxCalculator->calculateTotalPrice(100); // "110" (税率10%のまま!)

2. コードの重複が発生しやすい

手続き型プログラミングでは、コードをモジュール化して再利用することが難しいため、コードの重複が発生しやすくなります。
以下の例では、ほぼ同じ処理が書かれていたり、同じ関数を何度も呼び出しています。
これは非常に簡単な例ですが、複雑な処理が多くなるとどうしても重複が発生します。

<?php
function bark_dog() {
    echo "犬がワンと鳴いた\n";
}

// 似たような処理…
function meow_cat() {
     echo "猫がニャーと鳴いた\n";
}

// 何度も同じ関数を呼んでいる
bark_dog(); // "犬がワンと鳴いた"
meow_cat(); // "猫がニャーと鳴いた"
bark_dog(); // "犬がワンと鳴いた"
bark_dog(); // "犬がワンと鳴いた"
meow_cat(); // "猫がニャーと鳴いた"

継承とポリモーフィズムを用いる

一方、オブジェクト指向なら継承することで同じ処理を再利用することができます。
また、ポリモーフィズムによってコードの重複を防ぐことができます。

<?php
class Animal {
    protected $animal;
    protected $cry;

    public function __construct($animal, $cry) {
        $this->animal = $animal;
        $this->cry = $cry;
    }

    public function cry() {
        echo $this->animal . "が" .  $this->cry  . "と鳴いた\n";
    }
}

class Dog extends Animal { 
    public function __construct() {
        parent::__construct("犬", "ワン");
    }
    // Animalクラスを継承しているので、cry()関数を使える!
}

class Cat extends Animal {
    public function __construct() {
        parent::__construct("猫", "ニャー");
    }
    // Animalクラスを継承しているので、cry()関数を使える!
}

 // ポリモーフィズムでまとめてオブジェクトを扱える!
$animals = [new Dog(), new Cat(), new Dog(), new Dog(), new Cat()];
foreach ($animals as $animal) {
    $animal->cry(); // 出力省略 (犬、猫、犬、犬、猫の順番で鳴く)
}

3. データと処理が分離される

手続き型プログラミングでは、データとそれに関連する処理が分離されがちです。これにより、意図しない副作用を招く可能性があります。
以下の例では、いろいろな値をグローバル変数で管理しており、コードの理解や変更が困難になっています。

<?php
function getAdultAge($date) {
    if ($date < strtotime("2022-04-01")) {
        return 20;
    } else {
        return 18;
    }
}

function reachBirthday($name, $age) { // 外部の状態を変更するような関数
    $age++;
    echo "$name は誕生日を迎え、$age 歳になりました\n";
    return $age;
}

function isAdult($name, $age, $adult_age) { // 外部の状態に依存した関数
    if ($age >= $adult_age) {
        echo "$name は成人です\n";
    } else {
        echo "$name は未成年です\n";
    }
}

$person_name = "John";
$person_age = 17;

// 成人年齢をグローバル変数で管理する必要がある
$adult_age = getAdultAge(strtotime("2024-01-01")); 

isAdult($person_name , $person_age, $adult_age); // "Johnは未成年です"

// Johnの年齢もグローバル変数で管理する必要がある
$person_age = reachBirthday($person_name , $person_age);  // "John は誕生日を迎え、18 歳になりました"

isAdult($person_name , $person_age, $adult_age); // "Johnは成人です"

クラスで管理する

オブジェクト指向プログラミングでは、クラスによってデータと処理がセットで管理されます。
それぞれのデータをクラス内で管理できるので、クラスの外ではデータの状態を気にしなくて大丈夫です。

<?php
class Person {
    private $name;
    private $age;
    // 名前と年齢はこのクラス内で管理する

    public function __construct($name, $age) {
        $this->name = $name;
        $this->age = $age;
    }

    public function reachBirthday() {
        $this->age++; 
        echo "$this->name は誕生日を迎え、$this->age 歳になりました\n";
    }
    
    public function getName() {
        return $this->name;
    }
    
    public function getAge() {
        return $this->age;
    }
}

class JudgeAdult {
    private $person;
    private $adult_age; 
    // 成人年齢の処理はこのクラス内で完結する
    // つまり、成人年齢の仕様に変更があってもこのクラスを見るだけで良い!
    
    public function __construct($person, $date) {
        $this->person = $person;
        if ($date < strtotime("2022-04-01")) {
            $this->adult_age = 20;
        } else {
            $this->adult_age = 18;
        }
    }
    
    public function isAdult() {
        if ($this->person->getAge() >= $this->adult_age ) {
            echo $this->person->getName()  . "は成人です\n";
        } else {
            echo $this->person->getName()  . "は未成年です\n";
        }
    }
}

$john = new Person("John", 17);

$john_judge = new JudgeAdult($john, strtotime("2024-01-01"));

// 成人年齢は JudgeAdultクラスで管理するので、気にしなくて良い!
$john_judge ->isAdult(); // "Johnは未成年です"

// Johnの年齢はPersonクラスで管理するので、気にしなくて良い!
$john->reachBirthday();  // "John は誕生日を迎え、18 歳になりました"

$john_judge ->isAdult(); // "Johnは成人です"

まとめ

今回は、手続き型プログラミングのやりづらさと、オブジェクト指向プログラミングによる解決策について取り上げました。

  1. データの変更が処理に影響を与えやすい→カプセル化する
  2. コードの重複が発生しやすい→継承とポリモーフィズムを用いる
  3. データと処理が分離される→クラスで管理する

全体を通して、コードの再利用性、保守性が向上したことを感じていただけたでしょうか?
大規模な開発になればなるほど、これらの影響がどんどん大きくなります。私は毎日オブジェクト指向プログラミングの恩恵を享受しています!
最後まで読んでいただきありがとうございました。

Copyright © RAKUS Co., Ltd. All rights reserved.