コードの重複を排除するリファクタリング術

プログラミング

コードの重複排除リファクタリング術

コードの重複は、ソフトウェア開発において避けては通れない課題の一つです。重複したコードは、保守性の低下、バグの温床、開発効率の悪化といった様々な問題を引き起こします。これらの問題を解消し、よりクリーンで管理しやすいコードベースを構築するために、コードの重複を排除するリファクタリング術は非常に重要です。本稿では、コードの重複を排除するための主要なリファクタリング手法について、その原則、適用方法、そして関連する注意点までを詳細に解説します。

重複排除の原則と重要性

コードの重複排除の根本的な原則は、「Don’t Repeat Yourself (DRY)」です。これは、同じ情報やロジックがシステム内の複数の場所に存在しないようにするという設計原則です。DRY原則に従うことで、以下のようなメリットが得られます。

  • 保守性の向上: コードの修正が必要になった場合、一箇所を修正するだけで、すべての関連箇所にその変更が反映されます。これにより、修正漏れや意図しない副作用を防ぐことができます。
  • バグの削減: 重複したコードがあると、修正時に一部だけが更新され、他の部分は古いまま残ってしまうことがあります。これがバグの原因となります。DRY原則に従うことで、このようなバグの発生リスクを低減できます。
  • 可読性の向上: ロジックが共通化されることで、コード全体の見通しが良くなり、理解しやすくなります。
  • 開発効率の向上: 新機能の開発や既存機能の改修において、既存の共通部分を再利用できるため、開発時間を短縮できます。

主要な重複排除リファクタリング手法

コードの重複を排除するためには、様々なリファクタリング手法が存在します。ここでは、代表的な手法をいくつか紹介します。

1. 関数の抽出 (Extract Function)

重複しているコードブロックがある場合、それを独立した関数として抽出し、元の場所からはその関数を呼び出すようにします。

適用方法

重複しているコードブロックを特定します。そのコードブロックが独立して実行可能で、かつ再利用可能であることを確認します。次に、そのコードブロックを新しい関数として切り出します。元のコードブロックがあった場所には、新しく作成した関数を呼び出すコードを記述します。関数の引数や戻り値は、元のコードブロックが依存していた値や、計算結果に合わせて適切に設定します。

具体例

例えば、以下のようなコードがあったとします。

function processOrder1(order) {
  // ... 注文処理の一部
  let tax = order.price * 0.1;
  let total = order.price + tax;
  console.log("Total: " + total);
  // ... 注文処理の続き
}

function processOrder2(order) {
  // ... 別の注文処理の一部
  let tax = order.price * 0.1;
  let total = order.price + tax;
  console.log("Total: " + total);
  // ... 別の注文処理の続き
}

この場合、税金計算と合計金額計算の部分が重複しています。これを関数として抽出します。

function calculateTotal(price) {
  let tax = price * 0.1;
  return price + tax;
}

function processOrder1(order) {
  // ... 注文処理の一部
  let total = calculateTotal(order.price);
  console.log("Total: " + total);
  // ... 注文処理の続き
}

function processOrder2(order) {
  // ... 別の注文処理の一部
  let total = calculateTotal(order.price);
  console.log("Total: " + total);
  // ... 別の注文処理の続き
}

これにより、税金計算と合計金額計算のロジックが一箇所に集約され、保守性が向上しました。

2. クラスの抽出 (Extract Class)

関連する属性(データ)とメソッド(振る舞い)が複数のクラスに分散している場合、それらをまとめて新しいクラスとして抽出します。

適用方法

まず、どの属性とメソッドが論理的に一つのまとまりとして扱われるべきかを判断します。次に、それらの属性とメソッドを持つ新しいクラスを作成します。元のクラスにあったこれらの属性とメソッドは、新しいクラスに移動させます。元のクラスからは、必要に応じて新しいクラスのインスタンスを生成し、そのメソッドを呼び出すように変更します。

具体例

例えば、`Customer` クラスが顧客情報と配送先情報を両方持っているとします。

class Customer {
  constructor(name, email, shippingAddress) {
    this.name = name;
    this.email = email;
    this.shippingAddress = shippingAddress; // 配送先情報
  }

  getShippingAddress() {
    return this.shippingAddress;
  }

  setShippingAddress(address) {
    this.shippingAddress = address;
  }

  // ... 顧客情報に関するその他のメソッド
}

配送先情報が複雑になり、顧客情報とは独立して管理したい場合、`ShippingAddress` クラスを抽出することを検討します。

class ShippingAddress {
  constructor(street, city, zipCode) {
    this.street = street;
    this.city = city;
    this.zipCode = zipCode;
  }

  getFullAddress() {
    return `${this.street}, ${this.city} ${this.zipCode}`;
  }
}

class Customer {
  constructor(name, email, shippingAddress) {
    this.name = name;
    this.email = email;
    this.shippingAddress = shippingAddress; // ShippingAddress オブジェクト
  }

  getShippingAddress() {
    return this.shippingAddress;
  }

  setShippingAddress(address) {
    this.shippingAddress = address;
  }

  // ... 顧客情報に関するその他のメソッド
}

これにより、`Customer` クラスは顧客情報に特化し、`ShippingAddress` クラスは配送先情報に特化するため、各クラスの責任が明確になり、コードの理解と管理が容易になります。

3. モジュールの利用 (Use Modules)

重複した機能や共通のロジックを、独立したモジュールとして切り出し、必要な場所でインポートして利用します。

適用方法

