【Apex】Comparableインターフェースを使ってカスタムソートを行う

# はじめに
この記事では、[Comparableインターフェース](https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_comparable.htm)を使ったカスタムソートの方法を解説します。
「インターフェース？implements？よく分からん！」
と拒否反応をお持ちの方にも読んでもらえるよう、なるべく噛み砕いて説明していきます。

# sortメソッドの基本
まず最初にsortメソッドの基本的な使い方について説明します。（そんなのいらねえ知ってるわって人は飛ばして[こちら](#comparableインターフェースの使い方)へ）
Comparableインターフェースを使用していないリストに対してsortメソッドを使用した際は、一定のルールに従って全て昇順で並び替えられます。

https://developer.salesforce.com/docs/atlas.ja-jp.238.0.apexcode.meta/apexcode/apex_methods_system_list.htm#apex_System_List_sort

## プリミティブ型
特別なルールは無く、シンプルに昇順で並び替えが行われます。

### 1. String型
```apex
List<String> strList = new List<String>();

strList.add('b');
strList.add('c');
strList.add('a');

strList.sort();
System.debug(strList);

// 結果 → (a, b, c)
```

### 2. Integer型
```apex
List<Integer> intList = new List<Integer>();

intList.add(2);
intList.add(3);
intList.add(1);

intList.sort();
System.debug(intList);

// 結果 → (1, 2, 3)
```

### 3. Date型
```apex
List<Date> dateList = new List<Date>();

dateList.add(Date.newInstance(2023, 5, 1));
dateList.add(Date.newInstance(2023, 9, 1));
dateList.add(Date.newInstance(2023, 1, 1));

dateList.sort();
System.debug(dateList);

// 結果 → (2023-01-01 00:00:00, 2023-05-01 00:00:00, 2023-09-01 00:00:00)
```

## SObject型 
SObject型のリストに対してsortメソッドを使用した場合、以下の優先順位で並び替えが行われます。

> 1. sObject 型の表示ラベル。
たとえば、Account sObject は Contact の前になります。
> 2. Name 項目 (該当する場合)。
たとえば、リストに A と B という名前の 2 つの取引先がある場合、取引先 A が取引先 B よりも前になります。
> 3. 標準項目。ID 項目と Name 項目を除き、アルファベット順で最初の項目から使用されます。
たとえば、2 つの取引先が同じ名前の場合、並び替えに使用される最初の標準項目は AccountNumber です。
> 4. カスタム項目。アルファベット順で最初の項目から使用されます。
たとえば、2 つの取引先の名前と標準項目が同じで、FieldA と FieldB という 2 つのカスタム項目がある場合、並び替えでは FieldA の値が最初に使用されます。
> 
>[Apex開発者ガイド：sObject のリストの並び替え](https://developer.salesforce.com/docs/atlas.ja-jp.238.0.apexcode.meta/apexcode/apex_list_sorting_sobject.htm)

### 1. sObject型の表示ラベルによるソート
```apex
List<SObject> sobjectList = new List<SObject>();

sobjectList.add(new Contact());
sobjectList.add(new Opportunity());
sobjectList.add(new Account());

sobjectList.sort();
System.debug(sobjectList);

 // 結果 → (Account:{}, Contact:{}, Opportunity:{})
```

### 2. Name項目によるソート
```apex
List<Account> accountList = new List<Account>();

accountList.add(new Account(Name = 'Bob', AccountNumber = '2', Description = 'b', Age__c = 20));
accountList.add(new Account(Name = 'Charlie', AccountNumber = '1', Description = 'c', Age__c = 18));
accountList.add(new Account(Name = 'Alice', AccountNumber = '3', Description = 'a', Age__c = 19));

accountList.sort();
System.debug(accountList);

// 結果：Name項目が存在するため最優先でで並び替えられる
// Account:{Name=Alice, AccountNumber=3, Description=a, Age__c=19}, 
// Account:{Name=Bob, AccountNumber=2, Description=b, Age__c=20},
// Account:{Name=Charlie, AccountNumber=1, Description=c, Age__c=18}
```

### 3. 標準項目によるソート
```apex
List<Account> accountList = new List<Account>();

accountList.add(new Account(AccountNumber = '2', Description = 'b', Age__c = 20));
accountList.add(new Account(AccountNumber = '1', Description = 'c', Age__c = 18));
accountList.add(new Account(AccountNumber = '3', Description = 'a', Age__c = 19));

accountList.sort();
System.debug(accountList);

// 結果：標準項目の中で最もアルファベット順が若いAccountNumber基準で並び替えられる
// Account:{AccountNumber=1, Description=c, Age__c=18},
// Account:{AccountNumber=2, Description=b, Age__c=20},
// Account:{AccountNumber=3, Description=a, Age__c=19}
```

### 4. カスタム項目によるソート
```apex
List<Account> accountList = new List<Account>();

accountList.add(new Account(Age__c = 20, mail__c = 'aaa@example.jp'));
accountList.add(new Account(Age__c = 18, mail__c = 'bbb@example.jp'));
accountList.add(new Account(Age__c = 19, mail__c = 'ccc@example.jp'));

accountList.sort();
System.debug(accountList);

// 結果：カスタム項目の中で最もアルファベット順が若いAge__cで並び替えられる
// Account:{Age__c=18, mail__c=bbb@example.jp},
// Account:{Age__c=19, mail__c=ccc@example.jp},
// Account:{Age__c=20, mail__c=aaa@example.jp}
```

SObjectがミックスされたリストはさておき、基本的に保持しているプロパティによって並び替え順序が決まります。
つまり、意図した順番で並び替えることはかなり難しいと言えるでしょう。

# Comparableインターフェースの使い方
前置きが長くなりましたが、ここからはComparableインターフェースを実装して任意の並び替えを行う手順を紹介します。

## 基本的な使い方
```apex:ComparableAccount.cls
global with sharing class ComparableAccount implements Comparable {
    Account record;

    global ComparableAccount(Account acc) {
        this.record = acc;
    }

    global Integer compareTo(Object compareAcc) {
        ComparableAccount compareTo = (ComparableAccount)compareAcc;
        if(record.Age__c > compareTo.record.Age__c){
            return 1;
        } else if(record.Age__c < compareTo.record.Age__c){
            return -1;
        }
        return 0;
    }
}
```

```apex
List<Account> accountList = new List<Account>();

accountList.add(new Account(Name = 'Alice',   Age__c = 20));
accountList.add(new Account(Name = 'Ellen',   Age__c = 21));
accountList.add(new Account(Name = 'Dave',    Age__c = 19));
accountList.add(new Account(Name = 'Bob',     Age__c = 21));
accountList.add(new Account(Name = 'Charlie', Age__c = 18));

List<ComparableAccount> comparableAccountList = new List<ComparableAccount>();
for(Account acc : accountList){
    comparableAccountList.add(new ComparableAccount(acc));
}

comparableAccountList.sort();
System.debug(comparableAccountList);

// 結果：Age__cの順番で並び替えられる
// ComparableAccount:[record=Account:{Name=Charlie, Age__c=18}],
// ComparableAccount:[record=Account:{Name=Dave, Age__c=19}],
// ComparableAccount:[record=Account:{Name=Alice, Age__c=20}],
// ComparableAccount:[record=Account:{Name=Ellen, Age__c=21}],
// ComparableAccount:[record=Account:{Name=Bob, Age__c=21}]
```

ここではAge__c項目の昇順で並び替えを行いたいものとします。
インターフェースを使用せず`accountList.sort()`を行った場合は、[Name項目によるソート](#2-name項目によるソート)のルールが適用され、Age__cの順番では並び替えられません。
上記の例では、Comparableインターフェースを実装したComparableAccountクラスを用いて、Age__cの昇順で並び替えを行う処理をcompareToメソッドに実装しています。

## 実装のポイント

### 1. インターフェース実装
クラス名の後ろに`implements Comparable`を付与することにより、このクラスがComparableインターフェースを実装していることを宣言しています。
クラスを継承する時に使う`extends 〇〇`と同じ感覚です。

### 2. compareToメソッド
しかし上記だけではインターフェースを実装したことにはなりません。
もし適当なクラスに`implements Comparable`を付けてデプロイした場合、以下のコンパイルエラーが発生します。
> Class 〇〇 must implement the method: Integer System.Comparable.compareTo(Object)

このエラー内容は、
1. Object型を引数に取り
1. Integer型が返り値の
1. compareToという名前のメソッド

上記全てを満たすメソッドが、Comparableインターフェースを実装したクラスには必須という意味合いになります。
なぜ必須なのか？と言うと、「ある機能が存在することを保証する」というのがインターフェースの目的の1つだからです。
言葉だけでは伝わりにくいと思うので、sort()が実行された際の簡略化した流れ（予想）を以下に示します。

1. List型に対してsort()を実行
1. Comparableインターフェースを実装したクラスか判定
1. 2に該当する場合 → compareTo()のロジックに基づいてソート処理
1. 該当しなかった場合 → [基本のソート処理](#sortメソッドの基本)に基づいてソート処理

「Comparableインターフェースを実装している＝compareToメソッドが存在する」ことが保証されているからこそ、2に該当したとき安全にcompareTo()を呼び出せるという仕組みです。
興味がある方は開発者ドキュメントにも目を通してみてください。

https://developer.salesforce.com/docs/atlas.ja-jp.apexcode.meta/apexcode/apex_classes_interfaces.htm

### 3. 並び替えのロジック
ある値に基づいて昇順で並び替えを行う場合、以下の処理を記述すればOKです（降順の場合は逆）

- 「基準となる値 > 比較対象の値」の場合は"1"を返却 → `if(record.Age__c > compareTo.record.Age__c){return 1})`
- 「基準となる値 < 比較対象の値」の場合は"-1"を返却 → `if(record.Age__c < compareTo.record.Age__c){return -1}`

「リストのインデックスをどちらに動かしたいか」という覚え方で問題ありません。

## 少しだけ応用編
```diff_apex:ComparableAccount.cls
global with sharing class ComparableAccount implements Comparable {
    Account record;

    global ComparableAccount(Account acc) {
        this.record = acc;
    }

    global Integer compareTo(Object compareAcc) {
        ComparableAccount compareTo = (ComparableAccount)compareAcc;
        if(record.Age__c > compareTo.record.Age__c){
            return 1;
        } else if(record.Age__c < compareTo.record.Age__c){
            return -1;
+       } else if(record.Birthdate__c > compareTo.record.Birthdate__c){
+           return 1;
+       } else if(record.Birthdate__c < compareTo.record.Birthdate__c){
+           return -1;
        }
        return 0;
    }
}
```

```apex
List<Account> accountList = new List<Account>();

accountList.add(new Account(Name = 'Alice',   Age__c = 20, Birthdate__c = Date.newInstance(2000, 1, 1)));
accountList.add(new Account(Name = 'Ellen',   Age__c = 21, Birthdate__c = Date.newInstance(2000, 4, 1)));
accountList.add(new Account(Name = 'Dave',    Age__c = 19, Birthdate__c = Date.newInstance(2000, 3, 1)));
accountList.add(new Account(Name = 'Bob',     Age__c = 21, Birthdate__c = Date.newInstance(2000, 2, 1)));
accountList.add(new Account(Name = 'Charlie', Age__c = 18, Birthdate__c = Date.newInstance(2000, 5, 1)));

List<ComparableAccount> comparableAccountList = new List<ComparableAccount>();
for(Account acc : accountList){
    comparableAccountList.add(new ComparableAccount(acc));
}

comparableAccountList.sort();
System.debug(comparableAccountList);

// 結果：①Age__c ②Birthdate__cの順番で並び替えられる
// ComparableAccount:[record=Account:{Name=Charlie, Age__c=18, Birthdate__c=2000-05-01 00:00:00}],
// ComparableAccount:[record=Account:{Name=Dave, Age__c=19, Birthdate__c=2000-03-01 00:00:00}],
// ComparableAccount:[record=Account:{Name=Alice, Age__c=20, Birthdate__c=2000-01-01 00:00:00}],
// ComparableAccount:[record=Account:{Name=Bob, Age__c=21, Birthdate__c=2000-02-01 00:00:00}],
// ComparableAccount:[record=Account:{Name=Ellen, Age__c=21, Birthdate__c=2000-04-01 00:00:00}]
```

上記は最初の例にBirthdate__cでの並び替え基準を追加したもので、Age__cが同じ場合に限り生年月日による昇順ソートが行われます。
最初の例と比較すると、Age__cが同じBobとEllenの順番がBirthdate__cの昇順で入れ替わっています。

# おわりに
一度覚えてしまえば使い方はとても簡単ですね！
しかし「インターフェース」という言葉の圧力が怖くて避けてきた人は多いのではないでしょうか・・・
でもどんな風に使うのか・なぜ便利なのかが理解できるととても楽しいです（）
苦手意識が少しでも薄れて興味が湧いた方は、ぜひぜひ参考記事を読んでインターフェースへの理解を深めてください！
最後は少し本題から逸れてしまいましたが、本記事は以上です！

# 参考記事
- [【C#】インターフェイスの利点が理解できない人は「インターフェイスには３つのタイプがある」ことを理解しよう - Qiita](https://qiita.com/yutorisan/items/d28386f168f2f3ab166d)
- [インターフェースと抽象クラスの使い分け、活用方法 - Qiita](https://qiita.com/igayamaguchi/items/e1d35db0a14a84bda452)