Blog#123: SOLID原則:コードをきれいにして理解しやすくする

image.png

こんにちは、私はトゥアンと申します。東京からフルスタックWeb開発者です。 将来の有用で面白い記事を見逃さないように、私のブログをフォローしてください。

コーディングをするとき、読みやすく理解しやすいものにすることが大切です。特に大きなプロジェクトで複数の部分があるときは、特に重要です。コードを理解しやすくするための方法として、SOLIDの原則を守ることがあります。

SOLIDとは、5つの原則を意味します:

  1. Single Responsibility Principle (SRP)
  2. Open-Closed Principle (OCP)
  3. Liskov Substitution Principle (LSP)
  4. Interface Segregation Principle (ISP)
  5. Dependency Inversion Principle (DIP)

それぞれの原則を一つずつ見ていき、どのようにコードをより良くするのかを見ていきましょう。

説明

Single Responsibility Principle (SRP)

SRPは、コードの各部分が1つの仕事だけをするようにするということを言っています。これは、1つのコードが複数のことをする場合、小さな部分に分けて、それぞれに仕事を与えるべきだということを意味します。

例えば、ボタンが何回クリックされたかを記録し、画面にクリック数を表示するコードがあるとします。SRPによると、このコードは2つの別々の部分に分けるべきです:1つはクリックを記録するもの、もう1つは画面にクリック数を表示するものです。

// Incorrect way of doing it
let clickCount = 0;

function handleButtonClick() {
    clickCount += 1;
    document.getElementById("click-count").innerHTML = clickCount;
}
// Correct way of doing it
let clickCount = 0;

function handleButtonClick() {
    clickCount += 1;
}

function updateClickCount() {
    document.getElementById("click-count").innerHTML = clickCount;
}

Open-Closed Principle (OCP)

OCPとは、コードを拡張するために開いているべきであり、修正するためには閉じているべきだということです。これは、既存のコードを変更せずに新しい機能を追加できるようにすることを意味します。

例えば、2つの数字を足し合わせるプログラムがあるとします。OCPに従うと、既存の足し算のコードを変更せずに、新しい機能(引き算など)を追加できるようになります。

// Incorrect way of doing it
function add(a, b) {
    return a + b;
}

// It is not a good idea to change the `add` function to a `subtract` function.
function subtract(a, b) {
    return a - b;
}
// Correct way of doing it
class Calculator {
    static add(a, b) {
        return a + b;
    }

    static subtract(a, b) {
        return a - b;
    }
}

Liskov Substitution Principle (LSP)

LSPとは、親クラスを使う場所でサブクラスを使えるようにするというものです。これは、サブクラスが親クラスの「より良い」バージョンであり、親クラスを使うコードを壊さないことを意味します。

例えば、「Animal」という親クラスと「Dogs」というサブクラスがあるとします。LSPによると、「Animal」オブジェクトを使う場所で「Dog」オブジェクトを使えるはずであり、コードは正しく動作するはずです。

class Animals {
    constructor(name) {
        this.name = name;
    }

    speak() {
        return "Animals make noise";
    }
}

class Dogs extends Animals {
    speak() {
        return "Woof";
    }
}

const animal = new Animals("Animals");
console.log(animal.speak()); // prints "Animals make noise"

const dog = new Dogs("Dog");
console.log(dog.speak()); // prints "Woof"
console.log(dog instanceof Animals); // prints "true"

Interface Segregation Principle (ISP)

ISPとは、クライアントに使わないインターフェースを強制するべきではないということです。これは、関連する機能のグループごとに小さなインターフェースを作成するべきだということです。

例えば、「Automobile」というインターフェースが、走行と飛行の両方の機能を持っているとします。ISPによると、走行しかできない車のクラスは、「Automobile」インターフェースからの飛行機能を強制されてはなりません。

// Incorrect way of doing it
interface Automobile {
    drive(): void;
    fly(): void;
}

class Car implements Automobile {
    drive(): void {
        // code for driving
    }

    fly(): void {
        // code for flying (not applicable for cars)
    }
}
// Correct way of doing it
interface Drivable {
    drive(): void;
}

interface Flyable {
    fly(): void;
}

class Car implements Drivable {
    drive(): void {
        // code for driving
    }
}

Dependency Inversion Principle (DIP)

DIPとは、高レベルのモジュールは低レベルのモジュールに依存してはいけないが、両方とも抽象化に依存するべきだということです。これは、あなたのコードが特定のクラスや関数に依存してはいけないということを意味しますが、むしろ抽象的な概念に依存するべきです。

例えば、「Car」というクラスが「Engine」というクラスに依存しているとします。DIPによると、「Car」クラスは特定の「Engine」クラスに依存してはいけず、エンジンとは何かという抽象化に依存すべきです。

// Incorrect way of doing it
class Engine {
    start(): void {
        // code for starting the engine
    }
}

class Car {
    private engine: Engine;

    constructor() {
        this.engine = new Engine();
    }

    start(): void {
        this.engine.start();
    }
}
// Correct way of doing it
interface Engine {
    start(): void;
}

class RealEngine implements Engine {
    start(): void {
        // code for starting the engine
    }
}

