1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

知らないことを浅く理解するAdvent Calendar 2024

Day 5

そろそろオブジェクト指向に決着をつけようか…

Last updated at Posted at 2024-12-05

今まで何度かオブジェクト指向について触れたが、毎回なんとなくわかったような腑に落ちないような複雑な気持ちになったのでここで決着をつけたい。

「書けるようになる」ではなくて「”完全に理解する”」ことと「便利じゃん!使ってみよう!」ってなることが目的

そもそもオブジェクト指向って何だっけ??

英語表記の「Object Oriented」は「対象物指向」「目的指向」と訳され、ソフトウェアの設計開発で、操作の手順よりも操作対象自体にポイントを置く考え方とされる。
カプセル化されたオブジェクトの中身(データ構造や手続き)を気にすることなく、ソフトウェアを組み立てる考え方である。

です。少し小難しく感じます:crying_cat_face:

イメージとしては、

オレンジジュースとは 液体で、飲むことができて、オレンジの荷重が入ったもの、飲むと減る
コーヒーとは     液体で、飲むことができて、珈琲豆を挽いたもの、飲むと減る
ビールとは      液体で、飲むことができて、麦を発酵させたものである、飲むと減る

って感じでひとつづつ定義していくよりも

飲み物とは      液体で、飲むことができるもの、飲むと減る

オレンジジュースとは 飲み物の一種で、オレンジの果汁が入ったもの
コーヒーとは     飲み物の一種で、珈琲豆を挽いたものである
ビールとは      飲み物の一種で、麦を発酵させたものである

と定義した方が分かりやすい(この例だと特徴が少ないからそこまで変わらないけど、特徴が増えれば余計に)
し、飲み物の定義を変えてやるだけでいいから保守性が上がるよねって感じ

この考え方がオブジェクト指向。

関数でよくね??

ここまでの説明だと、きっとこれだったら関数で良くね??って疑問が出てくると思う。

例えば

def drink(特徴):
	return {"液体", "飲める", 特徴}
	
オレンジジュース = drink("オレンジの果汁が入っている")
珈琲 = drink("珈琲豆を挽いたもの")
ビール = drink("麦を発酵させたもの")

といった具合に

関数ではなくてオブジェクト指向を使う理由をオブジェクト指向の説明で良く出てくる四銃士の説明と一緒に説明する。

簡単に言ってしまえば、大規模開発の時に管理しやすくする為なのだが…

オブジェクト指向の説明で良く出てくる四銃士

オブジェクト指向四銃士を連れてきたよ

🔴カプセル化

カプセル化は、データ(変数)とそのデータに関する操作(関数)を1つにまとめること。

これによって、入っている値だったり関数を保証することができたり(ミスで入っていないとかが無くなる)し、privateとかでアクセス制限をかければ外から値を変更できなくなって操作ミスで変な値が入るなんてことが無くなる。
オブジェクト指向を使うと

class Drink:
    def __init__(self, name, ingredient):
		    self.
        self.name = name               # 名前(例:オレンジジュース)
        self.ingredient = ingredient   # 材料(例:オレンジ果汁)
        self.amount = 100              # 飲み物の量(%)

    def drink(self, sip_amount):
        # 「飲む」操作をこのクラス内で定義し、量を減らす処理もここにまとめる
        self.amount -= sip_amount
        if self.amount < 0:
            self.amount = 0
        print(f"{self.name}{sip_amount}%飲んだ。残り: {self.amount}%")

# 使用例
orange_juice = Drink("オレンジジュース", "オレンジ果汁")
coffee = Drink("コーヒー", "珈琲豆")

# 操作は簡単に「オブジェクト.メソッド()」の形で
orange_juice.drink(20)  # オレンジジュースを20%飲む
coffee.drink(10)       # コーヒーを10%飲む

こんな感じで書ける。

一応関数でも

# 飲み物ごとの状態を保持する辞書
orange_juice = {"name": "オレンジジュース", "ingredient": "オレンジ果汁", "amount": 100}
coffee = {"name": "コーヒー", "ingredient": "珈琲豆", "amount": 100}

# 共通の飲む機能を関数として定義
def drink(beverage, sip_amount):
    beverage["amount"] -= sip_amount
    if beverage["amount"] < 0:
        beverage["amount"] = 0
    print(f"{beverage['name']}{sip_amount}%飲んだ。残り: {beverage['amount']}%")

# 使用例
drink(orange_juice, 30)  # オレンジジュースを30%飲む
drink(coffee, 15)        # コーヒーを15%飲む

って感じで書けるけど、値が入っている保証ができなかったり、オブジェクト内に関数を持つ。みたいなことができない

しかも、オブジェクト指向ならprivate指定することで外部から値を書き換えられなくできる。

これによってコードのミスで変な値が入らなくすることができる

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

違うクラスから生成されたオブジェクトは同じ関数名の処理でも処理の内容を変えられるってこと

これによって、色々関数名を考える必要がなくなる

その上、新しく飲み物を作るときもクラスとして新しくブロックを作ることとなって、元のコードを大きく変えるわけではないので、保守性が上がる

class Juice:
    def __init__(self, name):
        self.name = name

    def drink(self):
        print(f"{self.name}のジュースを飲む!")

class Coffee:
    def __init__(self, name):
        self.name = name

    def drink(self):
        print(f"{self.name}のコーヒーを飲む!")

class Tea:
    def __init__(self, name):
        self.name = name

    def drink(self):
        print(f"{self.name}のお茶を飲む!")

# それぞれのオブジェクトを作成
orange_juice = Juice("オレンジ")
black_coffee = Coffee("ブラック")
green_tea = Tea("")

beverages = [orange_juice, black_coffee, green_tea]
for beverage in beverages:
    beverage.drink() # 同じdrinkという関数名でもそれぞれ違う処理をされる

🟢継承

あるクラスを基に別のクラスを作るイメージ

例えば飲み物クラスの子クラスとしてジュースクラス を作ったり、珈琲クラス を作ったりできるってこと

これによって重複して同じコードを書く部分を減らすことができる。

コードを重複して書く必要がなくなるため、コードの編集が楽になったり、読みやすくなったりする。

class Drink:
    def __init__(self, name):
        self.name = name
      
    def drink(self):
        raise NotImplementedError("このメソッドはサブクラスで定義されるべきです")

class Juice(Drink):
    def drink(self):
        print(f"{self.name}のジュースを飲む!甘くて美味しい!")

class Coffee(Drink):
    def drink(self):
        print(f"{self.name}のコーヒーを飲む!香り高い一杯!")

orange_juice = Juice("オレンジ")
coffee = Coffee("ブラック")

beverages = [orange_juice, coffee]
for beverage in beverages:
    beverage.drink() 

🟡抽象化

親クラスに処理を定義していない関数(抽象メソッド)を定義することで、オーバーライド(上書き)し忘れを防ぐことができる

継承で紹介したコードのdrinkクラス

def drink(self):
	raise NotImplementedError("このメソッドはサブクラスで定義されるべきです")

の部分
また、親クラスに抽象メソッドを定義しておくことで、何を実装すべきか分かりやすくなる

まとめ

正直、もっとうまく説明できたら...と、もどかしくはあるけれどもそこそこ理解が進んだ気がする。

オブジェクト指向は大規模開発になると輝くもので、今まで何となく理解できなかったのは大規模開発のイメージがあまり思い浮かばなかったからなのかもしれない

1
0
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
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?