LoginSignup
55
46

猿でもわかるオブジェクト指向とデザインパターン

Last updated at Posted at 2023-09-05

はじめに

本記事では、初学者向けにオブジェクト指向について簡単に解説します。
細かい文法部分については省略していますが、イメージを掴んでもらえたら嬉しいです。

また、本記事ではGoFのデザインパターンについても簡単に紹介します。
今回は、Singleton、Factory、Strategy、Observer、Adaptorパターンについて取り上げます。

初学者の方は、今回紹介するデザインパターンをなんとなくでも理解できたらバッチリです。

前提知識

デザインパターンを学ぶにあたって、オブジェクト指向についての理解は必須になります。
ここでは前提知識として、クラスとインスタンスの関係、オブジェクト指向の3大要素について簡単に解説をしておきます。
不要な方はそもそもデザインパターンとは?から読み進めてください。

クラス と インスタンス

クラス

クラスのイメージは"設計図"です。
例として、家を建てる時のことを考えてみます。

ある人は、
間取り:2LDK
階層:2階建て
壁の色:白系統
造り:鉄筋コンクリート造

別の人は、
間取り:3LDK
階層:1階建て平屋
壁の色:黒
造り:木造

の家を建てたとします。

同じ条件でも、全く別の家が建ちましたね。
これらの家の共通部分(条件)を抽出すると、

  • 間取り
  • 階層
  • 壁の色
  • 造り

これら4つの属性が考えられます。
ではこれをコードに落とし込んでみましょう。

class House {
    // 属性
    String layout;   // 間取り
    int floors;      // 階層
    String wallColor; // 壁の色
    String construction; // 造り

    // 各種メソッドがここへ
}

これがクラスです。(実際はコンストラクタや各種メソッドを含む)
あるオブジェクトに対して、それらの構成を抽象化したものをクラスと言います。

class Human {
    // 属性
    String name;   // 氏名
    int age;      // 年齢
    String gender; // 性別
    
    // 各種メソッドがここへ
}
class File {
    // 属性
    String name;   // ファイル名
    int size_KB;      // ファイルサイズ(KB)
    String filePath; // ファイルの絶対パス

    // 各種メソッドがここへ
}

現実では設計図を書かないような、人や電子データなどの抽象的な物ついてもクラスの概念を適用できます。


インスタンス

では、インスタンスとは何でしょう。
よく言われるのは"実体"ですね。

簡単に言うと、

  • 間取り:2LDK
  • 階層:2階建て
  • 壁の色:白系統
  • 造り:鉄筋コンクリート造

これのことです。

クラス(設計図)を基に、具体的な値を当てはめて実体化したものをインスタンスと呼びます。

  • 山田さん(23歳男性)
  • 田中さん(50歳女性)
  • 佐藤さん(2歳男の子)

これらはすべてHumanクラスのインスタンス(実体)です。
名前・年齢・性別 という共通の属性を持っていますが、値が違うため全くの別人ですよね。

オブジェクト指向では、これらの各インスタンスたちが相互にやり取りを行うことでシステムが構築されます。
オブジェクト(物)にフォーカスした考え方とも言えますね。

まとめると、
クラス:オブジェクトの構造を定義
インスタンス:クラスを基に作られた実体
です。

オブジェクト指向の3大要素

オブジェクト指向には、"継承","カプセル化","多態性"の3大要素があります。

継承

あるクラスを拡張して(引き継いで)、新しいクラスを作成することを継承といいます。

例として、図形を考えてみます。

public abstract class Figure{
    private int teihen;
    private int takasa;

    public abstract int calcArea();
}

今回は簡単に、図形は底辺と高さの2つの属性を持つものとします。
Figureクラスでは、自身の面積を求めるcalcAreaメソッドを定義しています。

ここで注目すべきは、calcAreaメソッドの中身です。
具体的な処理が書かれておらず、戻り値の型とメソッド名のみ定義されています。
このようなメソッドを抽象メソッドと言います。
※抽象メソッドを持つクラスを抽象クラスといい、abstractを使って定義します。