共通して使われる関数、クラス、定数などを特定します。これらの要素を新しいファイル(モジュール)にまとめます。他のファイルでは、これらのモジュールを `import` 文などを使って読み込み、内部で定義されている機能を利用します。

具体例

複数のファイルで日付フォーマットの処理が重複している場合、それを `dateUtils.js` のようなモジュールにまとめます。

// dateUtils.js
export function formatDateTime(date) {
  // ... 日付フォーマット処理
  return formattedString;
}

別のファイルでこれを利用する場合:

// main.js
import { formatDateTime } from './dateUtils.js';

const today = new Date();
console.log(formatDateTime(today));

モジュール化により、共通機能が一元管理され、コードの再利用性が高まります。

4. テンプレートメソッドパターン (Template Method Pattern)

アルゴリズムの骨格は共通だが、一部のステップはサブクラスで異なる場合に有効なデザインパターンです。共通のアルゴリズムの骨格をスーパークラスに定義し、可変の部分を抽象メソッドとして定義して、サブクラスで実装させます。

適用方法

共通のアルゴリズムを持つ複数のクラスがある場合、それらを抽象化してスーパークラスを作成します。アルゴリズムの固定部分をスーパークラスのメソッドとして実装します。アルゴリズムの可変部分(サブクラスごとに異なる処理)は、抽象メソッドとして定義し、サブクラスで具体的な実装を提供させます。

具体例

様々な種類のドキュメントを生成する処理を考えます。ヘッダー、ボディ、フッターの生成は共通ですが、各ドキュメントの種類で内容が異なります。

// 抽象クラス
class DocumentGenerator {
  generate() {
    this.addHeader();
    this.addBody();
    this.addFooter();
  }

  addHeader() {
    console.log("Default Header");
  }

  addBody() {
    throw new Error("addBody() must be implemented");
  }

  addFooter() {
    console.log("Default Footer");
  }
}

// 具体クラス1
class ReportGenerator extends DocumentGenerator {
  addBody() {
    console.log("Report Body");
  }
}

// 具体クラス2
class EmailGenerator extends DocumentGenerator {
  addHeader() {
    console.log("Email Subject");
  }
  addBody() {
    console.log("Email Content");
  }
}

`generate()` メソッドは共通のアルゴリズムを提供し、`addBody()` のようなメソッドはサブクラスで実装することで、コードの重複を防ぎつつ、柔軟な拡張性を実現しています。

5. ポリモーフィズムの活用 (Leverage Polymorphism)

`if-else` や `switch` 文による条件分岐で処理が分かれている部分を、ポリモーフィズム(多態性)を利用して解消します。

適用方法

条件分岐で処理が分かれている対象を、共通のインターフェースを持つクラス群として再設計します。各クラスは、その条件分岐の各ケースに対応する処理を、同じ名前のメソッドとして実装します。元のコードでは、条件分岐の代わりに、該当するクラスのインスタンスのメソッドを呼び出すように変更します。

具体例

図形の面積を計算する処理で、図形の種類によって処理が分かれている場合。

function calculateArea(shape) {
  if (shape.type === "circle") {
    return Math.PI * shape.radius * shape.radius;
  } else if (shape.type === "rectangle") {
    return shape.width * shape.height;
  } else if (shape.type === "triangle") {
    return 0.5 * shape.base * shape.height;
  }
  return 0;
}

これをポリモーフィズムで書き換えます。

class Shape {
  calculateArea() {
    throw new Error("calculateArea() must be implemented");
  }
}

class Circle extends Shape {
  constructor(radius) {
    super();
    this.radius = radius;
  }
  calculateArea() {
    return Math.PI * this.radius * this.radius;
  }
}

class Rectangle extends Shape {
  constructor(width, height) {
    super();
    this.width = width;
    this.height = height;
  }
  calculateArea() {
    return this.width * this.height;
  }
}

// ... Triangle クラスも同様に定義

function calculateArea(shape) {
  return shape.calculateArea();
}

これにより、新しい図形を追加する際に `if-else` 文を修正する必要がなくなり、コードが拡張しやすくなります。

リファクタリング実施上の注意点

コードの重複排除は非常に効果的ですが、実施する際にはいくつかの注意点があります。

  • テストの実施: リファクタリングを行う前に、必ずテストコードを作成し、リファクタリング後にもテストがパスすることを確認します。これにより、意図しないバグの混入を防ぐことができます。
  • 一度に多くの変更をしない: 小さな単位でリファクタリングを行い、その都度テストを実行します。一度に大量のコードを変更すると、問題が発生した場合の原因特定が困難になります。
  • コードの意図を理解する: コードの重複を解消するだけでなく、なぜそのコードが書かれていたのか、その意図を理解することが重要です。表面的な重複だけを解消しても、根本的な問題が解決しない場合があります。
  • 命名規則: 抽出した関数やクラスには、その役割が明確にわかるような、分かりやすい名前を付けることが重要です。
  • 抽象化の度合い: 過度な抽象化は、コードの複雑性を増大させる可能性があります。必要な範囲で適切に抽象化することが重要です。

まとめ

コードの重複排除は、ソフトウェアの品質を向上させるための不可欠なプロセスです。DRY原則に基づき、関数の抽出、クラスの抽出、モジュールの利用、デザインパターンの適用、ポリモーフィズムの活用といった様々なリファクタリング術を適切に使い分けることで、保守性、可読性、開発効率の高いコードベースを構築することができます。リファクタリングは継続的なプロセスであり、常にコードの改善を意識することが、健全なソフトウェア開発に繋がります。