4
Help us understand the problem. What are the problem?

posted at

updated at

Organization

独自コレクションクラスのすゝめ

この記事は『LITALICO Engineers Advent Calendar 2021』の14日目の記事です。
株式会社LITALICOでエンジニアをしている、@tatsuya-fujii-LITALICOと申します。
よろしくお願いいたします。

LITALICOでは定期的にモブプロ方式の勉強会が開かれているのですが、いまいちユーザー定義のコレクションクラスが市民権を得ていないなと感じたので、この場を借りて布教したいと思います。

ちなみに私はコレクションクラス大好き人間です。
その言語でコレクションクラスが作れるかどうかで、言語に対する好感度が露骨に変わります。

コレクションクラスとは

私が勝手に「コレクションクラス」と呼んでいるものは、ザックリと、添字でアクセスできてforeachで回せてメソッドを持っているモノの事です。

各言語によって「Iterable object (反復可能なオブジェクト)」とか「Enumerable object (列挙可能なオブジェクト)」、あるいは「array-like object (配列に似たオブジェクト)」などと呼ばれます。

色々と差異はあると思いますが、本記事内では全部ひっくるめて「コレクションクラス(オブジェクト)」で行きます。

何故、コレクションクラスを作るのか?

結論から言うと、配列に対する手続きを最も自然に表現できる方法だと考えているからです。

例としてJavaScriptでコレクションクラスを使う場面を見てみましょう。
弁当の名前と値段のプロパティを持ったBentoクラスがあったとします。

Bento.js
export default class Bento {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }
}

そして、Bentoオブジェクトの配列から500円以下の物を抜き出す処理を実装してみます。書き下すと以下のようになります。

※JSにはfilterというメソッドが存在しますが、ここでは一般化のために使用しません。

main.js
import Bento from "./Bento.js";

const bentos = [
  new Bento ("海苔弁当", 450),
  new Bento ("ハンバーグ弁当", 550),
  new Bento ("シュウマイ弁当", 530),
  new Bento ("親子丼", 450),
  new Bento ("麻婆豆腐丼", 430),
  new Bento ("かつ丼", 550),
];

const items = [];
for(const bento of bentos) {
  if(bento.price <= 500) {
    items.push(bento);
  }
}
console.log(items);
/*[
  User { name: "海苔弁当", price: 450 },
  User { name: "親子丼", price: 450 },
  User { name: "麻婆豆腐丼", price: 430 }
]*/

この配列から500円以下の物を抜き出す処理は、また使われそうです。
ですので、関数として切り出しておきましょう。

getOneCoinItems関数
function getOneCoinItems(bentos) {
  const items = [];
  for(const bento of bentos) {
    if(bento.price <= 500) {
      items.push(bento);
    }
  }
  return items;
}

さて、このgetOneCoinItems関数ですが、どこに配置するのが良いでしょうか?
他のファイルからも使われるでしょうから、少なくともmain.jsではないでしょう。

では、Bentoクラスと一緒の場所に置く?
悪くはないと思いますが、ちょっと蛇足な感じがします。

単純に考えましょう。getOneCoinItems関数はBento配列がないと利用できません。
つまり、getOneCoinItems関数はBento配列に依存しています。
ならば、getOneCoinItems関数はBento配列に配置しましょう。

Bentos.js
class Bentos extends Array {
  getOneCoinItems() {
    const items = [];
    for(const bento of this) {
      if(bento.price <= 500) {
        items.push(bento);
      }
    }
    return items;
  }
}
main.js
import Bento from "./Bento.js";
import Bentos from "./Bentos.js";

const bentos = new Bentos (
  new Bento ("海苔弁当", 450),
  new Bento ("ハンバーグ弁当", 550),
  new Bento ("シュウマイ弁当", 530),
  new Bento ("親子丼", 450),
  new Bento ("麻婆豆腐丼", 430),
  new Bento ("かつ丼", 550),
);

const oneCoinItems = bentos.getOneCoinItems();

Arrayクラスを継承してコレクションクラスのBentosを作成し、getOneCoinItemsメソッドを持たせました。
どうでしょう、良い感じじゃないですか?

