Wednesday

18-06-2025 Vol 19

Code Smell 301 – Database as Parameter

Code Smell 301: Database as Parameter – Mengungkap Bahaya dan Solusinya

Dalam dunia pengembangan perangkat lunak, kita seringkali mencari cara untuk meningkatkan efisiensi dan fleksibilitas kode kita. Namun, terkadang, upaya untuk mencapai tujuan tersebut justru dapat menghasilkan praktik yang buruk, yang dikenal sebagai “code smell”. Salah satu code smell yang kurang dikenal tetapi berpotensi merusak adalah “Database as Parameter”. Artikel ini akan membahas secara mendalam tentang code smell ini, mengapa hal itu berbahaya, dan bagaimana cara menghindarinya dengan solusi yang efektif.

Daftar Isi

  1. Pendahuluan: Apa itu Code Smell?
  2. Definisi: Database as Parameter – Menjelaskan Masalahnya
  3. Mengapa Database as Parameter Adalah Code Smell?
    1. Ketergantungan yang Kuat
    2. Masalah Keamanan
    3. Skalabilitas yang Terbatas
    4. Sulitnya Pengujian
    5. Abstraksi yang Bocor (Leaky Abstraction)
    6. Masalah Pemeliharaan
  4. Contoh Nyata Database as Parameter
    1. Contoh Sederhana: Fungsi Pencarian Pengguna
    2. Contoh Kompleks: Proses Pembayaran
  5. Solusi: Cara Menghindari Database as Parameter
    1. Repository Pattern
    2. Data Access Object (DAO) Pattern
    3. Query Object Pattern
    4. Object-Relational Mapping (ORM)
    5. Dependency Injection
  6. Studi Kasus: Refactoring Database as Parameter dalam Proyek Riil
  7. Praktik Terbaik Tambahan untuk Desain Database yang Bersih
  8. Kesimpulan: Mencegah Database as Parameter untuk Kode yang Lebih Baik

1. Pendahuluan: Apa itu Code Smell?

Code smell adalah indikator dalam kode sumber program yang mungkin menandakan masalah yang lebih dalam. Meskipun code smell bukan bug secara langsung yang akan menyebabkan program crash, mereka menunjukkan kelemahan dalam desain yang dapat memperlambat pengembangan, meningkatkan risiko bug, dan membuat kode lebih sulit dipahami dan dipelihara. Mengidentifikasi dan mengatasi code smell adalah bagian penting dari praktik pengembangan perangkat lunak yang baik.

2. Definisi: Database as Parameter – Menjelaskan Masalahnya

Database as Parameter adalah code smell yang terjadi ketika koneksi database, atau objek yang mewakili koneksi database (seperti objek Connection atau DbContext), diteruskan secara langsung sebagai parameter ke fungsi atau metode. Alih-alih meneruskan data yang dibutuhkan oleh fungsi, fungsi tersebut diberikan akses langsung ke seluruh database.

3. Mengapa Database as Parameter Adalah Code Smell?

Meneruskan database sebagai parameter memiliki sejumlah konsekuensi negatif, yang menjadikannya sebagai code smell yang harus dihindari:

  1. Ketergantungan yang Kuat:

    Fungsi atau metode yang menerima koneksi database sebagai parameter menjadi sangat terikat pada implementasi database tertentu. Perubahan pada skema database, teknologi database, atau konfigurasi koneksi dapat memaksa perubahan di banyak tempat dalam kode.

  2. Masalah Keamanan:

    Dengan memberikan akses langsung ke database, fungsi berpotensi dapat melakukan operasi apa pun pada database, termasuk operasi yang tidak dimaksudkan atau yang tidak diotorisasi. Hal ini meningkatkan risiko kebocoran data, kerusakan data, atau serangan SQL injection.

  3. Skalabilitas yang Terbatas:

    Mengelola koneksi database secara langsung dalam fungsi dapat menyebabkan masalah skalabilitas. Jika banyak fungsi secara bersamaan mencoba mengakses database, sumber daya koneksi dapat habis, menyebabkan performa yang buruk atau bahkan kegagalan aplikasi.

  4. Sulitnya Pengujian:

    Menguji fungsi yang bergantung pada koneksi database langsung menjadi lebih sulit. Anda perlu membuat dan mengelola database uji, memastikan bahwa data uji konsisten, dan membersihkan data uji setelah setiap pengujian. Hal ini dapat membuat proses pengujian menjadi lambat dan rumit.

  5. Abstraksi yang Bocor (Leaky Abstraction):

    Database seharusnya menjadi detail implementasi. Meneruskan koneksi database ke fungsi mengekspos detail implementasi ini ke lapisan yang lebih tinggi dalam aplikasi. Hal ini melanggar prinsip abstraksi dan membuat kode lebih sulit untuk diubah dan dipelihara.

  6. Masalah Pemeliharaan:

    Kode yang menggunakan Database as Parameter cenderung lebih sulit untuk dipahami, diubah, dan dipelihara. Menelusuri jejak panggilan fungsi dan memahami bagaimana data diakses dan dimanipulasi menjadi lebih sulit, meningkatkan risiko bug dan memperlambat proses pemeliharaan.

