DESIGN-PATTERNS SOFTWARE-ARCHITECTURE BACKEND-DEVELOPMENT CLEAN-CODE TESTABILITY DATA-ACCESS DATABASE ORM NODEJS TYPESCRIPT SOFTWARE-DESIGN MAINTAINABILITY ABSTRACTION DEPENDENCY-INJECTION

The Repository Pattern: Membangun Lapisan Abstraksi Data yang Bersih dan Mudah Diuji

⏱️ 12 menit baca
👨‍💻

The Repository Pattern: Membangun Lapisan Abstraksi Data yang Bersih dan Mudah Diuji

Sebagai developer web, kita setiap hari berinteraksi dengan data. Entah itu menyimpan data pengguna ke database, mengambil daftar produk, atau memperbarui status pesanan. Interaksi ini adalah jantung banyak aplikasi, dan cara kita mengelolanya sangat memengaruhi kualitas kode, fleksibilitas, dan kemudahan pengujian aplikasi kita.

Seringkali, terutama di proyek-proyek awal, kita mungkin menulis kode akses data langsung di dalam logika bisnis atau service layer kita. Misalnya, memanggil db.query() atau User.findById() secara langsung di dalam fungsi yang seharusnya hanya fokus pada aturan bisnis. Ini memang cepat di awal, tapi bisa jadi bumerang seiring proyek bertumbuh.

Di sinilah Repository Pattern hadir sebagai penyelamat! Pola desain ini membantu kita membangun lapisan abstraksi yang bersih antara logika bisnis dan mekanisme penyimpanan data. Mari kita selami lebih dalam kenapa pola ini penting dan bagaimana menerapkannya di aplikasi web modern Anda.

1. Pendahuluan: Kenapa Repositori Itu Penting?

Bayangkan Anda sedang membangun sebuah toko online. Ada banyak bagian yang perlu diurus: menampilkan produk, mengelola keranjang belanja, memproses pembayaran, dan lain-lain. Setiap bagian ini membutuhkan data — daftar produk, detail pelanggan, riwayat transaksi.

Jika setiap kali Anda butuh data produk, Anda langsung menulis kode SQL (SELECT * FROM products WHERE id = ...) atau memanggil ORM (Object-Relational Mapping) secara langsung di mana-mana, apa yang terjadi jika:

Pasti pusing, kan? Kode Anda akan jadi sangat terikat (tightly coupled) dengan detail implementasi database. Perubahan kecil di database bisa memicu perubahan besar di banyak tempat pada aplikasi Anda.

📌 The Repository Pattern mengatasi masalah ini dengan menciptakan sebuah “penjaga gerbang” atau “perpustakaan data” yang khusus menangani semua operasi terkait data untuk entitas tertentu. Logika bisnis Anda tidak perlu tahu bagaimana atau di mana data itu disimpan; mereka hanya perlu “meminta” data kepada repositori, dan repositori yang akan mengurus sisanya.

2. Memahami Repository Pattern: Sang Penjaga Gerbang Data

Secara sederhana, Repository Pattern adalah pola desain yang mengabstraksi lapisan data. Ia menyediakan sebuah antarmuka (interface) atau kelas abstrak yang mendefinisikan operasi-operasi yang bisa dilakukan terhadap kumpulan entitas tertentu (misalnya, User, Product, Order).

Tujuan utamanya:

Analogi yang bagus adalah seperti seorang pelayan (repository) di restoran. Anda (logika bisnis) tidak perlu tahu bagaimana koki (database) memasak makanan. Anda hanya memberi tahu pelayan apa yang Anda inginkan (“Saya mau data produk dengan ID 123”), dan pelayan akan mengurus komunikasi dengan koki dan membawakan pesanan Anda. Jika koki diganti, atau cara memasak berubah, Anda sebagai pelanggan tidak perlu tahu atau peduli, selama pelayan tetap bisa membawakan pesanan Anda.

3. Manfaat Menggunakan Repository Pattern

Mari kita jabarkan manfaat konkretnya:

Isolasi dan Dekopling (Decoupling): Logika bisnis Anda tidak lagi terikat langsung dengan detail database. Ini berarti perubahan pada skema database, jenis database, atau bahkan teknologi ORM tidak akan memengaruhi service layer atau domain layer Anda. Ini sangat penting untuk sistem yang skalabel dan mudah beradaptasi.

Kemudahan Pengujian (Testability): Ini adalah salah satu manfaat terbesar! Saat Anda menguji logika bisnis, Anda tidak ingin tes Anda bergantung pada koneksi database sungguhan yang lambat atau tidak stabil. Dengan repositori, Anda bisa membuat implementasi repositori “palsu” (misalnya, InMemoryUserRepository) yang menyimpan data di memori untuk keperluan tes. Ini membuat tes unit Anda berjalan sangat cepat dan konsisten.

