きゃらめるの備忘録

Salesforceに関してお勉強したことをまとめるブログ。目指せ週1更新~~~!

Salesforceを知ってる人に向けて、クラスとオブジェクト指向を言語化する

こんばんは、きゃらめるです。
今日はApexに限らない、一般的なプログラミングの話をしたいと思います。
タイトルの通り、「クラス」とはなんぞや、「オブジェクト指向」はなぜ使うんやという話をします。

今回の内容は、私が社会人1年目の頃にあやふやな理解をしていた部分でもあります。
この部分をイメージに落とし込むことができたことで、やっと思い通りに使えるようになりました。
イメージに落とし込むには、経験ももちろん大事でしたが、いろんな説明を聞くのもとても役立ちました。
1つの説明を聞いただけではわからなくても、いろんな人の説明を聞いていくと、少しずつ蓄積された知識やイメージのお陰で理解が進んだり、どれか一つの説明が絶妙に理解に刺さったり、ということが多々ありました。
今回の記事でも、なるべくイメージを伝えられるように言語化してみようと思いますが、ぜひ私の説明だけではなく、いろんな説明を読んでみて欲しいです。

今回説明する内容

今回、イメージを共有しておきたい内容としては3つあります。

クラスの概念

ApexにはApexクラスがありますね。では、クラスってなんなんでしょう?どういう単位なんでしょう?
クラスを大まかな私の理解で表すと、
一つの概念について、「情報・要素」と「できること」をまとめたもの
です。
これをインスタンス化すること(= new MyClass()で呼び出すこと)で「実体」となります。

正直、昔の私はこれだけ聞いてもよく分かりませんでした。でも実は、クラスの考え方は実世界にある様々なものにあてはめられます。

実世界とクラスの考えを結びつける

例を挙げます。私も、今このブログを読んでいるあなたも、実在しています。つまり、私たちは「実体」です。

(a)私たちは名前を持っています。
(b)私たちは年齢を持っています。
(c)私たちは生物学上の雌雄を持っています。
(Ⅰ)私たちは食べることができます。
(Ⅱ)私たちは寝ることができます。

(a)~(c)は私たちが持つ「情報・要素」です。(Ⅰ)(Ⅱ)は私たちが「できること」です。
これらは私たちだけでなく、人間全体に共通することですよね。
この(a)~(c)と(Ⅰ)(Ⅱ)をまとめたものが、人間という「概念」です。
※実際の人間が持つ「情報・要素」や「できること」はもっと多いですが、簡略化しています。

クラスは、この人間という概念を記述しておくものです。それを、私たち「実体」として扱うためにインスタンス化を行います。
インスタンス化することで、実体ごとに異なる指示を出したり、情報を変更・閲覧することが可能です。

実体ごとに異なるとはどういうことか、こちらも例を挙げます。
人間は誕生日を迎えると年齢が1つ上がりますよね。では、私が誕生日を迎えたとき、あなたの年齢は上がるでしょうか?
誕生日が一緒でない限り、実世界でそんなことはありえませんよね。
なぜなら、私とあなたは、別の実体として独立しているからです。

この、独立している1つの実体を表しているのが1つのインスタンスです。
なので、「インスタンス化する」ということは、私やあなたのように別の人間を生み出す行為にあたります。
もう一つ追記しておくと、クラスの変数が「情報・要素」にあたり、クラスのメソッドが「できること」にあたります。

Salesforceとクラスの考えを結びつける

Salesforeのオブジェクト自体もこの考え方とつながってくると思いませんか?コードで例を挙げます。

Account acc1 = new Account(Name = '取引先1'); // <- 「取引先1」という名前を持つ取引先の実体(acc1)を作成
Account acc2 = new Account(Name = '取引先2'); // <- 「取引先2」という名前を持つ取引先の実体(acc2)を作成
acc1.Phone = '000-1111-2222';
acc2.Phone = '111-2222-3333';

この時、acc1の電話番号は 000-1111-2222 ですし、acc2の電話番号は 111-2222-3333 になります。独立しています。
Account自体がクラス、電話番号や名前などの項目はクラスの変数です。

このように、Apex内ではSalesforceのオブジェクトもクラスとして扱っていると思うと、Salesforceを触っている人はかなりイメージしやすいのではないでしょうか?

オブジェクト指向を使う理由

オブジェクト指向という言葉もよく聞きますよね。これってなんなんでしょう?
上記で説明したクラスがまさに「オブジェクト」です。
オブジェクトを作成する(インスタンス化する)、オブジェクトを操作する(メソッドを呼び出したり、変数を更新する)ことで、プログラムを組み立てていくのがオブジェクト指向の考え方です。
Apexクラスを実装するとき、newでインスタンス化をし、「インスタンス名.メソッド名()」でメソッドを呼び出したりしているかと思います。
それこそ、オブジェクト指向のプログラミングです。

では、こんな風に組み立てる目的はなんなのでしょう?
もっと言えば、なんでわざわざクラスに分ける必要があるのでしょうか?上から下に順番に処理を記載するだけじゃダメなのでしょうか?

個人的に、オブジェクト指向を使う最大の目的は「変更時の修正コストを下げるため」です。なんなら、設計思想全体がこの目的のためと言っても過言ではないと思っています。
なので、一回きりしか実行しない小規模なソースコードに対してまで、バッチリ設計を考えて実装するのは無駄だとすら思っています。
ただし、一回きりしか実行しないコードでも、規模が大きければ設計をちゃんとした方が実装しやすいというメリットもあるかと思います。なぜなら、規模が大きいものは実装中にほかの箇所を「変更する」可能性が高いからです。
そう考えると、編集されないソースコードというものは多くないはずです。

オブジェクト指向で、なぜ修正コストが下がるのか?

