Bonus Materi - OOP Lanjutan (Pilar OOP)

Bonus Materi - OOP Lanjutan

Materi pengayaan opsional tentang empat pilar OOP - Encapsulation, Inheritance, Polymorphism, dan Abstraction.

Tentang Materi Ini

Sifat: OPSIONAL (Pengayaan). Materi ini tidak diujikan dan bukan bagian dari praktikum wajib. Dikerjakan bagi mahasiswa yang ingin memahami OOP secara lebih utuh, dan bernilai poin bonus.

Prasyarat: sudah menyelesaikan Pertemuan 11 (OOP Python).

Pada Pertemuan 11 kita mempelajari fondasi OOP — class, object, atribut, dan method — secukupnya untuk membangun aplikasi penjualan. Namun OOP sesungguhnya memiliki empat pilar yang sering ditanyakan di dunia kerja maupun mata kuliah lanjutan. Materi ini melengkapinya.

Pilar Pertanyaan yang dijawab
Encapsulation Bagaimana melindungi data agar tidak diubah sembarangan?
Inheritance Bagaimana membuat class baru tanpa menulis ulang kode?
Polymorphism Bagaimana satu perintah bisa berperilaku berbeda pada objek berbeda?
Abstraction Bagaimana menetapkan aturan yang wajib dipenuhi class turunan?
Catatan penting: aplikasi penjualan yang kita bangun (Pertemuan 12–16) tidak mewajibkan keempat pilar ini. Materi ini murni untuk memperluas pemahaman, bukan mengubah project.

Tujuan

Setelah menyelesaikan materi bonus ini, mahasiswa mampu:

  1. Menerapkan Encapsulation menggunakan atribut privat dan @property.
  2. Menerapkan Inheritance dengan super() dan override method.
  3. Menerapkan Polymorphism melalui perlakuan seragam terhadap objek berbeda.
  4. Menerapkan Abstraction menggunakan ABC dan @abstractmethod.

Persiapan

text
- VS Code
- Project toko-elektronik
- Virtual environment aktif (venv)
- File models/produk.py (dari Pertemuan 11)

Buat file baru oop_lanjutan.py di folder utama project untuk seluruh latihan di materi ini.

Konsep Singkat

Empat pilar OOP menjawab masalah nyata dalam pengembangan aplikasi:

  • Encapsulation (pembungkusan) — data disembunyikan dan hanya boleh diubah lewat "pintu" yang kita sediakan, sehingga aturan selalu terjaga.
  • Inheritance (pewarisan) — class baru mewarisi seluruh isi class induk, lalu menambah atau mengganti bagian tertentu. Tidak perlu menyalin kode.
  • Polymorphism (banyak bentuk) — objek yang berbeda dapat menanggapi perintah yang sama dengan cara masing-masing.
  • Abstraction (abstraksi) — membuat kerangka/kontrak yang wajib dilengkapi oleh class turunan.

Langkah 1 - Encapsulation

Pada Pertemuan 11, atribut stok bisa diubah langsung dari luar. Ini berbahaya:

python
laptop = Produk("Laptop", "Elektronik", 8500000, 10)
laptop.stok = -50        # tidak masuk akal, tetapi Python mengizinkan!

Encapsulation menutup celah tersebut. Python menandai atribut yang dilindungi dengan garis bawah:

Penulisan Arti Sifat
self.nama Publik Bebas diakses
self._harga Protected Sebaiknya hanya dipakai internal (kesepakatan)
self.__stok Private Tidak bisa diakses langsung dari luar

Gunakan @property sebagai "pintu" resmi untuk membaca dan mengubah data:

python
class ProdukEnc:
    def __init__(self, nama, harga, stok=0):
        self.nama = nama
        self._harga = harga         # protected
        self.__stok = stok          # private

    @property
    def stok(self):                 # getter - dipakai saat MEMBACA
        return self.__stok

    @stok.setter
    def stok(self, nilai):          # setter - dipakai saat MENGUBAH
        if nilai < 0:
            raise ValueError("Stok tidak boleh negatif")
        self.__stok = nilai

    @property
    def harga(self):
        return self._harga

Uji:

python
p = ProdukEnc("Laptop", 8500000, 10)

print("Stok:", p.stok)          # membaca -> lewat getter
p.stok = 7                      # mengubah -> lewat setter
print("Stok:", p.stok)

try:
    p.stok = -5                 # ditolak setter