具体的な説明は省略しますが、自クラスを継承した子クラスに具体的な処理内容の実装を任せています。

では、これを基に四角形・三角形クラスを考えてみましょう。

// 四角形
public class Rectangle extends Figure{
    @Override 
    public int calcArea(){
        return this.teihen * this.takasa;
    }
}
// 三角形
public class Triangle extends Figure{
    @Override 
    public int calcArea(){
        return this.teihen * this.takasa / 2;
    }
}

Figureクラスを継承することで、Figureの派生版クラスRectangleとTriangleを実装できました。
親:Figure
子:Rectangle,Triangle の関係性です。

また、コードには書かれていませんが、TriangleはFigureを継承しているので、teihen と takasa の属性を持っています。

注目するべき部分としては、calcAreaメソッドの中身が各クラスで異なっている点です。
当たり前ですが、三角形と四角形では面積を求める公式は違いますよね。
これに対応するために、Figureではメソッドの枠組みだけ提供し、継承先で具体的な処理を上書き実装しています。
※親クラスのメソッドを上書きすることをOverrideといいます。

実際に面積を求めてみると、

Rectangle rect = new Rectangle();
rect.teihen = 3;
rect.takasa = 4;
System.out.println(rect.calcArea());// 12が表示される

Triangle tri = new Triangle();
tri.teihen = 4;
tri.takasa = 5;
System.out.println(tri.calcArea());// 10が表示される

Figure fig = new Figure();// これはできない

カプセル化

オブジェクトの持つメンバ(変数など)を隠蔽することをカプセル化といいます。
簡単に言うと、変数などの値を参照・更新できる範囲を定義することをいいます。

例えば、

public class Human {
    // 属性
    public String name;   // 氏名
    private int age;      // 年齢
    public String gender; // 性別

    //コンストラクタ
    public Human(String name, int age, String gender){
        this.name = name;
        this.age = age;
        this.gender = gender;
    }
}

このようなHumanクラスが定義されている時、


public class Main{
    public static void main(String[] args){
        //Humanクラスのインスタンスを生成
        Human tanaka = new Human("田中", 30, "男");

        //できる
        tanaka.name = "佐藤";
        //できない
        tanaka.age = 50;
    }
}

このような現象が起こり得ます。

Humanクラスにおいて、'name'はpublicで定義されているため、外部のクラスから参照・変更が可能になっています。
逆に、'age'についてはprivateで定義されているため、自クラスからのみアクセス可能です。

このように、属性等の参照権限・公開レベルを設定し、適切に隠蔽することをカプセル化と呼びます。
なお、privateなどの修飾子は'アクセス修飾子'と呼ばれます。
※言語によって種類・仕様が異なります。

多態性(ポリモーフィズム)

語弊を恐れず簡単に言うと、
ある1つのメソッドの呼び出しに対して、オブジェクト毎に異なる機能や動作をすることを多態性といいます。

例えば、

public abstract class Animal{
    public abstract void bark();
}

public class Dog extends Animal{
    public void bark(){
        System.out.println("わん!");
    }
}

public class Cat extends Animal{
    public void bark(){
        System.out.println("にゃー!");
    }
}

public class Main{
    public static void main(String[] args){
        //Animalの子クラスをインスタンス化
        Dog dog = new Dog();
        Cat cat = new Cat();

        //listに格納
        List<Animal> animals = new ArrayList<Animal>();
        animals.add(dog);
        animals.add(cat);

        //それぞれ鳴かせる           \わん!/   \にゃー!/
        animals.forEach(animal -> animal.bark());
    }
}

この時、繰り返し文で各インスタンスのbarkメソッドを呼んでいますが、
動物ごとに違う鳴き声を発します。

このように、メソッドの呼び出しに対してオブジェクトごとに振る舞いを変えることを多態性といいます。
'多'くの 振る舞い('態') を持つ '性'質 と考えればわかりやすいですね。