私はオブジェクト指向で一番大事なのはカプセル化だと思っています。
カプセル化とは何かと調べてみると、以下のようにあります。

関連するデータの集合とそれらに対する操作をオブジェクトとして一つの単位にまとめ、外部に対して必要な情報や手続きのみを提供すること。

例で行ったことは、まんまカプセル化ですね。

配列は添字でアクセスできるなど、プログラミング言語の中でちょっと特殊な存在です。
そのため、配列とオブジェクトを分けて考えがちですが、オブジェクト指向的には分けて考える必要はありません。
配列に対する手続きが必要な場合は、配列にメソッドを生やすのがスマートだと思います。

各言語での実装方法

ここからは、各言語におけるコレクションクラスの実装方法を解説します。

各言語と言いつつ、私が使ったことのある言語だけです。
あまり触ってない言語もあるので間違いがあるかもしれません。ご了承ください。
また、解説の量は私の言語に対する思い入れによって増減します。ご了承ください。

JavaScript (ES6以降)

まずはユーザー数が多そうなJSから行きましょう。
ブラウザ上で動作する唯一無二の存在です。

しかしながら、ES5以前のJSではArrayが継承されることを想定しておらず、「array-like object(配列に似たオブジェクト)」は作れても、「本物の配列オブジェクト」は作れませんでした。

しかも、classのないプロトタイプベースのオブジェクト指向だったために私はどうにも馴染めず、JSに対して「君はそういう奴なんだね……」と冷めた目線を送っていました。

しかし、ES6でclass構文が追加され、Arrayクラスも継承できるようになりました!
JS君、君のことを誤解していたよ! 今では友達だよ!

MyCollection.js
class MyCollection extends Array {}

TypeScript

JSで出来るんだから、当然TSでも出来ます。
更にジェネリクス型があるため、配列要素の型も指定できます。
コレクションクラスオタクかつ、データ型信者の私も大満足です。

MyCollection.ts
class MyCollection extends Array<number> {}

Ruby

Rubyは詳しくないんですが、こんな感じ。
「すべてがオブジェクト」というのは伊達じゃないですね、君とはいい友達になれそうだ。

MyCollection.rb
class MyCollection < Array
end

Python

Linuxに標準で入っていたり、プラグインの開発言語だったりして何かとお世話になるPython。
PythonではlistかUserListを継承します。UserListの説明を読むにどちらを使うかはケースバイケースみたいです。

このクラスの必要性は、 list から直接的にサブクラス化できる能力に部分的に取って代わられました; しかし、根底のリストに属性としてアクセスできるので、このクラスを使った方が簡単になることもあります。

MyCollection.py
class MyCollection(list):
    pass

Java

コンパイラ言語界隈の大物、Java御大です。
気のせいかコードから貫禄を感じます。
Javaでは引数ありのコンストラクタは継承されないため、基底クラスのコンストラクタを明示的に呼び出してやる必要があります。

MyCollection.java
import java.util.*;
class MyCollection extends ArrayList<Integer> {
    MyCollection() { super(); }
    MyCollection(Collection<Integer> c) { super(c); }
    MyCollection(int initialCapacity) { super(initialCapacity); }
}

C#

次はC#です。

C#では複数の要素を扱うときはList<T>クラスを使うことが多いのですが、List<T>クラスは処理効率を重視しているため拡張性が低いという理由から、わざわざ拡張用にCollection<T>クラスを用意してくれています。素敵。

Javaと同じく基底クラスのコンストラクタを明示的に呼び出してやる必要があります。
IEnumerableを引数に取るコンストラクタは必須ではありませんが、C#ではIEnumerableを扱う機会が多いため実装しておくと作業が捗ります。

MyCollection.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

class MyCollection: Collection<int> {
  public MyCollection() {}
  public MyCollection(IList<int> list): base(list) {}
  public MyCollection(IEnumerable<int> collection): base(collection.ToArray()) {}
}

本筋とは外れますが、C#にはLINQというSQLっぽい文法でデータを取得できる機能があります。配列はもちろん、xmlやデータベースのデータも扱える優れものです。