except ValueError as e:
    print("Ditolak:", e)

try:
    print(p.__stok)             # akses paksa -> gagal
except AttributeError as e:
    print("Akses langsung ditolak:", e)

Output:

text
Stok: 10
Stok: 7
Ditolak: Stok tidak boleh negatif
Akses langsung ditolak: 'ProdukEnc' object has no attribute '__stok'
Keunggulannya: pemakaian tetap sederhana (p.stok = 7, seperti atribut biasa), tetapi di baliknya ada penjaga. Data tidak bisa dirusak dari luar.

Langkah 2 - Inheritance

Bayangkan toko mulai menjual produk digital (voucher, antivirus, software). Produk digital punya harga dan nama seperti produk biasa, tetapi tidak memiliki berat dan stoknya tidak habis.

Menyalin seluruh class Produk jelas boros. Solusinya: pewarisan.

Diagram inheritance

Class induk (sama seperti Pertemuan 11, ditambah method info()):

python
class Produk:
    def __init__(self, nama_produk, kategori, harga, stok=0, id_produk=None):
        self.id_produk = id_produk
        self.nama_produk = nama_produk
        self.kategori = kategori
        self.harga = harga
        self.stok = stok

    def nilai_persediaan(self):
        return self.harga * self.stok

    def info(self):
        return f"{self.nama_produk} - Rp {self.harga:,}"

    def kurangi_stok(self, jumlah):
        if jumlah > self.stok:
            raise ValueError(f"Stok {self.nama_produk} tidak cukup")
        self.stok -= jumlah
        return self.stok

    def __str__(self):
        return self.info()

Dua class turunan:

python
class ProdukFisik(Produk):
    def __init__(self, nama_produk, kategori, harga, stok=0, berat_kg=0, id_produk=None):
        super().__init__(nama_produk, kategori, harga, stok, id_produk)
        self.berat_kg = berat_kg              # atribut TAMBAHAN

    def ongkos_kirim(self):                   # method TAMBAHAN
        return int(self.berat_kg * 10000)

    def info(self):                           # OVERRIDE method induk
        return f"{self.nama_produk} - Rp {self.harga:,} | {self.berat_kg} kg | stok: {self.stok}"


class ProdukDigital(Produk):
    def __init__(self, nama_produk, kategori, harga, ukuran_mb=0, id_produk=None):
        super().__init__(nama_produk, kategori, harga, stok=999, id_produk=id_produk)
        self.ukuran_mb = ukuran_mb

    def ongkos_kirim(self):
        return 0                              # digital tidak dikirim

    def kurangi_stok(self, jumlah):           # OVERRIDE: stok digital tak pernah habis
        return self.stok

    def info(self):
        return f"{self.nama_produk} (digital) - Rp {self.harga:,} | {self.ukuran_mb} MB"

Uji:

python
laptop = ProdukFisik("Laptop", "Elektronik", 8500000, 10, berat_kg=2.5)
antivirus = ProdukDigital("Antivirus 1thn", "Software", 350000, ukuran_mb=120)

print(laptop.info())
print(antivirus.info())

# method WARISAN dari induk tetap bisa dipakai tanpa ditulis ulang
print("Nilai persediaan laptop:", laptop.nilai_persediaan())
print("laptop juga sebuah Produk?", isinstance(laptop, Produk))

Output:

text
Laptop - Rp 8,500,000 | 2.5 kg | stok: 10
Antivirus 1thn (digital) - Rp 350,000 | 120 MB
Nilai persediaan laptop: 85000000
laptop juga sebuah Produk? True

Istilah penting:

Istilah Arti
class Anak(Induk) Anak mewarisi seluruh atribut & method induk
super().__init__(...) Memanggil constructor induk (agar tidak ditulis ulang)
Override Menulis ulang method induk dengan perilaku berbeda
isinstance(obj, Produk) Memeriksa apakah objek termasuk turunan Produk

Langkah 3 - Polymorphism

Polymorphism = "banyak bentuk". Kita memanggil method yang sama pada objek berbeda, dan masing-masing menanggapi dengan caranya sendiri.

Perhatikan: kode di bawah tidak perlu tahu jenis tiap produk. Tidak ada satu pun if.

python
keranjang = [
    ProdukFisik("Laptop", "Elektronik", 8500000, 10, berat_kg=2.5),
    ProdukDigital("Antivirus 1thn", "Software", 350000, ukuran_mb=120),
    ProdukFisik("Mouse", "Aksesoris", 150000, 50, berat_kg=0.2),
]