ここまでがオブジェクト指向の概念についての簡単な解説です。
今回は概念的な部分のみ解説しましたが、文法的な理解も必須のものなのでご自身でしっかり学んでください。

そもそもデザインパターンとは?

ここからが本題です。

デザインパターンとは、オブジェクト指向において色んなプログラムで再利用できる設計パターンのことです。
ざっくりいうと、こう設計したら保守楽になるよね、読みやすくなるよね っていうパターンをまとめたものです。
様々なデザインパターンが存在していますが、今回は一般的なGoFのデザインパターンの中から5種類を解説します。

デザインパターン

Singletonパターン

特定のクラスのインスタンスがプログラム内で唯一であることを保証します。

そのクラスの唯一のインスタンスがシステム全体で共有される必要がある場合や、特定の設定、ログ、キャッシュ、データベース接続などのリソースへのアクセスを制御する必要がある場合に役立ちます。

public class Singleton {
    // インスタンスを格納するプライベート変数
    private static Singleton instance;

    // プライベートコンストラクタ(外部からのインスタンス化を防ぐ)
    private Singleton() {
        // シングルトンの初期化処理
    }

    // シングルトンインスタンスを取得するための静的メソッド
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

    // その他のメソッドやフィールドをここに追加できます

    public void doSomething() {
        System.out.println("シングルトンが何かを実行中です。");
    }
}

Singletonでは、コンストラクタをprivateで定義することで外部からのインスタンス化を防いでいます。
代わりに、クラスのメンバーとして定義しているstaticな自クラスのインスタンスをメソッド経由で渡してあげることで、インスタンスが唯一であることを保証しています。

Factoryパターン

Factoryパターンは、生成に関するパターンの一種です。

このパターンは、オブジェクトの生成処理部分をカプセル化し、直接インスタンスを作成する代わりに、ファクトリーメソッドを介してオブジェクトを生成する方法を提供します。これにより、クライアントコードは具体的なオブジェクトの生成方法に関与せず、結果として柔軟性や保守性の向上が見込めます。

Factoryパターンには主に2つのバリエーションがあります。

Factory Methodパターン

このパターンでは、特定のクラスを生成するためのメソッドをカプセル化します。

クラスの具体的な生成方法を知らなくてもオブジェクトの生成が可能になり、新しいオブジェクトを追加する際にも既存のコードを変更する必要が無く実装が可能になります。

例として、ウェブサイトの生成をするコードを考えてみます。
ページの種類ごとにファクトリーメソッドが用意されており、使用者は具体的なページの生成方法を知らずに生成できます。

// ページの抽象クラス
abstract class Page {
    abstract void render();
}

// 具体的なページのサブクラス
class WelcomePage extends Page {
    @Override
    void render() {
        System.out.println("Welcome Page");
    }
}

class AboutPage extends Page {
    @Override
    void render() {
        System.out.println("About Page");
    }
}
// ページファクトリーのインタフェース
interface PageFactory {
    Page createPage();
}

// 具体的なページファクトリークラス
class WelcomePageFactory implements PageFactory {
    @Override
    public Page createPage() {
        return new WelcomePage();
    }
}

class AboutPageFactory implements PageFactory {
    @Override
    public Page createPage() {
        return new AboutPage();
    }
}
public class Main {
    public static void main(String[] args) {
        // Welcomeページを生成
        PageFactory welcomeFactory = new WelcomePageFactory();
        Page welcomePage = welcomeFactory.createPage();
        welcomePage.render();
        
        // Aboutページを生成
        PageFactory aboutFactory = new AboutPageFactory();
        Page aboutPage = aboutFactory.createPage();
        aboutPage.render();
    }
}

Abstract Factoryパターン

このパターンでは、抽象的なファクトリークラスを実装し、関連した生成対象クラス群の生成をカプセル化します。