IEnumerableインターフェイスを実装していればもれなくLINQを使用できます。Collection<T>はIEnumerableを実装しているので、もちろんその派生クラスでも使用可能です。

MyCollection.cs
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;

class MyCollection: Collection<int> {
  public MyCollection() {}
  public MyCollection(IList<int> list): base(list) {}
  public MyCollection(IEnumerable<int> collection): base(collection.ToList()) {}

  public MyCollection OverFive() {
    var result = 
      from item in this
      where item >= 5
      orderby item descending
      select item;

    return new MyCollection(result);
  }
}

クエリ形式の書き方がどうにも受け付けないという方には、メソッド形式の書き方もあります。
C#はいいぞ! 使おう! (ダイマ)

Go

Go言語だとこんな感じになるのか?
ほぼほぼ触ったことないので、間違ってたらごめんなさい。

Goにはそもそもクラスがないので、配列にメソッドを外付けするような形になります。
ただ、配列に直接メソッドを生やすことはできないようなので、typeを使って新しく配列型(?)を定義する必要があります。

MyCollection.go
type MyCollection []int

func (items MyCollection) Sum() int {
    result := 0
    for _, item := range items {
        result += item
    }
    return result
}

C++

速度が求められる場面で使われるC++。
ここから少し雲行きが怪しくなります。

素直にvectorクラスを継承できればいいのですが、vectorのデストラクタはvirtualではないので、普通に継承してしまうと派生クラスのデストラクタが呼ばれない事があります。

それを回避するために、ここではprivate継承を使います。
private継承をすると基底クラスのメンバーが全てprivateになるため、必要なメンバーはusingでpublicに引き上げてやります。
とりあえず、要素を追加するpush_backメソッド。foreachを利用するためのbeginメソッドとendメソッド。そして、添字アクセスするための[]オペレーターをusingしています。
ちなみにprivate継承は継承と言いつつ、実態は合成(コンポジション)なので注意が必要です。

う~む、コレクションクラスを作っただけですがC++様の深淵が垣間見えますね。くわばらくわばら。

MyCollection.cpp
#include <vector>
using namespace std;
class MyCollection: private vector<int> {
public:
    using vector<int>::push_back;
    using vector<int>::begin;
    using vector<int>::end;
    using vector<int>::operator[];
};

PHP

Web開発者が一度は通るPHPです。
ここに来て、なんと継承できるクラスがなくなってしまいます。

PHPに可変長配列クラスがないのは、標準の配列が柔軟だからなんでしょうか?
Laravelでは独自にCollectionクラスを提供しているので需要はあると思うんですが……

でも大丈夫。 PHPはちゃんと独自コレクションクラス作成のためのインターフェイスを用意してくれています。
コレクションクラスオタクとしては、逆に燃えてきます。

さて、PHPにおいてforeachを利用するには、オブジェクトがTraversableインターフェイスを持っていなくてはいけません。しかし、Traversableは抽象インターフェイスなので単体で実装することはできません。

なのでTraversableを継承しているIteratorインターフェイスを実装します。でもでもイテレーターを実装するのは大変なので、PHPがあらかじめイテレーターを用意してくれています。

そうした定義済みのイテレーターを使用するにはIteratorAggregateインターフェイスを利用します。IteratorAggregateはgetIteratorメソッドのみを持つシンプルなインターフェイスですので、getIteratorメソッドを実装して定義済みのイテレーターオブジェクトを返してやればOKです。

これで完成なら楽なのですが、このままだと添字でのアクセスができません。
添字アクセスをするにはArrayAccessインターフェイスを実装する必要があります。
ArrayAccessインターフェイスには4つメソッドがありますが、配列に対して右から左に処理を流してやるだけなので難しいことはありません。

チャチャっと実装してやれば、コレクションクラスの完成です。

