Apa kursus ini, dan bagaimana menggunakannya
Ini adalah kursus keempat dalam Ilmu Komputer Universitas: pemrograman berorientasi objek — bagaimana mengembangkan skrip kecil menjadi sistem pemrograman**— bagaimana mengembangkan skrip kecil menjadi sistem yang dapat dengan aman diubah oleh orang lain: objek yang memilikinya sendiri, antarmuka yang menyembunyikan mekanisme mereka, dan uji coba yang menjaga ketat saat Anda melakukan refactor.
Batasan ruang lingkup, dinyatakan sekali: sebuah studi kursus, bukan gelar; Python 3 adalah kendaraan, dan setiap fragmen program adalah asli, mandiri, dan telah dieksekusi untuk menghasilkan output yang ditampilkan.
- Unit 4.1–4.2 membangun objek (kelas, referensi, dinding); 4.3–4.4 menghubungkannya; 4.5–4.6 menjaga sistem tetap hidup.
Kebiasaan desain: setiap kelas adalah kontrak dengan pemanggilnya — setiap unit menamai kontrak tersebut, dan setiap contoh adalah program yang dapat Anda jalankan.
Objek dan kelas
Mengapa ini penting. Program gagal ketika keadaan ada di mana-mana: sebuah kelas menggabungkan state dan perilaku sehingga ada tepat satu tempat untuk dilihat. Mulai dari sini, setiap sistem yang Anda bangun adalah kelas-kelas yang berbicara.
Gagasan, dengan gambar. Sebuah kelas adalah cetak biru; sebuah instansi
(sebuah objek) adalah satu hal yang dibangun darinya. __init__ berjalan pada
konstruksi dan menaburkan properti instance; metode
mengambil self, instansinya sendiri. Properti kelas tinggal di
kelas dan dibagi oleh semua instansi. Penugasan tidak pernah menyalin: sebuah
variabel adalah referensi ke sebuah objek, sehingga dua nama bisa
alias dari satu objek.
class Book:
next_id = 1 # class attribute, shared
def __init__(self, title, copies):
self.id = Book.next_id; Book.next_id += 1
self.title = title
self.copies = copies # instance attribute
def take(self):
self.copies -= 1; return self.copies

Urutan pencarian (sebuah derivasi). obj.x diselesaikan dengan berjalan keluar:
dikt instance terlebih dahulu (obj.__dict__), lalu dikt kelas, lalu
kelas dasar. Satu aturan tunggal ini menjelaskan jebakan klasik: penugasan
obj.next_id = 9 tidak menyentuh kelas — itu menindihnya hanya untuk satu
instance. id di atas masuk ke dalam dikt instance; next_id tetap berada di dalam
dikt kelas, sehingga buku-buku mendapatkan ids 1, 2, 3, … dan Book.next_id selalu
menyimpan nomor bebas berikutnya.
Contoh terpecahkan 1 — satu kelas, ditelusuri. Dengan Book di atas:
a = Book('Signals', 3)
b = Book('Signals', 3)
c = a
a.take(); a.take()
Kondisi setelah: a.copies adalah $1$, b.copies adalah $3$ (tidak tersentuh), dan
karena c adalah a, c.copies juga menjadi $1$ — satu objek, dua nama.
a == b adalah False (kesetaraan default adalah identitas) sementara a is c adalah
True: kesetaraan membandingkan nilai (setelah Anda mendefinisikan caranya), identitas
(is) membandingkan objek.
Contoh terpecahkan 2 — penghitung bersama. Setelah a dan b di atas,
Book.next_id, a.next_id, dan b.next_id semua membaca $3$ — penghitung
milik kelas, sehingga setiap pemanggil setuju. Tulis a.next_id = 9
dan hanya a yang membaca $9$: kelas masih menyatakan $3$, dan buku berikutnya
masih mendapatkan id $3$.
Kesalahan umum. Melupakan self dalam header metode. Memodifikasi
atribut kelas melalui instance dan bertanya-tanya mengapa yang lain berbeda.
Mengharapkan d = c untuk menyalin — ia membuat alias.
Ringkasan. Class = cetak biru, instance = objek; dict instance menutupi
dict class; penugasan membuat alias, tidak pernah menyalin; is adalah identitas, ==
adalah kesamaan (terdefinisi).
Enkapsulasi
Mengapa ini penting. Setiap class memiliki invariant — sebuah pernyataan yang benar di antara panggilan (salinan berada di rak atau diberikan kepada satu peminjam, tidak ada lagi). Encapsulasi adalah disiplin agar pernyataan tersebut tetap benar: state bersifat pribadi, perubahan melewati metode yang memeriksanya.
Ide tersebut, dengan gambar. Secara konvensi garis bawah di awal menandai
state internal (_copies); pemanggil menggunakan method publik — batas
antarmuka. Validasi di gerbang: konstruktor dan setiap
method yang mengubah state harus menetapkan kembali invariant atau menolak.
class Copy:
def __init__(self, title):
if title == '': raise ValueError('empty title')
self._title = title; self._out = False
def issue(self):
if self._out: raise ValueError('already out')
self._out = True
def give_back(self):
if not self._out: raise ValueError('not out')
self._out = False
def is_out(self): # public reader, no wall breach
return self._out