4. Contoh Nyata Database as Parameter

Untuk memahami dampak dari Database as Parameter dengan lebih baik, mari kita lihat beberapa contoh:

  1. Contoh Sederhana: Fungsi Pencarian Pengguna

    Bayangkan sebuah fungsi yang mencari pengguna berdasarkan ID di database:

    Contoh (Bahasa C#):

    
    public User GetUserById(SqlConnection connection, int userId)
    {
      using (SqlCommand command = new SqlCommand("SELECT * FROM Users WHERE Id = @UserId", connection))
      {
        command.Parameters.AddWithValue("@UserId", userId);
        using (SqlDataReader reader = command.ExecuteReader())
        {
          if (reader.Read())
          {
            return new User
            {
              Id = (int)reader["Id"],
              Username = (string)reader["Username"],
              Email = (string)reader["Email"]
            };
          }
          else
          {
            return null;
          }
        }
      }
    }
          

    Dalam contoh ini, fungsi GetUserById menerima objek SqlConnection sebagai parameter. Ini berarti bahwa setiap kali fungsi ini dipanggil, pemanggil harus membuat dan mengelola koneksi database. Ini menciptakan ketergantungan yang kuat dan mempersulit pengujian.

  2. Contoh Kompleks: Proses Pembayaran

    Pertimbangkan proses pembayaran yang melibatkan beberapa langkah, seperti memvalidasi informasi kartu kredit, memproses pembayaran, dan memperbarui inventaris. Jika koneksi database diteruskan sebagai parameter ke setiap langkah, hal itu dapat menyebabkan kode yang sangat kompleks dan sulit dipelihara.

    Contoh (Deskripsi Konseptual):

    
    public void ProcessPayment(SqlConnection connection, PaymentDetails paymentDetails)
    {
      ValidateCreditCard(connection, paymentDetails.CreditCardNumber, paymentDetails.ExpiryDate);
      ChargeCreditCard(connection, paymentDetails.CreditCardNumber, paymentDetails.Amount);
      UpdateInventory(connection, paymentDetails.ProductId, paymentDetails.Quantity);
      SendConfirmationEmail(connection, paymentDetails.Email);
    }
          

    Dalam contoh ini, fungsi ProcessPayment menerima koneksi database dan kemudian meneruskannya ke beberapa fungsi lain. Setiap fungsi memiliki akses langsung ke database, meningkatkan risiko dan mempersulit untuk melacak bagaimana data dimanipulasi.

5. Solusi: Cara Menghindari Database as Parameter

Untungnya, ada beberapa pola desain dan praktik terbaik yang dapat Anda gunakan untuk menghindari Database as Parameter dan membuat kode yang lebih bersih, lebih mudah dipelihara, dan lebih aman:

  1. Repository Pattern:

    Pola Repository menyediakan lapisan abstraksi antara domain aplikasi Anda dan lapisan akses data. Repository bertanggung jawab untuk menyediakan antarmuka untuk mengakses dan memanipulasi data, tanpa mengekspos detail implementasi database ke domain aplikasi. Dengan kata lain, Anda membuat kelas khusus (Repository) yang tugasnya adalah berinteraksi dengan database. Kelas ini menyembunyikan kompleksitas kueri SQL dan detail koneksi database.

    Contoh (Bahasa C#):

    
    public interface IUserRepository
    {
      User GetUserById(int userId);
      void AddUser(User user);
      void UpdateUser(User user);
      void DeleteUser(int userId);
    }
    
    public class UserRepository : IUserRepository
    {
      private readonly string _connectionString;
    
      public UserRepository(string connectionString)
      {
        _connectionString = connectionString;
      }
    
      public User GetUserById(int userId)
      {
        using (SqlConnection connection = new SqlConnection(_connectionString))
        {
          connection.Open();
          using (SqlCommand command = new SqlCommand("SELECT * FROM Users WHERE Id = @UserId", connection))
          {
            command.Parameters.AddWithValue("@UserId", userId);
            using (SqlDataReader reader = command.ExecuteReader())
            {
              if (reader.Read())
              {
                return new User
                {
                  Id = (int)reader["Id"],
                  Username = (string)reader["Username"],
                  Email = (string)reader["Email"]
                };
              }
              else
              {
                return null;
              }
            }
          }
        }
      }
    
      // Implementasi metode lain (AddUser, UpdateUser, DeleteUser)
    }
    
    // Penggunaan di lapisan aplikasi:
    public class UserService
    {
      private readonly IUserRepository _userRepository;
    
      public UserService(IUserRepository userRepository)
      {
        _userRepository = userRepository;
      }
    
      public User GetUser(int userId)
      {
        return _userRepository.GetUserById(userId);
      }
    }
          

    Dalam contoh ini, UserService berinteraksi dengan IUserRepository untuk mengakses data pengguna. UserRepository bertanggung jawab untuk mengelola koneksi database dan melakukan kueri. UserService tidak perlu tahu tentang detail implementasi database.

  2. Data Access Object (DAO) Pattern:

    Pola DAO mirip dengan pola Repository, tetapi lebih fokus pada pemisahan logika akses data dari logika bisnis. DAO menyediakan antarmuka untuk mengakses data tertentu, seperti tabel atau tampilan database. Perbedaan utama antara DAO dan Repository adalah bahwa DAO biasanya lebih dekat dengan struktur database yang mendasarinya.

    Contoh (Bahasa Java):

    
    public interface UserDAO {
      User getUserById(int userId);
      void addUser(User user);
      void updateUser(User user);
      void deleteUser(int userId);
    }
    
    public class UserDAOImpl implements UserDAO {
      private final DataSource dataSource;
    
      public UserDAOImpl(DataSource dataSource) {
        this.dataSource = dataSource;
      }
    
      @Override
      public User getUserById(int userId) {
        String sql = "SELECT * FROM Users WHERE Id = ?";
        try (Connection connection = dataSource.getConnection();
             PreparedStatement preparedStatement = connection.prepareStatement(sql)) {
          preparedStatement.setInt(1, userId);
          try (ResultSet resultSet = preparedStatement.executeQuery()) {
            if (resultSet.next()) {
              return new User(
                resultSet.getInt("Id"),
                resultSet.getString("Username"),
                resultSet.getString("Email")
              );
            } else {
              return null;
            }
          }
        } catch (SQLException e) {
          throw new RuntimeException(e); // Tangani exception dengan tepat
        }
      }
    
      // Implementasi metode lain (addUser, updateUser, deleteUser)
    }
    
    // Penggunaan di lapisan aplikasi:
    public class UserService {
      private final UserDAO userDAO;
    
      public UserService(UserDAO userDAO) {
        this.userDAO = userDAO;
      }
    
      public User getUser(int userId) {
        return userDAO.getUserById(userId);
      }
    }
          

    Dalam contoh ini, UserDAO menyediakan antarmuka untuk mengakses data pengguna. UserDAOImpl mengimplementasikan antarmuka ini dan menggunakan DataSource untuk mengelola koneksi database. UserService berinteraksi dengan UserDAO untuk mengakses data pengguna, tanpa perlu tahu tentang detail implementasi database.

  3. Query Object Pattern:

    Pola Query Object memungkinkan Anda untuk mengenkapsulasi kueri database sebagai objek. Ini memungkinkan Anda untuk membuat kueri yang kompleks secara dinamis dan untuk menggunakan kembali kueri di beberapa tempat dalam kode Anda. Ini juga memisahkan logika kueri dari logika bisnis, membuat kode lebih mudah dipahami dan dipelihara.

    Contoh (Bahasa Ruby):

    
    class UserQuery
      def initialize(criteria = {})
        @criteria = criteria
      end
    
      def where(conditions)
        @criteria.merge!(conditions)
        self
      end
    
      def all
        # Bangun kueri SQL berdasarkan @criteria
        sql = "SELECT * FROM users"
        if @criteria.any?
          sql += " WHERE " + @criteria.map { |key, value| "#{key} = '#{value}'" }.join(" AND ")
        end
    
        # Jalankan kueri dan kembalikan hasil
        # (kode untuk eksekusi kueri dan pemetaan hasil ke objek User)
      end
    end
    
    # Penggunaan:
    query = UserQuery.new.where(username: 'john.doe', active: true)
    users = query.all
          

    Dalam contoh ini, UserQuery memungkinkan Anda untuk membangun kueri secara dinamis dengan menggunakan metode where. Metode all kemudian menjalankan kueri dan mengembalikan hasil. Ini memisahkan logika kueri dari logika bisnis dan memungkinkan Anda untuk menggunakan kembali kueri di beberapa tempat dalam kode Anda.

  4. Object-Relational Mapping (ORM):

    ORM adalah teknik yang memungkinkan Anda untuk memetakan objek dalam kode Anda ke tabel dalam database. ORM menyediakan lapisan abstraksi antara kode Anda dan database, memungkinkan Anda untuk bekerja dengan objek alih-alih SQL. ORM secara otomatis menangani konversi antara objek dan data database, menyederhanakan pengembangan dan mengurangi risiko bug.

    Contoh ORM populer termasuk Entity Framework (C#), Hibernate (Java), dan Django ORM (Python).

    Contoh (Entity Framework C#):

    
    public class User
    {
      public int Id { get; set; }
      public string Username { get; set; }
      public string Email { get; set; }
    }
    
    public class ApplicationDbContext : DbContext
    {
      public DbSet<User> Users { get; set; }
    
      protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
      {
        optionsBuilder.UseSqlServer("YourConnectionString");
      }
    }
    
    // Penggunaan:
    using (var context = new ApplicationDbContext())
    {
      var user = context.Users.Find(1); // Mendapatkan pengguna dengan ID 1
      // ...
    }
          

    Dalam contoh ini, Entity Framework memetakan kelas User ke tabel Users di database. Anda dapat menggunakan DbContext untuk mengakses dan memanipulasi data pengguna tanpa perlu menulis SQL secara langsung.

  5. Dependency Injection:

    Dependency Injection (DI) adalah pola desain yang memungkinkan Anda untuk menyediakan dependensi ke kelas daripada kelas membuat dependensi tersebut sendiri. Hal ini membuat kode lebih longgar dan lebih mudah diuji. Dalam konteks Database as Parameter, Anda dapat menggunakan DI untuk menyuntikkan objek Repository atau DAO ke kelas yang membutuhkannya, alih-alih meneruskan koneksi database secara langsung.

    Contoh (Bahasa C# dengan Dependency Injection):

    
    public interface IUserRepository
    {
      User GetUserById(int userId);
    }
    
    public class UserRepository : IUserRepository
    {
      private readonly string _connectionString;
    
      public UserRepository(string connectionString)
      {
        _connectionString = connectionString;
      }
    
      public User GetUserById(int userId)
      {
        // (Kode untuk mengakses database)
      }
    }
    
    public class UserService
    {
      private readonly IUserRepository _userRepository;
    
      public UserService(IUserRepository userRepository)
      {
        _userRepository = userRepository;
      }
    
      public User GetUser(int userId)
      {
        return _userRepository.GetUserById(userId);
      }
    }
    
    // Konfigurasi Dependency Injection (contoh menggunakan .NET Core DI container):
    public void ConfigureServices(IServiceCollection services)
    {
      services.AddTransient<IUserRepository>, UserRepository>(provider => new UserRepository("YourConnectionString"));
      services.AddTransient<UserService>();
    }
    
    // Penggunaan:
    public class MyController : Controller
    {
      private readonly UserService _userService;
    
      public MyController(UserService userService)
      {
        _userService = userService;
      }
    
      public IActionResult GetUser(int id)
      {
        var user = _userService.GetUser(id);
        // ...
      }
    }
          

    Dalam contoh ini, UserService menerima IUserRepository melalui konstruktornya. Kontainer DI bertanggung jawab untuk membuat instance UserRepository dan menyuntikkannya ke UserService. Ini membuat kode lebih longgar dan lebih mudah diuji.

6. Studi Kasus: Refactoring Database as Parameter dalam Proyek Riil

Bayangkan Anda bekerja pada aplikasi web e-commerce yang memiliki fungsi untuk menampilkan detail produk. Awalnya, kode tersebut menggunakan pendekatan Database as Parameter:


public Product GetProductDetails(SqlConnection connection, int productId) {
  // ...kode SQL untuk mengambil data produk dari database...
}

public void DisplayProduct(int productId) {
  using (SqlConnection connection = new SqlConnection(connectionString)) {
    connection.Open();
    Product product = GetProductDetails(connection, productId);
    // ...kode untuk menampilkan data produk di halaman web...
  }
}

Setelah mengidentifikasi Database as Parameter sebagai code smell, Anda memutuskan untuk melakukan refactoring menggunakan Repository Pattern:


public interface IProductRepository {
  Product GetProductById(int productId);
}

public class ProductRepository : IProductRepository {
  private readonly string _connectionString;

  public ProductRepository(string connectionString) {
    _connectionString = connectionString;
  }

  public Product GetProductById(int productId) {
    using (SqlConnection connection = new SqlConnection(_connectionString)) {
      connection.Open();
      // ...kode SQL untuk mengambil data produk dari database...
    }
  }
}

public void DisplayProduct(int productId) {
  IProductRepository productRepository = new ProductRepository(connectionString);
  Product product = productRepository.GetProductById(productId);
  // ...kode untuk menampilkan data produk di halaman web...
}

Dengan refactoring ini, kode menjadi lebih modular, mudah diuji, dan kurang terikat pada detail implementasi database. Perubahan database di masa depan akan lebih mudah diakomodasi tanpa mempengaruhi bagian lain dari aplikasi.

7. Praktik Terbaik Tambahan untuk Desain Database yang Bersih

Selain menghindari Database as Parameter, ada beberapa praktik terbaik lain yang dapat Anda ikuti untuk memastikan desain database yang bersih dan mudah dipelihara:

  • Gunakan prinsip Single Responsibility: Setiap kelas atau fungsi harus memiliki satu tanggung jawab dan hanya satu.
  • Ikuti prinsip Open/Closed: Kode harus terbuka untuk perluasan tetapi tertutup untuk modifikasi.
  • Gunakan Dependency Inversion Principle: Modul tingkat tinggi tidak boleh bergantung pada modul tingkat rendah. Keduanya harus bergantung pada abstraksi. Abstraksi tidak boleh bergantung pada detail. Detail harus bergantung pada abstraksi.
  • Desain untuk pengujian: Tulis kode yang mudah diuji dengan menggunakan unit test dan integration test.
  • Gunakan konvensi penamaan yang konsisten: Konvensi penamaan yang konsisten membuat kode lebih mudah dipahami dan dipelihara.
  • Dokumentasikan kode Anda: Dokumentasi yang baik membantu orang lain memahami kode Anda dan cara menggunakannya.
  • Lakukan code review secara teratur: Code review membantu mengidentifikasi masalah dan meningkatkan kualitas kode.

8. Kesimpulan: Mencegah Database as Parameter untuk Kode yang Lebih Baik

Database as Parameter adalah code smell yang sering diabaikan tetapi dapat menyebabkan masalah serius dalam jangka panjang. Dengan memahami risiko yang terkait dengan praktik ini dan dengan menerapkan solusi seperti Repository Pattern, DAO Pattern, Query Object Pattern, ORM, dan Dependency Injection, Anda dapat menghindari Database as Parameter dan membuat kode yang lebih bersih, lebih mudah dipelihara, lebih aman, dan lebih skalabel. Ingatlah bahwa tujuan kita sebagai pengembang perangkat lunak adalah untuk menulis kode yang bukan hanya berfungsi tetapi juga mudah dipahami, diubah, dan dipelihara dalam jangka panjang. Dengan menghindari code smell seperti Database as Parameter, kita dapat mencapai tujuan ini.

“`

omcoding

Leave a Reply

Your email address will not be published. Required fields are marked *