// 抽象ボタンクラス
interface Button {
    void render();
    void onClick();
}

// 具体的なWindowsスタイルボタン
class WindowsButton implements Button {
    @Override
    public void render() {
        System.out.println("Render a Windows button");
    }

    @Override
    public void onClick() {
        System.out.println("Click on a Windows button");
    }
}

// 具体的なMacスタイルボタン
class MacButton implements Button {
    @Override
    public void render() {
        System.out.println("Render a Mac button");
    }

    @Override
    public void onClick() {
        System.out.println("Click on a Mac button");
    }
}
// 抽象ファクトリーインタフェース
interface GUIFactory {
    Button createButton();
}

// 具体的なWindowsスタイルファクトリー
class WindowsFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new WindowsButton();
    }
}

// 具体的なMacスタイルファクトリー
class MacFactory implements GUIFactory {
    @Override
    public Button createButton() {
        return new MacButton();
    }
}
public class Main {
    public static void main(String[] args) {
        String os = "Windows"; // ここで使用するGUIスタイルを選択

        GUIFactory factory;

        if (os.equals("Windows")) {
            factory = new WindowsFactory();
        } else if (os.equals("Mac")) {
            factory = new MacFactory();
        } else {
            throw new UnsupportedOperationException("Unsupported OS");
        }

        Button button = factory.createButton();
        button.render();
        button.onClick();
    }
}

どちらのパターンも、オブジェクトの生成プロセスをカプセル化し、柔軟性と保守性を向上させることが可能です。

違いとしては、
Factory Methodパターンが、特定のクラスの生成方法をカプセル化しているのに対し、
Abstract Factoryパターンでは、特定の生成対象クラス群の生成方法をカプセル化しています。

どちらのパターンを選択するかは、システムが要求する使用に依存します。
関連するクラス群を生成する必要がある場合は Abstract Factory パターンを、
単一のオブジェクトを生成するだけで済む場合は Factory Method パターンを使用することが一般的です。

Strategyパターン

Strategyパターンでは、具体的なアルゴリズムの実装をクラスとして別で定義します。
これにより処理部分の交換が可能になり、アルゴリズムの柔軟性や拡張性の向上が見込めます。

例として、支払い方法をクレジットカードとPayPayで選べるような実装を行ってみます。

// Strategy インターフェース
interface PaymentStrategy {
    void pay(int amount);
}

// 具体的なストラテジー
class CreditCardPayment implements PaymentStrategy {
    private String cardNumber;
    private String name;

    public CreditCardPayment(String cardNumber, String name) {
        this.cardNumber = cardNumber;
        this.name = name;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + "円をクレジットカードで支払いました。");
    }
}

class PayPalPayment implements PaymentStrategy {
    private String email;

    public PayPalPayment(String email) {
        this.email = email;
    }

    @Override
    public void pay(int amount) {
        System.out.println(amount + "円をPayPalで支払いました。");
    }
}
// コンテキスト
class ShoppingCart {
    private PaymentStrategy paymentStrategy;

    public void setPaymentStrategy(PaymentStrategy paymentStrategy) {
        this.paymentStrategy = paymentStrategy;
    }

    public void checkout(int amount) {
        paymentStrategy.pay(amount);
    }
}
public class Main {
    public static void main(String[] args) {
        ShoppingCart cart = new ShoppingCart();
        
        // クレジットカードで支払う
        cart.setPaymentStrategy(new CreditCardPayment("1234-5678-9876-5432", "John Doe"));
        cart.checkout(1000);
        
        // PayPalで支払う
        cart.setPaymentStrategy(new PayPalPayment("john.doe@example.com"));
        cart.checkout(500);
    }
}

この例では、クレジットカードとPayPayの2種類の支払い方法をそれぞれStrategyクラスとして実装し、
それらを具体的な支払い処理方法として、後からShoppingCartクラスに渡してあげることで、柔軟性がある実装になっています。