Turunan (mengapa satu gerang cukup). Jika setiap jalur yang menyentuh
_out melewati issue/give_back, maka invariant (_out adalah tepat
True atau False) dapat dibuktikan dari dua metode kecil
metode. Tambahkan satu penulis yang tidak terverifikasi dan pembuktian hilang — jumlah
tempat untuk dicek bertambah seiring setiap pemanggil, begitu pula tingkat kesalahan.
Contoh terpecahkan 1 — dinding tetap kokoh. c.issue() berhasil;
c.issue() lagi memunculkan ValueError — pemanggil tidak dapat mencapai
keadaan mustahil, dan setelah panggilan gagal objek tidak berubah
(cek-dulu, jadi tidak perlu rollback). c.give_back(); c._out adalah
False, dan c.issue() bekerja lagi: tepat keadaan True /
False, tidak ada yang lain, selamanya.
Contoh terpecahkan 2 — biaya kebocoran. Seorang rekan kerja yang terburu-buru menulis
c._out = 'y'. Tidak ada yang menghambat mereka — garis bawah adalah konvensi — dan sekarang
is_out() menjawab 'y': bernilai benar tetapi bukan True. Satu pemanggil
(if c.is_out():) menyimpan salinan sebagai keluar; pemanggil lain
(if c.is_out() is True:) menyimpannya sebagai tersedia — dua pembaca
berpendapat berbeda tentang satu objek, suatu keadaan yang tidak mungkin dihasilkan oleh metode apa pun.
Biaya perbaikan meningkat seiring setiap pembaca _out; biaya validasi adalah
dua pernyataan if. Ketidakseimbangan inilah inti argumennya.
Kesalahan umum. Mengembalikan daftar internal (return self._loans)
— pemanggil mengubah state pribadi Anda; kembalikan salinan. Validasi dalam
konstruktor tetapi bukan dalam setter. Menulis pasangan get/set untuk semuanya:
tampilkan perilaku (available()), bukan penyimpanan.
Ringkasan. Garis bawah menandakan niat; metode memvalidasi di gerbang; satu jalur yang dicek per perubahan menjaga invarian dapat dibuktikan; tampilkan perilaku, bukan bidang.
Komposisi dan pewarisan
Mengapa ini penting. Objek terhubung dengan dua cara: pewarisan
(is-a: sebuah Member adalah Person) dan komposisi (has-a: sebuah
Library memiliki Catalog). Memilih salah mengikat kelas begitu erat
bahwa perbaikan kelas dasar merusak anak-anaknya — masalah kelas dasar rapuh
masalah.
Ide tersebut, dengan gambar. Rantai pewarisan inisialisasi:
super().__init__(...) berjalan naik sepanjang rantai, dimulai dari kelas dasar. Komposisi
mendelegasikan: seluruh objek memiliki bagian dan meneruskan pemanggilan.
class Person:
def __init__(self, name):
self._name = name
class Member(Person):
def __init__(self, name, card_no):
super().__init__(name) # base first
self._card = card_no; self._loans = 0
def borrow(self):
self._loans += 1; return self._loans

