Lompat ke konten

Pemrograman Berorientasi Objek

Ilmu Komputer Universitas · Topik 4

Lembar Kerja

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
Graf objek dan referensi
Dua nama pada satu objek: a dan c adalah alias untuk Book yang sama, b adalah objek kedua yang setara tetapi berbeda

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
Batas antarmuka
Method publik membentuk dinding: setiap perubahan state melewati pemeriksaan; field di balik dinding bersifat pribadi

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
Diagram hubungan kelas
Dua jenis koneksi: pewarisan berarti is-a (segitiga kosong ke kelas dasar); komposisi berarti has-a (berlian penuh ke bagian)

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}'
Urutan penyaluran dinamis
Satu pemanggilan, tiga langkah: variabel menamai antarmuka, objek membawa tipe waktu-eksekusi, penyaluran memilih pengiriman dari tipe tersebut

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
Alur pengecualian
Dilempar dalam pembaruan membongkar masalah dan utama hingga penangan yang cocok; finally berjalan saat keluar, di setiap jalur

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
Peta cakupan uji
Perilaku ke bawah, uji melintang: setiap sel adalah perilaku yang ditahan oleh suatu uji; sel kosong adalah yang berikutnya untuk ditulis

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.

Kosa kata
English Bahasa Indonesia
object-oriented programming/ˈɒbdʒekt ˈɔːrɪəntɪd ˈprəʊɡræmɪŋ/ pemrograman berorientasi objek
class/klæs/ kelas
object/ˈɒbdʒekt/ objek
instance/ˈɪnstəns/ instans
instance attributes atribut实例
Class attributes Atribut Kelas
reference/ˈrefrəns/ referensi
aliases alias
equality/iːˈkwɒlɪti/ kesetaraan
identity/aɪˈdentɪti/ identitas
invariant/ɪnˈveərɪənt/ invarian
encapsulation/ɪnˌkæpsjʊˈleɪʃn/ pengkapsulan
inheritance/ɪnˈherɪtəns/ warisan
composition/ˌkɒmpəˈzɪʃn/ komposisi
fragile base class kelas dasar rapuh
interface/ˈɪntəfeɪs/ antarmuka
polymorphism/ˈpɒlɪmɔːfɪzəm/ polimorfisme
duck typing pengetikan bebek
dynamic dispatch penyampaian dinamis
substitution/ˌsʌbstɪˈtjuːʃn/ substitusi
exceptions pengecualian
unit tests uji unit
coupling koppeling
cohesion/kəʊˈhiːʒn/ kohesi
single responsibility principle prinsip tanggung jawab tunggal
runtime type tipe waktu berjalan
open/closed terbuka/tertutup
arrange persiapan
act/ækt/ pelaksanaan
assert asumsi

Pelajaran interaktif untuk topik ini

Kerjakan langkah demi langkah, dengan latihan pengecekan instan.

Topik lain dalam Ilmu Komputer Universitas

Masuk atau buat akun

IGCSE, A-Level & AP