Strategy(=戦略)そのものをオブジェクトとして定義している部分が面白い発想ですね。

Observerパターン

Observerパターンでは、観察者クラス(Observer)を設け、特定のオブジェクト(Subject)に変更があった際にObserverに対して通知を行います。
通知をもらったObserverクラスは、それに応じた処理を行います。

// Observer (オブザーバー) インターフェース
public interface Observer{
    public abstract void update(NumberGenerator generator);
}

// Subject (サブジェクト) クラス
public abstract class NumberGenerator {
    private ArrayList observers = new ArrayList();
    public void addObserver(Observer observer) {
        observers.add(observer);
    }
    public void deleteObserver(Observer observer) {
        observers.remove(observer);
    }
    // 通知をObserverに送るメソッド
    public void notifyObservers() {
        Iterator it = observers.iterator();
        while(it.hasNext()) {
            Observer o = (Observer)it.next();
            o.update(this);
        }
    }
    // 通知するnumberを取得
    public abstract int getNumber();
    // numberの生成と通知処理
    public abstract void execute();
}
// Concrete Subject (具体的なサブジェクト)
public class RandomNumberGenerator extends NumberGenerator {
    private Random random = new Random();
    private int number;
    @Override
    public int getNumber() {
        return number;
    }
    @Override 
    public void execute() {
        for(int i = 0; i < 20; i++) {
            number = random.nextInt(50);
            notifyObservers();// ここで通知を送っている
        }
    }
}
// Concrete Observer (具体的なオブザーバー)
public class DigitObserver implements Observer {
    @Override
    public void update (NumberGenerator generator) {
        System.out.println("DigitObserver:" + generator.getNumber());
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
    }
}

public class GraphObserver implements Observer {
    @Override
    public void update(NumberGenerator generator) {
        System.out.print("GraphObserver:");
        int count = generator.getNumber();
        for (int i = 0; i < count; i++) {
            System.out.print("*");
        }
        System.out.println("");
        try {
            Thread.sleep(100);
        } catch (InterruptedExeption e) {
        }
    }
}
public class Main {
    public static void main(String[] args) {
        NumberGenerator generator = new RandomNumberGenerator();
        Observer observer1 = new DigitObserver();
        Observer observer2 = new GraphObserver();
        generator.addObserver(observer1);
        generator.addObserver(observer2); 
        generator.execute();
    }
}

この例では、Concrete Subjectのexecuteメソッド内で通知を行っています。

Adapterパターン

Adapterパターンは、2つの異なるインターフェース間で互換性を提供します。
このパターンは、既存のクラスを新しいクラスに統合する必要がある場合や、異なるインターフェースを持つクラスを協力させる場合に役立ちます。

充電ケーブルなどで言うアダプターと役割は同じで、
2つのクラス間の仕様の差異を吸収し、相互にやり取りをできるようにすることが主な目的です。

// Target インターフェース
interface Target {
    void request();
}

// Adaptee(適応元)クラス
class Adaptee {
    void specificRequest() {
        System.out.println("Adaptee's specific request");
    }
}
// Adapter クラス(オブジェクトアダプター)
class ObjectAdapter implements Target {
    private Adaptee adaptee;

    public ObjectAdapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void request() {
        adaptee.specificRequest();
    }
}
public class Main {
    public static void main(String[] args) {
        Adaptee adaptee = new Adaptee();
        Target adapter = new ObjectAdapter(adaptee);
        adapter.request();
    }
}

このパターンは、既存のクラスを新しいインターフェースに適合させる場合や、
既存のクラスやライブラリを変更せずに、新しいインターフェースに合わせる場合などに有効です。

終わりに

今回はオブジェクト指向の概念とデザインパターン5つを紹介しました。
今回取り上げたデザインパターンはGofのデザインパターンと呼ばれるもので、全23種類存在します。
これらを理解することで、より効率的な実装を行えるようになるので、スキルアップをしたい方はぜひ学んでみてください。

55
46
2

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
55
46