total_ongkir = 0
for item in keranjang:
    print(f"{item.info()}")
    print(f"   ongkir: Rp {item.ongkos_kirim():,}")
    total_ongkir += item.ongkos_kirim()      # method sama, hasil berbeda

print(f"\nTotal ongkir: Rp {total_ongkir:,}")

Output:

text
Laptop - Rp 8,500,000 | 2.5 kg | stok: 10
   ongkir: Rp 25,000
Antivirus 1thn (digital) - Rp 350,000 | 120 MB
   ongkir: Rp 0
Mouse - Rp 150,000 | 0.2 kg | stok: 50
   ongkir: Rp 2,000

Total ongkir: Rp 27,000

Perhatikan juga override kurangi_stok() pada produk digital:

python
antivirus.kurangi_stok(5)
print("Stok digital setelah terjual 5:", antivirus.stok)   # tetap 999
Mengapa ini hebat? Tanpa polymorphism, kita harus menulis:
```python
if isinstance(item, ProdukDigital):
ongkir = 0
elif isinstance(item, ProdukFisik):
ongkir = item.berat_kg * 10000
```
Setiap kali ada jenis produk baru, if bertambah panjang. Dengan polymorphism, cukup buat class baru — kode yang memakainya tidak perlu diubah sama sekali.

Langkah 4 - Abstraction

Abstraction membuat "kontrak": sebuah kerangka class yang wajib dilengkapi turunannya. Contoh nyata: aplikasi kasir mendukung berbagai metode pembayaran, dan setiap metode wajib punya cara membayar.

Gunakan ABC (Abstract Base Class) dan @abstractmethod:

python
from abc import ABC, abstractmethod


class MetodePembayaran(ABC):
    """Kerangka: setiap metode pembayaran WAJIB punya method bayar()."""

    @abstractmethod
    def bayar(self, jumlah):
        pass                                  # sengaja dikosongkan

    def struk(self, jumlah):                  # method biasa, diwariskan apa adanya
        return f"Dibayar Rp {jumlah:,} via {self.__class__.__name__}"


class Tunai(MetodePembayaran):
    def bayar(self, jumlah):
        return f"Terima tunai Rp {jumlah:,}"


class Transfer(MetodePembayaran):
    def __init__(self, bank):
        self.bank = bank

    def bayar(self, jumlah):
        return f"Transfer Rp {jumlah:,} ke bank {self.bank}"

Uji — perhatikan polymorphism ikut bekerja di sini:

python
daftar_metode = [Tunai(), Transfer("BCA")]

for metode in daftar_metode:
    print(metode.bayar(8800000))
    print("  ", metode.struk(8800000))

# class abstrak TIDAK BISA dibuat objeknya
try:
    m = MetodePembayaran()
except TypeError as e:
    print("\nGagal:", e)

Output:

text
Terima tunai Rp 8,800,000
   Dibayar Rp 8,800,000 via Tunai
Transfer Rp 8,800,000 ke bank BCA
   Dibayar Rp 8,800,000 via Transfer

Gagal: Can't instantiate abstract class MetodePembayaran without an implementation for abstract method 'bayar'
Gunanya: bila suatu hari ditambahkan class QRIS(MetodePembayaran) tetapi lupa menulis method bayar(), Python langsung menolaknya. Kesalahan tertangkap sejak awal, bukan saat aplikasi sudah dipakai pelanggan.

Referensi Fungsi Penting

Ringkasan Empat Pilar

Pilar Kata kunci Python Inti
Encapsulation __atribut, @property, @x.setter Melindungi data
Inheritance class Anak(Induk), super() Mewarisi kode
Polymorphism override method Satu perintah, banyak perilaku
Abstraction ABC, @abstractmethod Kontrak yang wajib dipenuhi

Penulisan yang Sering Dipakai

Perintah Fungsi
self._x Atribut protected (kesepakatan)
self.__x Atribut private (tidak bisa diakses dari luar)
@property Menandai method sebagai getter
@x.setter Menandai method sebagai setter
super().__init__(...) Memanggil constructor induk
isinstance(obj, Class) Memeriksa jenis objek
obj.__class__.__name__ Mengambil nama class dari objek
from abc import ABC, abstractmethod Membuat class abstrak

Composition vs Inheritance