class Car {
    private engine: Engine;

    constructor(engine: Engine) {
        this.engine = engine;
    }

    start(): void {
        this.engine.start();
    }
}

const car = new Car(new RealEngine());

ケースを使う

1. インターネットショッピングのチェックアウトプロセス

お客様がカートにアイテムを追加し、チェックアウトページに進むeコマースウェブサイトを考えましょう。チェックアウトプロセスには、アイテムの合計金額を計算し、割引やプロモーションを適用し、そして支払いを処理することが含まれます。

SRPによると、チェックアウトプロセスを異なるクラスに分割すべきです。それぞれが独自の責任を持つようにします。例えば、カート内のアイテムを追跡するCartクラス、割引やプロモーションを適用するDiscountsクラス、そして支払い処理を行うPaymentクラスなどを作成することができます。

class Cart {
    items = [];
    addItem(item) {
        this.items.push(item);
    }
    getTotal() {
        return this.items.reduce((total, item) => total + item.price, 0);
    }
}

class Discounts {
    applyDiscount(total) {
        return total * 0.9; // 10% off
    }
}

class Payment {
    processPayment(total) {
        // code for processing the payment
    }
}

class Checkout {
    cart;
    discounts;
    payment;

    constructor(cart, discounts, payment) {
        this.cart = cart;
        this.discounts = discounts;
        this.payment = payment;
    }

    processCheckout() {
        const total = this.discounts.applyDiscount(this.cart.getTotal());
        this.payment.processPayment(total);
    }
}

const cart = new Cart();
cart.addItem({ name: "item1", price: 20 });
cart.addItem({ name: "item2", price: 30 });

const checkout = new Checkout(cart, new Discounts(), new Payment());
checkout.processCheckout();

この例では、それぞれのクラスに1つの責任があります:Cartクラスはカート内の商品を追跡します、Discountsクラスは割引を適用します、Paymentクラスは支払いを処理します、Checkoutクラスはプロセスを調整します。これにより、コードが保守性が高く、理解しやすくなります。

2. 天気アプリ

天気アプリに、現在の場所の気温、湿度、気圧を表示する機能があります。新しい機能として、風速と風向きを表示する機能を追加したいと思います。Open-Closed Principle(OCP)によると、既存のコードを変更することなく、この新しい機能を追加できるはずです。

class WeatherData {
  constructor(temperature, humidity, pressure) {
    this.temperature = temperature;
    this.humidity = humidity;
    this.pressure = pressure;
  }
}

class WeatherDisplay {
  display(weatherData) {
    console.log(`Temperature: ${weatherData.temperature}`);
    console.log(`Humidity: ${weatherData.humidity}`);
    console.log(`Pressure: ${weatherData.pressure}`);
  }
}

class WindDisplay {
  display(weatherData) {
    console.log(`Wind speed: ${weatherData.windSpeed}`);

    console.log(`Wind direction: ${(weatherData, windDirection)}`);
  }
}

class WeatherApp {
  weatherData;
  weatherDisplay;
  windDisplay;
  constructor(weatherData) {
    this.weatherData = weatherData;
    this.weatherDisplay = new WeatherDisplay();
    this.windDisplay = new WindDisplay();
  }

  displayWeather() {
    this.weatherDisplay.display(this.weatherData);
    this.windDisplay.display(this.weatherData);
  }
}

const weatherData = new WeatherData(72, 50, 1013);
weatherData.windSpeed = 5;
weatherData.windDirection = "NW";
const weatherApp = new WeatherApp(weatherData);
weatherApp.displayWeather();

この例では、既存のWeatherDisplayクラスを変更せずに、新しいWindDisplayクラスを追加することで、WeatherAppクラスを拡張できます。これにより、既存のコードに影響を与えることなく、アプリに新しい機能を追加できます。

3. ゲームのキャラクター

新しいキャラクターを追加したいと思っていますが、既存のゲームメカニクスを壊さないようにしたいです。LSPというものを使えば、親キャラクタークラスを使う場所で新しいキャラクタークラスを使っても、ゲームが正しく動くようになります。

class Character {
    move() {
        console.log("Character moved");
    }
}

class Warrior extends Character {
    attack() {
        console.log("Warrior attacked");
    }
}

class Mage extends Character {
    castSpell() {
        console.log("Mage cast a spell");
    }
}

class Paladin extends Warrior {
    heal() {
        console.log("Paladin healed");
    }
}

const characters = [new Warrior(), new Mage(), new Paladin()];
for (let character of characters) {
    character.move();
    if (character instanceof Warrior) {
        character.attack();
    }
    if (character instanceof Mage) {
        character.castSpell();
    }
    if (character instanceof Paladin) {
        character.heal();
    }
}

この例では、PaladinクラスはWarriorクラスのサブクラスで、回復する独自の能力を持っていますが、親クラスからmoveメソッドを正しく実装しているので、キャラクターオブジェクトが使われるどこでも使えます。これにより、既存のゲームメカニクスを壊さずに新しいキャラクタータイプを追加できます。

4. チャットアプリ

