🙆

[tips]ジェネリクスを使おう

2024/05/23に公開

こんにちは、ゆきおです。
今日はメモがてら、ちょっとしたことを書いていこうと思います。

Genericsとは

ジェネリクスとは汎用的なアルゴリズムやデータ構造の型を抽象化し、特定の型に依存しない再利用性の高い実装を表現するのに使用されます。
ジェネリクスによってコードの重複を減らし、また型の安全性や整合性を高めます。

例えば、「型が違うだけで実装が同じ関数」があった場合に役立ちます。
簡単な例ですが、Swiftや他の言語で書いていこうと思います。
・ユーザー情報を取得したい
・リポジトリ情報を取得したい
こんなケースを想定します。

サンプル実装(Swift)

ユーザー情報の取得とリポジトリ情報の取得APIを実装しよう!となった時

func fetchUser(from url: URL, completion: @escaping (Result<User, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        
        guard let data = data else {
            completion(.failure(NSError(domain: "Invalid data", code: 0, userInfo: nil)))
            return
        }
        
        do {
            let decoder = JSONDecoder()
            let user = try decoder.decode(User.self, from: data)
            completion(.success(user))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

func fetchRepository(from url: URL, completion: @escaping (Result<Repository, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        
        guard let data = data else {
            completion(.failure(NSError(domain: "Invalid data", code: 0, userInfo: nil)))
            return
        }
        
        do {
            let decoder = JSONDecoder()
            let repository = try decoder.decode(Repository.self, from: data)
            completion(.success(repository))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

こんな実装をしたとします。
まあこれでも呼び出すことは可能なんですが、UserなのかRepositoryなのかの違いで実装に関しては全く一緒です。
もし他に取得したい情報が増えたら、fetch〇〇メソッドがどんどん増えてとても長ったらしいですね。

こんな時にジェネリクスを使用して、fetchという1つのメソッドにまとめます。

func fetch<T: Decodable>(from url: URL, completion: @escaping (Result<T, Error>) -> Void) {
    URLSession.shared.dataTask(with: url) { data, _, error in
        if let error = error {
            completion(.failure(error))
            return
        }
        
        guard let data = data else {
            completion(.failure(NSError(domain: "Invalid data", code: 0, userInfo: nil)))
            return
        }
        
        do {
            let decoder = JSONDecoder()
            let object = try decoder.decode(T.self, from: data)
            completion(.success(object))
        } catch {
            completion(.failure(error))
        }
    }.resume()
}

特に型を持たない、何やら「T」と書かれたAPIの実装です。
この「T」というのがジェネリクスで、Type(型)のTで書くのが一般的です。
この実装の場合、「Decodableな何かしらの型が入る」とだけ定めています。

いざユーザー情報やリポジトリ情報を取得したいとなったら以下のように実装します。

// 使用例
fetch(from: userURL) { (result: Result<User, Error>) in
    switch result {
    case .success(let user):
        print("User: \(user)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

fetch(from: repositoryURL) { (result: Result<Repository, Error>) in
    switch result {
    case .success(let repository):
        print("Repository: \(repository)")
    case .failure(let error):
        print("Error: \(error)")
    }
}

なんだかめちゃくちゃスッキリしましたね。
細かい実装はfetch()内で実装しているので、呼び出す際は必要な引数やTに当たるDecodableなUserとかRepositoryを渡すだけです。
もし新たに別の情報を取得したいとなったら、使用例に沿って追加するだけです。
このようにコードの重複を減らしたり、型の整合性を保証したりしてくれます。

上記の例はSwiftですが、もちろん他の言語でもサポートされていますのでいくつか紹介します。

TypeScript

Before

async function fetchUser(url: string): Promise<User> {
    const response = await fetch(url);
    const data = await response.json();
    return data as User;
}

async function fetchRepository(url: string): Promise<Repository> {
    const response = await fetch(url);
    const data = await response.json();
    return data as Repository;
}

After

async function fetch<T>(url: string): Promise<T> {
    const response = await fetch(url);
    const data = await response.json();
    return data as T;
}

// 使用例
const user = await fetch<User>(userUrl);
const repository = await fetch<Repository>(repositoryUrl);

C#

Before

async Task<User> FetchUser(string url)
{
    using var client = new HttpClient();
    var response = await client.GetAsync(url);
    var data = await response.Content.ReadAsStringAsync();
    return JsonConvert.DeserializeObject<User>(data);
}

async Task<Repository> FetchRepository(string url)
{
    using var client = new HttpClient();
    var response = await client.GetAsync(url);
    var data = await response.Content.ReadAsStringAsync();
    return JsonConvert.DeserializeObject<Repository>(data);
}

After

async Task<T> Fetch<T>(string url)
{
    using var client = new HttpClient();
    var response = await client.GetAsync(url);
    var data = await response.Content.ReadAsStringAsync();
    return JsonConvert.DeserializeObject<T>(data);
}

// 使用例
var user = await Fetch<User>(userUrl);
var repository = await Fetch<Repository>(repositoryUrl);

終わり

以上、簡単ではありますがジェネリクスの紹介でした。
何だか実装がダブってて長ったらしいなあとか思った時に、綺麗なコードを書くときやリファクタのコツとして覚えておきましょう。

Discussion