Perlu diketahui: class Transaksi di Pertemuan 11 sebenarnya sudah memakai konsep bernama Composition — objek yang memuat objek lain (self.items berisi daftar ItemTransaksi).

Konsep Hubungan Contoh
Inheritance "adalah sebuah" (is-a) ProdukDigital adalah sebuah Produk
Composition "memiliki" (has-a) Transaksi memiliki beberapa ItemTransaksi
Keduanya sama-sama penting. Panduan praktisnya: gunakan inheritance bila memang sejenis, gunakan composition bila sekadar memuat/menggunakan.

Pengujian

Pastikan hasil praktikum sesuai berikut:

  1. Encapsulation: p.stok = -5 memunculkan ValueError, dan p.__stok tidak bisa diakses langsung.
  2. Inheritance: laptop.nilai_persediaan() bekerja walaupun method itu tidak ditulis di ProdukFisik (diwarisi dari Produk).
  3. Polymorphism: perulangan pada keranjang menghasilkan ongkir berbeda (Laptop 25.000, Antivirus 0, Mouse 2.000) tanpa satu pun if.
  4. Abstraction: MetodePembayaran() langsung memunculkan TypeError.

Verifikasi cepat:

python
laptop = ProdukFisik("Laptop", "Elektronik", 8500000, 10, berat_kg=2.5)
antivirus = ProdukDigital("Antivirus", "Software", 350000, ukuran_mb=120)

assert isinstance(laptop, Produk)
assert laptop.nilai_persediaan() == 85000000     # method warisan
assert laptop.ongkos_kirim() == 25000
assert antivirus.ongkos_kirim() == 0             # override
antivirus.kurangi_stok(5)
assert antivirus.stok == 999                     # stok digital tak berkurang
print("Semua pengujian lolos.")

Troubleshooting

Masalah Penyebab Solusi
AttributeError: object has no attribute '__stok' Mencoba mengakses atribut privat dari luar Gunakan property (p.stok), bukan p.__stok
TypeError: __init__() missing arguments pada class anak Lupa memanggil super().__init__(...) Panggil constructor induk di baris pertama
Method induk hilang di class anak Lupa menulis (Induk) saat mendefinisikan class Tulis class ProdukFisik(Produk):
Can't instantiate abstract class Membuat objek dari class abstrak Buat objek dari class turunannya
Class turunan ditolak Python Ada @abstractmethod yang belum diisi Lengkapi semua method abstrak
Setter tidak jalan Nama property dan setter berbeda @property def stok harus sepasang dengan @stok.setter

Tugas Bonus

Kerjakan bila ingin memperoleh nilai tambahan.

  1. Encapsulation: ubah class Produk sehingga harga menjadi atribut privat, dengan setter yang menolak harga bernilai nol atau negatif.
  2. Inheritance: buat class ProdukBundling(Produk) — paket berisi beberapa produk sekaligus, dengan atribut tambahan daftar_isi (list nama produk) dan method jumlah_isi().
  3. Polymorphism: override method info() pada ProdukBundling sehingga menampilkan isi paketnya. Lalu masukkan ketiga jenis produk (fisik, digital, bundling) ke dalam satu list dan tampilkan seluruhnya dengan satu perulangan saja.
  4. Abstraction: tambahkan class QRIS(MetodePembayaran). Buktikan bahwa bila method bayar() sengaja tidak ditulis, Python menolak pembuatan objeknya.
  5. Refleksi (tulis singkat): menurut Anda, apakah aplikasi penjualan pada Pertemuan 12–16 sebaiknya memakai ProdukFisik/ProdukDigital, ataukah cukup satu class Produk saja? Jelaskan alasannya.

Kumpulkan berupa file .py beserta tangkapan layar hasil menjalankannya (format PDF).

Kesimpulan

Materi bonus ini melengkapi pemahaman OOP dengan empat pilarnya. Encapsulation melindungi data lewat atribut privat dan property. Inheritance memungkinkan class baru mewarisi kode tanpa menyalinnya. Polymorphism membuat satu perintah dapat berperilaku berbeda pada objek berbeda, sehingga kode bebas dari tumpukan if. Abstraction menetapkan kontrak yang wajib dipenuhi class turunan. Perlu diingat, aplikasi penjualan pada Pertemuan 12–16 tetap cukup memakai class sederhana dari Pertemuan 11 — pilar-pilar ini bukan syarat, melainkan alat yang berguna ketika aplikasi tumbuh semakin besar dan beragam.