メッセージを送信する機能とファイルを送信する機能を分けることで、片方の機能しか必要ないクライアントはもう片方の機能を実装する必要がなくなります。インターフェース分離の原則(ISP)に従うと、メッセージを送信するインターフェースとファイルを送信するインターフェースを分けるべきです。

interface MessageSender {
  sendMessage(message: string): void;
}

interface FileSender {
  sendFile(file: File): void;
}

class ChatClient implements MessageSender {
  sendMessage(message: string): void {
    // code for sending a message
  }
}

class FileTransferClient implements FileSender {
  sendFile(file: File): void {
    // code for sending a file
  }
}

class AdvancedChatClient implements MessageSender, FileSender {
  sendMessage(message: string): void {
    // code for sending a message
  }
  sendFile(file: File): void {
    // code for sending a file
  }
}

const chatClient = new ChatClient();
chatClient.sendMessage("Hello!");

const fileTransferClient = new FileTransferClient();
fileTransferClient.sendFile(new File("file.txt"));

const advancedChatClient = new AdvancedChatClient();
advancedChatClient.sendMessage("Hello!");
advancedChatClient.sendFile(new File("file.txt"));

この例では、ChatClientクラスはMessageSenderインターフェースのみを実装し、FileSenderインターフェースを実装する必要はありません。また、FileTransferClientクラスはFileSenderインターフェースのみを実装し、MessageSenderインターフェースを実装する必要はありません。これにより、クライアントは必要な機能だけを実装し、コードを明確かつ理解しやすく保つことができます。

5. ソーシャルメディアプラットフォーム

私たちがテキストや画像の更新を扱うコードを変更せずに、ユーザーが動画の更新を投稿できる新しい機能を追加したいとします。依存性反転原則(DIP)によると、更新を処理するコードは特定のクラスや関数に依存しないようにして、抽象的な概念に依存するようにしなければなりません。

interface Update {
  display(): void;
}

class TextUpdate implements Update {
  text: string;
  constructor(text: string) {
    this.text = text;
  }
  display(): void {
    console.log(`Text Update: ${this.text}`);
  }
}

class ImageUpdate implements Update {
  imageUrl: string;
  constructor(imageUrl: string) {
    this.imageUrl = imageUrl;
  }

  display(): void {
    console.log(Image Update: ${ this.imageUrl });
  }
}

class VideoUpdate implements Update {
  videoUrl: string;
  constructor(videoUrl: string) {
    this.videoUrl = videoUrl;
  }
  display(): void {
    console.log(Video Update: ${ this.videoUrl });
  }
}

class SocialMediaApp {
  updates: Update[];
  constructor() {
    this.updates = [];
  }
  addUpdate(update: Update) {
    this.updates.push(update);
  }

  displayUpdates() {
    this.updates.forEach(update => update.display());
  }
}

const socialMediaApp = new SocialMediaApp();
socialMediaApp.addUpdate(new TextUpdate("Hello, world!"));
socialMediaApp.addUpdate(new ImageUpdate("image.jpg"));
socialMediaApp.addUpdate(new VideoUpdate("video.mp4"));
socialMediaApp.displayUpdates();

この例では、テキスト、画像、またはビデオ更新を処理する特定のクラスではなく、「Update」インターフェースの抽象的な概念に依存しています。これにより、ビデオ更新などの新しいタイプの更新をテキストと画像の更新を処理する既存のコードを変更することなく追加できます。

Conclusion

SOLIDの原則は、開発者がきれいで保守性があり、理解しやすいコードを書くのを助けるガイドラインの集まりです。これらの原則を守ることで、開発者はコードが扱いやすく、将来的に拡張したり変更したりするのが容易になります。ソリッドの原則とは、Single Responsibility Principle (SRP), Open-Closed Principle(OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle(ISP), and Dependency Inversion Principle(DIP)の5つです。それぞれの原則には、解説、利点、実際のコードスニペットを含む実用例があります。重要なのは、ソリッドの原則は厳格なルールではなく、特定のプロジェクトやアプリケーションに応じて異なる方法で適用できる一般的なガイドラインであるということです。

最後

いつもお世話になっています。この記事を楽しんで、新しいことを学べたら嬉しいです。

今度の記事でお会いしましょう!この記事が気に入ったら、私を応援するために「LIKE」を押して登録してください。ありがとうございました。


この記事の主な目的は、日本語レベルを上げるのを手伝うことです。ソフトウェア開発に関連する概念や知識なとを紹介するために簡単な日本語を使います。ITの知識に関しては、インターネット上でもっとよく説明されているかもしれませんが、この記事の主な目標はまだ日本語を学ぶことです。

NGUYỄN ANH TUẤN

Xin chào, mình là Tuấn, một kỹ sư phần mềm đang làm việc tại Tokyo. Đây là blog cá nhân nơi mình chia sẻ kiến thức và kinh nghiệm trong quá trình phát triển bản thân. Hy vọng blog sẽ là nguồn cảm hứng và động lực cho các bạn. Hãy cùng mình học hỏi và trưởng thành mỗi ngày nhé!

Đăng nhận xét

Mới hơn Cũ hơn