実際に、ソースコードを編集する時をイメージして、どんなソースコードであれば修正に時間がかかるかを考えてみてください。
修正するときは、まず修正する箇所を探すことになります。

例えば、処理が10,000行書いてあるソースコードがあったとして、それがすべて1つのメソッドに記載してあった場合はどうでしょうか?
そのメソッドを上から順に辿っていくことになりませんか?想像しただけでとてもツラいです。。自分が書いたコードでもツラいですが、人のコードであればなおさらです。

これがキレイにクラスに分かれているとどうでしょう?
上で記載した通り、クラスは概念を表現したものなので、同じ概念のものだけが同じクラスにまとまって存在しているのが正しい状態です。
この正しい状態を保っているのであれば、修正箇所に当たりをつけることができます。
Salesforceでも取引先オブジェクトの項目の設定を変えたい時に、商談オブジェクトを見に行きませんよね。

また、修正箇所が1つとは限りません。修正の影響範囲を見誤ると、一部は正しく修正できたけど一部は古い状態が残ってしまうというバグになりかねません。

public class Human {
    public String firstName;
    public String lastName;
    
    public Human(String lastName, String firstName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

// どこかのメソッド
Human human1 = new Human('姓1', '名1');
System.debug(human1.firstName + ' ' + human1.lastName); // フルネームを表示

// また別のどこかのメソッド
Human human2 = new Human('姓2', '名2');
System.debug(human2.firstName + ' ' + human2.lastName); // フルネームを表示

上記は単純なクラスとその使用箇所の例です。Humanクラスには姓(lastName)と名(firstName)があり、メソッドではそれらを結合させたフルネームをデバッグに表示しています。

では、このHumanクラスにミドルネームを追加して、フルネームは「姓+ミドルネーム+名」を使用するように変更したいという依頼があったとします。
上記の例であれば、変更は二箇所だけなので簡単!と思うかもしれませんが…
もしこのシステムが大規模なシステムの場合、すべての「フルネームを表示している部分」を見つけ出せるでしょうか?ちょっと不安ですよね。

ではどうしたらよかったのでしょうか?
フルネームを使いたくなった時点で、フルネームの変数もしくは表示用のgetterをHumanクラスに実装しておけばよかったはずです。

public class Human {
    public String firstName;
    public String lastName;
    public String fullName; // fullName変数を用意した場合
    
    public Human(String lastName, String firstName) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.fullName = this.firstName + ' ' + this.lastName; // ★変更箇所
    }
}

// どこかのメソッド
Human human1 = new Human('姓1', '名1');
System.debug(human1.fullName);

// また別のどこかのメソッド
Human human2 = new Human('姓2', '名2');
System.debug(human2.fullName);

これでフルネームの定義が変わっても、「★変更箇所」を変更するだけでよくなりました。

これはつまり、Humanクラスを呼び出しているメソッド側で、フルネームがどのようなロジックなのかを知っている必要がなくなったということです。
最初の例だと、呼び出すメソッド側にフルネームを求めるロジックが記載されていたため、メソッド側がフルネームのロジックを知ってしまっていました。
一方、訂正後の例では、Humanクラス側にフルネームを求めるロジックが記載されているため、呼び出し先のメソッド側ではフルネームのロジックを知らなくても、フルネームを使うことができるようになりました。

実世界でも、ある情報を知っている人が多ければ多いほど、その情報を訂正して回るのは大変です。それと同じで、ロジックを知っている部分が多いほど、修正は大変になります。
だからこそ、クラスの変数やメソッドを利用する側が詳細なロジックを知らなくてもいい状態を作るのがとても大切です。
このような考え方が「関心の分離」です。
そしてこの”関心が分離された状態”こそ、オブジェクト指向で実現したい状態であると思っています。
f:id:calamel_nuts:20201129141403p:plain

おまけ:「静的」の考え方

ここまでで、次の抽象クラス・インターフェースの違いの説明はできるのですが、せっかくクラスのインスタンス化などをイメージしたので、「静的」な変数やメソッドについてもイメージを言語化しておきます。

ここまで、クラスの説明で使った人間クラスには「人間」の概念に関する情報が詰め込まれています。
今まで説明した変数やメソッドは、独立した実体がそれぞれに持っている情報や、それぞれが実行できる処理の話でした。

では、人間全体に関する情報…例えば、人間クラスで作成された実体の数(=人口)はどこで管理するのでしょうか?
これも「人間」の概念の一つなので、人間クラスで管理したいはずです。
しかし今まで通りの変数で定義してしまうと、インスタンス毎に人口が定義されてしまうので総数が分かりません。

このようにインスタンスに対する情報ではなく、クラス全体に関する情報を管理するのが静的(static)変数です。
インスタンスから、静的変数は呼び出せません。同じように、静的メソッドもインスタンスからは呼び出せません。
静的変数や静的メソッドは、インスタンスに紐づくものではないからです。

おわりに

長くなりましたが、私の理解をざっくりと書き出してみました。

なぜこの記事を書こうと思ったかについて少し触れますと、来月受験するPlatformデベロッパー試験の勉強を行っていたのがきっかけでした。
そこで抽象クラスとインターフェースの違いについて学び直したので言語化してみようと思ったのですが、記事を書いているうちにここは先にイメージを共有しておいた方が伝わりやすいのかなーと思う部分があったので、前準備としてこの記事を切り出してみました。
なので、本題のブログについても近いうちに更新したいと思います!

はじめにも書きましたが、ぜひこのブログを読んだあとで、いろんな方のいろんな説明にも触れてみてください!昔読んで、理解できなかった説明を読み直すのもいいと思います。
理解が進むきっかけになると幸いです!

またギリギリの月二回更新になってますね…
明日は、デベロッパー試験勉強で新しく学んだことを、何か一つまとめられたらと思います!