Turunan (urutan inisialisasi). Member('Ana', 7) menjalankan
Member.__init__, tindakan pertamanya adalah super().__init__('Ana') — sehingga
Person.__init__ selesai sebelum baris Member apa pun dieksekusi. Urutannya
selalu dari atas ke bawah sepanjang rantai; lewati pemanggilan super() dan self._name
tidak ada saat Member membutuhkannya, gagal dengan error AttributeError yang jelas
bukan dengan merusak secara diam-diam.
Contoh terpecahkan 1 — komposisi mendelegasikan. Sebuah Library memiliki
Catalog (daftar dari Copy):
class Library:
def __init__(self):
self._shelves = [] # composition
def add(self, copy): self._shelves.append(copy)
def available(self):
return sum(1 for c in self._shelves if not c.is_out())
Setelah menambahkan salinan 5 dan menerbitkan 2, available() adalah $3$ — perpustakaan
tidak memiliki logika peminjaman; ia meminta bagian-bagiannya. Ganti daftar dengan basis data
berupa listalike dan Library tetap tidak berubah: bagian berubah, kesatuan tetap mempertahankan
antarmukanya.
Contoh terpecahkan 2 — dasar rapuh. Dasar Receipt.total() menjumlahkan item; subclass menimpa dengan menyalin kode penjumlahan dan mengurangkan
item; subkelas menimpa dengan menyalin kode penjumlahan dan mengurangkan
$10$. Basis kemudian mendapat baris loyalitas — setiap struk diskon
tanpa suara kehilangannya: salinan adalah percabangan. Perbaikan: return super().total() - 10 — menimpa dengan mendelegasikan. Pewarisan untuk is-a,
komposisi untuk segalanya.
logika dasar dalam override—percabangan yang menjadi usang.
Ringkasan. is-a mewarisi (dan memanggil super().__init__); has-a menyusun (dan
mendelegasikan); jangan pernah menyalin logika dasar ke dalam override.
Ringkasan. is-a mewarisi (dan memanggil super()); has-a mengkomposisi (dan
Mengapa hal ini penting. Sistem besar bertahan terhadap perubahan ketika pemanggil bergantung pada
Antarmuka dan polimorfisme
Mengapa ini penting. Sistem besar bertahan dari perubahan ketika pemanggil bergantung pada apa yang dilakukan oleh sesuatu, bukan kelas mana yang dimilikinya. Sebuah antarmuka menamai perilaku; polimorfisme adalah satu titik pemanggilan yang menjalankan banyak implementasi.
Gagasan, dengan gambar. Python bersifat penyetian dinamis: objek apa pun
dengan metode yang sesuai dapat digunakan — penyetian bebek. Untuk membuat
kontrak secara eksplisit, tulis Protocol (atau abc.ABC) yang menamai metode-metode tersebut.
class Notifier(Protocol):
def send(self, text: str) -> str: ...
class EmailNotify:
def send(self, text): return f'email: {text}'
class SmsNotify:
def send(self, text): return f'sms: {text}'

Penurunan (penyaluran dinamis). Ketika n.send('due') dijalankan,
pencarian atribut 4.1 diselesaikan pada tipe waktu-eksekusi —
objek itu sendiri, bukan anotasi variabelnya. Jadi satu badan perulangan
mengirimkan ke send yang berbeda untuk setiap elemen. Ini adalah penyaluran
dinamis: titik pemanggilan ditulis sekali, pemilihan
terjadi per objek saat pemanggilan.
Contoh terpecahkan 1 — satu perulangan, tiga pengirim. Dengan pengirim ketiga
QueueNotify dengan bentuk yang sama dan team = [EmailNotify(), SmsNotify(), QueueNotify()]:
for n in team:
print(n.send('loan due'))
Tiga baris keluar — email: loan due, sms: loan due, queue: pinjaman jatuh tempo — dan menambahkan pengirim keempat tidak menyentuh baris yang ada: Caller
tidak pernah diberitahu kelas konkret. Ini adalah prinsip buka/tutup
bentuk: terbuka terhadap jenis baru, tertutup untuk pengeditan.
Contoh terpecahkan 2 — substitusi dan perangkapnya. Substitusi yang
mematuhi slot kontrak secara tak kasatmata. Satu yang menyempitkannya gagal
tanpa suara: sebuah SilentNotify.send yang melempar pada string kosong melanggar
pemanggil yang kontraknya menyatakan send tidak pernah melempar. Aturan
substitusi (Liskov): pra-kondisi hanya boleh meluas,
jaminan hanya dapat bertambah.
Kesalahan umum. Rantai isinstance — ganti dengan metode setiap
kelas mengimplementasikannya. Override yang menambahkan pra-kondisi atau menghilangkan
jaminan: pemanggil tidak bisa tahu, sampai terjadi kegagalan.
Ringkasan. Beri nama perilaku dengan Protocol; andalkan nama itu; penyeleksian mengikuti tipe waktu eksekusi; substitusi harus mematuhi seluruh kontrak.
Pengecualian dan penanganan kesalahan
Mengapa ini penting. Kesalahan merambat. Fungsi tiga bingkai di bawahnya tidak bisa memperbaiki kegagalan, tetapi tidak boleh mengabaikannya. Pengecualian membawa kesalahan ke bingkai yang dapat memutuskan, menjalankan pembersihan setiap bingkai sepanjang jalur.
Ide tersebut, dengan gambar. raise melempar; try/except menangkap berdasarkan
tipe; finally berjalan pada setiap jalur. Hierarki kustom memungkinkan
pemanggil menangkap secara tepat: dasar yang luas, anak yang spesifik.
class LoanError(Exception): ...
class RenewalError(LoanError): ...
class LimitError(RenewalError): ...
class Loan:
def __init__(self, days):
self.days = days
def renew(loan, extra):
if loan.days + extra > 30:
raise LimitError(f'{loan.days}+{extra} exceeds 30')
loan.days += extra
return loan.days

Turunan (nilai status vs pengecualian). Pengembalian $-1$ adalah
nilai normal: kompiler dan runtime tidak melampirkan apa pun padanya, sehingga pemanggil
yang lupa memeriksa terus menghitung dengan hal tak bermakna — kegagalan
menyebar diam-diam. Pengecualian, setelah dilempar, tidak dapat diabaikan: ia
membongkar bingkai demi bingkai (menjalankan setiap finally) hingga beberapa except
cocok; jika tidak ada yang cocok, program berhenti keras. Kesalahan yang dapat diabaikan mendapat
pengabaian; kesalahan yang tidak dapat diabaikan ditangani. Pilih nilai status untuk
hasil yang diharapkan, dekat pemanggil (kegagalan pencarian), pengecualian
untuk kontrak yang rusak dan kegagalan yang tidak dapat diperbaiki oleh pemanggil langsung.
Contoh terpecahkan 1 — tangkapan presisi. renew(Loan(20), 15) melempar
LimitError dengan pesan 20+15 exceeds 30; pemanggil menangkap
RenewalError (induk) dan pinjaman tetap membaca $20$: periksa-dahulu
berarti panggilan gagal meninggalkan keadaan tak berubah. Hierarki adalah filter —
pemanggil memilih tingkat kehalusan.
Contoh terpecahkan 2 — pembersihan di kedua jalur. Penjaga kunci:
locked = True
loan = Loan(28)
try:
total = renew(loan, 10) # raises
except RenewalError:
kept = 'handled'
finally:
locked = False
Jejak: renew dilempar; except berjalan (kept = 'handled'); finally
berjalan; afterwards locked adalah False dan pinjaman tetap membaca $28$ —
dilepaskan pada jalur yang gagal. Pada jalur sukses finally berjalan hanya
yang sama. Pembersihan yang harus dilakukan masuk ke dalam finally (atau blok
with), tidak pernah setelah pernyataan terakhir.
Kesalahan umum. except: tanpa nama — menangkap KeyboardInterrupt dan
menyembunyikan bug asli. except ... pass — kegagalan menghilang dan status
terperbar separuh. Menangkap Exception ketika Anda bermaksud menangkap satu anak spesifik.
Ringkasan. Angkat pada kontrak yang rusak, tangkap di kerangka kerja yang dapat
memutuskan, finally untuk pembersihan; hierarki kecil membeli penanganan presisi;
panggilan yang gagal harus meninggalkan status tetap tidak berubah.
Pengujian dan kualitas desain
Mengapa ini penting. Refaktorisasi — mengubah struktur tanpa mengubah perilaku — hanya aman jika perilaku tertanam dengan jelas. Uji unit adalah jangkar tersebut, dan uji yang baik menguji perilaku, bukan metode.
Gagasan, dengan gambar. Satu uji = satu perilaku: susun
objek, tindakan satu panggilan, asertifikasi hasil yang terlihat.
Perilaku, bukan metode: renew past the limit raises adalah
perilaku; test_days adalah metode.
def test_renew_ok():
loan = Loan(20) # arrange
out = renew(loan, 7) # act
assert out == 27 # assert
def test_renew_boundary():
assert renew(Loan(20), 10) == 30 # exactly 30 is allowed
def test_renew_limit():
loan = Loan(20)
try:
renew(loan, 11); assert False
except LimitError:
assert loan.days == 20 # unchanged

Penurunan (mengapa pendekatan berbasis perilaku). Uji bernama metode rusak setiap kali penamaan diubah dan bertahan melalui setiap perubahan perilaku — kebisingan tanpa keselamatan; uji yang dinamai berdasarkan perilaku justru sebaliknya. Cakupan mengikuti daftar perilaku: setiap cabang kontrak adalah satu baris, dan grid menunjukkan lubang berikutnya.
Contoh terarah 1 — tiga perilaku, tiga uji. Untuk renew:
dalam batas ($20 + 7 = 27$), tepat di batas ($20 + 10 = 30$
diperbolehkan, karena pengecekan adalah $> 30$), dan melewatinya ($20 + 11$
mengangkat, status tidak berubah). Uji batas menangkap kesalahan klasik
penulisan >= untuk >. Menjalankannya mencetak 3 passed.
Contoh terarah 2 — refactor di bawah warna hijau. Denda pinjaman
perhitungan keluar dari Loan ke kelas FineCalculator (prinsip tanggung jawab tunggal: satu alasan untuk berubah). The
prinsip tanggung jawab: satu alasan untuk mengubah). The
tiga uji tidak tersentuh, tetap 3 passed — perilaku terjaga sementara
atas.
Kesalahan umum. Mengassert internal pribadi — refactor merusak
uji tersebut. Status uji mutabel bersama (uji B berhasil hanya setelah uji A berjalan). Mengejar cakupan baris pada getter sementara cabang batas tidak memiliki Ringkasan. Atur-tindakan-assert per perilaku; uji membatu perilaku sehingga uji.
refactor aman; SRP menurunkan biaya perubahan; ketergantungan rendah dan kohesi tinggi adalah arah setiap gerakan. kohesi tinggi adalah arah setiap langkah.