いや~、気が利きそうで利いてないところがPHPさんの愛嬌ですね ヽ(´∀`*)ノ

MyCollection.php
class MyCollection implements IteratorAggregate, ArrayAccess
{
    protected $items = array();

    function __construct(array $items)
    {
        $this->items = $items;
    }

    function getIterator()
    {
        return new ArrayIterator($this->items);
    }

    public function offsetExists($offset)
    {
        return isset($this->items[$offset]);
    }

    public function offsetGet($offset)
    {
        return $this->items[$offset] ?? null;
    }

    public function offsetSet($offset, $value)
    {
        if (is_null($offset)) {
            $this->items[] = $value;
        } else {
            $this->items[$offset] = $value;
        }
    }

    public function offsetUnset($offset)
    {
        unset($this->items[$offset]);
    }
}

VBA

VBAです。 Excelの自動化でお世話になりますよね。
えっ? Excelの自動化ならPythonがあるだろって? そもそもExcelが古い?
……動かせる言語はVBAだけ、なんて環境もあるし (´;ω;`)
 
VBAは実はかなりコレクションオブジェクトが使われている言語だったりします。
Workbooksオブジェクトとか、SheetsオブジェクトとかはFor Eachで回せます。

……なのに、VBAではまっとうな手段でコレクションクラスを作成する方法がありません。
でも、絶対にコレクションクラスは使用したいので、まっとうでない手段を紹介します。

まずは以下のようなクラスモジュールを作成します。

MyCollectionクラスモジュール
Dim Items As Collection

Private Sub Class_Initialize()
    Set Items = New Collection
End Sub

Public Sub Add(Item, Optional Key, Optional Before, Optional After)
    Call Items.Add( Item, Key , Before, After)
End Sub

Public Function Item(Index)
    Item = Items.Item(Index)
End Function

Public Function NewEnum() As IEnumVARIANT
    Set NewEnum = Items.[_NewEnum]
End Function

NewEnumメソッドが、PHPで言う所のgetIteratorメソッドになります。
PHPでは定義済みイテレーターが用意されていましたが、VBAにはそんなものないので組み込みのクラスであるCollectionオブジェクトから拝借します。
これでイテレーターの用意はできましたが、この段階ではまだforeachを利用できません。このクラスをコレクションクラスとして認識させるには、NewEnumメソッドがイテレーターを返すメソッドですよ~、という属性を設定する必要があります。

しかし、属性はVBエディタ上からは設定できません。 ので、おもむろにクラスモジュールをエクスポートします。
エクスポートされたテキストファイルを開くとVBエディタ上では見えない属性などが見えます。

MyCollection.cls
VERSION 1.0 CLASS
BEGIN
  MultiUse = -1  'True
END
Attribute VB_Name = "MyCollection"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = False
Attribute VB_Exposed = False
Option Explicit

Dim Items As Collection

Private Sub Class_Initialize()
    Set Items = New Collection
End Sub

Public Sub Add(Item, Optional Key, Optional Before, Optional After)
    Call Items.Add(Item, Key, Before, After)
End Sub

Public Function Item(Index)
    Item = Items.Item(Index)
End Function

Public Function NewEnum() As IEnumVARIANT
    Set NewEnum = Items.[_NewEnum]
End Function

VBAは基本VB6.0なのでVB6.0の作法に則って、NewEnumメソッドにイテレーターの属性設定を行います。
また、添字でアクセスできるようにItemメソッドに既定のプロパティの属性設定を行います。

MyCollection.cls(一部)
Public Function Item(Index)
Attribute Item.VB_UserMemId = 0
    Item = Items.Item(Index)
End Function

Public Function NewEnum() As IEnumVARIANT
Attribute NewEnum.VB_UserMemId = -4
    Set NewEnum = Items.[_NewEnum]
End Function

保存したテキストファイルをVBエディタからインポートしなおせば、コレクションクラスの完成です!!

ふぅ、VBA。
君はやればできる子だって信じてたよ ( ´꒳` )

おわりに

さて、長々と書き連ねてしまいましたが、いかがだったでしょうか?
配列を引数に取る関数を書く際、「そういえば、コレクションクラスなんてものがあったな」と思い出していただければ幸いです。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
4
Help us understand the problem. What are the problem?