Fleksibilitas Database: Pernahkah Anda bekerja di proyek yang tiba-tiba harus beralih dari satu database ke database lain? Tanpa Repository Pattern, ini bisa jadi mimpi buruk. Dengan pola ini, Anda hanya perlu menulis ulang implementasi repositori untuk database baru, sementara sisa aplikasi Anda tetap tidak berubah.

Kode yang Lebih Bersih dan Terorganisir: Semua logika akses data untuk entitas tertentu terkumpul di satu tempat. Ini membuat kode lebih mudah dibaca, dimengerti, dan dirawat. Anda tahu persis di mana harus mencari atau mengubah cara data diambil atau disimpan.

Konsistensi dalam Akses Data: Repositori dapat memastikan bahwa semua operasi data untuk entitas tertentu mengikuti aturan dan validasi yang sama, mencegah inkonsistensi.

4. Implementasi Sederhana Repository Pattern

Mari kita lihat contoh sederhana menggunakan TypeScript (konsepnya sama untuk bahasa lain seperti JavaScript, Java, C#, Go, dll.). Kita akan membuat repositori untuk entitas User.

// 1. Definisikan Entitas (atau Model)
interface User {
  id: string;
  name: string;
  email: string;
  createdAt: Date;
  updatedAt: Date;
}

// 2. Definisikan Antarmuka Repositori (Kontrak)
// Ini adalah "kontrak" yang harus dipatuhi oleh setiap implementasi repositori.
interface IUserRepository {
  findById(id: string): Promise<User | null>;
  findByEmail(email: string): Promise<User | null>;
  save(user: User): Promise<User>; // Bisa untuk create atau update
  delete(id: string): Promise<void>;
  findAll(): Promise<User[]>;
}

// 3. Implementasi Repositori untuk Database (misal, dengan ORM/SQL)
// Ini adalah implementasi konkret yang berinteraksi dengan database sungguhan.
class DatabaseUserRepository implements IUserRepository {
  // Dalam proyek nyata, ini akan menerima koneksi database atau instance ORM
  // constructor(private dbClient: any) {} // Contoh: PrismaClient, Knex, dll.

  async findById(id: string): Promise<User | null> {
    console.log(`[DB] Mencari user dengan ID: ${id}`);
    // Logika untuk mengambil data dari database
    // return await this.dbClient.user.findUnique({ where: { id } });
    return { id, name: "Budi Database", email: "budi@example.com", createdAt: new Date(), updatedAt: new Date() }; // Contoh dummy
  }

  async findByEmail(email: string): Promise<User | null> {
    console.log(`[DB] Mencari user dengan email: ${email}`);
    // Logika untuk mengambil data dari database
    // return await this.dbClient.user.findUnique({ where: { email } });
    return { id: "user-456", name: "Ani Database", email: "ani@example.com", createdAt: new Date(), updatedAt: new Date() }; // Contoh dummy
  }

  async save(user: User): Promise<User> {
    console.log(`[DB] Menyimpan user: ${user.name}`);
    // Logika untuk menyimpan/memperbarui data di database
    // return await this.dbClient.user.upsert({
    //   where: { id: user.id || undefined },
    //   update: user,
    //   create: { ...user, id: user.id || 'new-id-' + Math.random().toString(36).substring(7) }
    // });
    user.updatedAt = new Date();
    if (!user.id) user.id = 'new-id-' + Math.random().toString(36).substring(7); // Dummy ID
    return { ...user }; // Contoh dummy
  }

  async delete(id: string): Promise<void> {
    console.log(`[DB] Menghapus user dengan ID: ${id}`);
    // Logika untuk menghapus data dari database
    // await this.dbClient.user.delete({ where: { id } });
  }

  async findAll(): Promise<User[]> {
    console.log(`[DB] Mengambil semua user`);
    // Logika untuk mengambil semua data dari database
    // return await this.dbClient.user.findMany();
    return [
      { id: "user-1", name: "Budi Database", email: "budi@example.com", createdAt: new Date(), updatedAt: new Date() },
      { id: "user-2", name: "Ani Database", email: "ani@example.com", createdAt: new Date(), updatedAt: new Date() },
    ]; // Contoh dummy
  }
}

// 4. Implementasi Repositori untuk Pengujian (In-Memory)
// Ini adalah implementasi "palsu" yang bisa digunakan di test, tidak menyentuh database asli.
class InMemoryUserRepository implements IUserRepository {
  private users: User[] = [];

  constructor(initialUsers: User[] = []) {
    this.users = initialUsers;
  }

  async findById(id: string): Promise<User | null> {
    console.log(`[In-Memory] Mencari user dengan ID: ${id}`);
    return this.users.find(u => u.id === id) || null;
  }

  async findByEmail(email: string): Promise<User | null> {
    console.log(`[In-Memory] Mencari user dengan email: ${email}`);
    return this.users.find(u => u.email === email) || null;
  }

  async save(user: User): Promise<User> {
    console.log(`[In-Memory] Menyimpan user: ${user.name}`);
    const existingIndex = this.users.findIndex(u => u.id === user.id);
    user.updatedAt = new Date();
    if (existingIndex !== -1) {
      this.users[existingIndex] = { ...this.users[existingIndex], ...user };
    } else {
      if (!user.id) user.id = 'mem-id-' + Math.random().toString(36).substring(7);
      this.users.push(user);
    }
    return { ...user };
  }

  async delete(id: string): Promise<void> {
    console.log(`[In-Memory] Menghapus user dengan ID: ${id}`);
    this.users = this.users.filter(u => u.id !== id);
  }

  async findAll(): Promise<User[]> {
    console.log(`[In-Memory] Mengambil semua user`);
    return [...this.users];
  }
}

// 5. Penggunaan Repositori di Service Layer (Logika Bisnis)
// Service layer tidak peduli implementasi repositori yang mana yang digunakan.
class UserService {
  constructor(private userRepository: IUserRepository) {}

  async registerUser(name: string, email: string): Promise<User> {
    const existingUser = await this.userRepository.findByEmail(email);
    if (existingUser) {
      throw new Error("Email sudah terdaftar.");
    }

    const newUser: User = {
      id: "temp-id-" + Math.random().toString(36).substring(7), // ID akan di-generate di repositori atau database
      name,
      email,
      createdAt: new Date(),
      updatedAt: new Date(),
    };
    return this.userRepository.save(newUser);
  }

  async getUserProfile(id: string): Promise<User | null> {
    return this.userRepository.findById(id);
  }

  async getAllUsers(): Promise<User[]> {
    return this.userRepository.findAll();
  }
}

// Contoh Penggunaan:
// Untuk produksi atau pengembangan normal
const databaseRepo = new DatabaseUserRepository();
const userServiceProd = new UserService(databaseRepo);

console.log("\n--- PRODUKSI ---");
userServiceProd.registerUser("Produk User", "prod@example.com")
  .then(user => console.log("User terdaftar (Prod):", user.name))
  .catch(error => console.error("Error (Prod):", error.message));

userServiceProd.getUserProfile("user-1")
  .then(user => console.log("Profil User (Prod):", user?.name));


// Untuk pengujian (menggunakan in-memory repository)
const initialUsersForTest: User[] = [
  { id: "test-1", name: "Test User 1", email: "test1@example.com", createdAt: new Date(), updatedAt: new Date() },
];
const inMemoryRepo = new InMemoryUserRepository(initialUsersForTest);
const userServiceTest = new UserService(inMemoryRepo);

console.log("\n--- PENGUJIAN ---");
userServiceTest.registerUser("Test User 2", "test2@example.com")
  .then(user => console.log("User terdaftar (Test):", user.name))
  .catch(error => console.error("Error (Test):", error.message));

userServiceTest.getUserProfile("test-1")
  .then(user => console.log("Profil User (Test):", user?.name));

userServiceTest.getAllUsers()
  .then(users => console.log("Semua User (Test):", users.map(u => u.name)));

💡 Tips: Perhatikan bagaimana UserService hanya berinteraksi dengan IUserRepository, bukan DatabaseUserRepository atau InMemoryUserRepository secara langsung. Inilah inti dari abstraksi!

5. Repository Pattern dalam Konteks Aplikasi Modern

Dalam aplikasi modern, Repository Pattern sering diintegrasikan dengan konsep lain untuk memaksimalkan manfaatnya:

6. Kapan Menggunakan (dan Kapan Tidak Menggunakan) Repository Pattern

Seperti setiap pola desain, Repository Pattern memiliki tempatnya dan bukan merupakan solusi universal.

🎯 Gunakan saat:

⚠️ Jangan gunakan saat:

Anti-Pattern:

Kesimpulan

Repository Pattern adalah alat yang sangat ampuh di gudang senjata setiap developer. Dengan menerapkan pola ini, Anda tidak hanya membuat kode Anda lebih bersih dan teratur, tetapi juga membangun fondasi yang kuat untuk aplikasi yang lebih tangguh, fleksibel, dan mudah diuji.

Meskipun mungkin terasa seperti menambahkan sedikit kompleksitas di awal, investasi waktu ini akan terbayar lunas dalam jangka panjang, terutama saat aplikasi Anda berkembang dan kebutuhan bisnis berubah. Mulailah berlatih menggunakannya, dan rasakan perbedaannya!

🔗 Baca Juga