Python OOP

Abstract Class

Memahami abstract classes, abstract methods, dan interface di Python

Apa itu Abstract Class?

Abstract class adalah class yang tidak dapat diinstansiasi secara langsung dan berfungsi sebagai blueprint atau template untuk class lain. Abstract class dapat berisi abstract methods (method tanpa implementasi) yang harus diimplementasikan oleh subclass.

Analogi

Bayangkan blueprint rumah. Kalian tidak bisa tinggal di dalam blueprint, tapi Kalian bisa menggunakan blueprint tersebut untuk membangun rumah yang sebenarnya. Abstract class adalah blueprint, dan concrete class adalah rumah yang dibangun dari blueprint tersebut.

Module ABC (Abstract Base Classes)

Python menyediakan module abc (Abstract Base Classes) untuk membuat abstract class:

from abc import ABC, abstractmethod

Komponen Utama

  • ABC: Base class untuk membuat abstract class
  • @abstractmethod: Decorator untuk menandai method sebagai abstract

Membuat Abstract Class

Basic Abstract Class

basic_abstract.py
from abc import ABC, abstractmethod

class Shape(ABC):
    """Abstract base class untuk bentuk geometris"""

    @abstractmethod
    def area(self):
        """Method abstract untuk menghitung luas"""
        pass

    @abstractmethod
    def perimeter(self):
        """Method abstract untuk menghitung keliling"""
        pass

    def describe(self):
        """Method concrete (non-abstract)"""
        return "Ini adalah bentuk geometris"

# Error - tidak bisa membuat instance dari abstract class
try:
    shape = Shape()
except TypeError as e:
    print(f"Error: {e}")
    # Output: Can't instantiate abstract class Shape with abstract methods area, perimeter

Concrete Implementation

Subclass harus mengimplementasikan semua abstract methods:

concrete_shape.py
from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass

    @abstractmethod
    def perimeter(self):
        pass

class Rectangle(Shape):
    def __init__(self, width, height):
        self.width = width
        self.height = height

    def area(self):
        return self.width * self.height

    def perimeter(self):
        return 2 * (self.width + self.height)

    def describe(self):
        return f"Rectangle {self.width}x{self.height}"

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        import math
        return math.pi * self.radius ** 2

    def perimeter(self):
        import math
        return 2 * math.pi * self.radius

# Sekarang bisa membuat instance
rect = Rectangle(5, 4)
circle = Circle(3)

print(f"Rectangle area: {rect.area()}")           # 20
print(f"Rectangle perimeter: {rect.perimeter()}")  # 18
print(f"Circle area: {circle.area():.2f}")         # 28.27
print(f"Circle perimeter: {circle.perimeter():.2f}") # 18.85

Partial Implementation

Jika subclass tidak mengimplementasikan semua abstract methods, subclass tersebut juga harus abstract:

partial_implementation.py
from abc import ABC, abstractmethod

class Animal(ABC):
    @abstractmethod
    def speak(self):
        pass

    @abstractmethod
    def move(self):
        pass

# Partial implementation - masih abstract
class Mammal(Animal):
    def speak(self):
        return "Some sound"
    # move() belum diimplementasikan, jadi Mammal masih abstract

# Error - tidak bisa instantiate Mammal
try:
    m = Mammal()
except TypeError as e:
    print(f"Error: {e}")

# Concrete implementation
class Dog(Mammal):
    def move(self):
        return "Walking on four legs"

# Sekarang bisa instantiate
dog = Dog()
print(dog.speak())  # Some sound
print(dog.move())   # Walking on four legs

Abstract Properties

Kalian juga bisa membuat abstract properties menggunakan @property dan @abstractmethod:

abstract_property.py
from abc import ABC, abstractmethod

class Vehicle(ABC):
    @property
    @abstractmethod
    def max_speed(self):
        """Abstract property untuk kecepatan maksimum"""
        pass

    @property
    @abstractmethod
    def fuel_type(self):
        """Abstract property untuk jenis bahan bakar"""
        pass

    @abstractmethod
    def start_engine(self):
        pass

class Car(Vehicle):
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
        self._max_speed = 200
        self._fuel_type = "Gasoline"

    @property
    def max_speed(self):
        return self._max_speed

    @property
    def fuel_type(self):
        return self._fuel_type

    def start_engine(self):
        return f"{self.brand} {self.model} engine started"

class ElectricCar(Vehicle):
    def __init__(self, brand, model):
        self.brand = brand
        self.model = model
        self._max_speed = 180
        self._fuel_type = "Electric"

    @property
    def max_speed(self):
        return self._max_speed

    @property
    def fuel_type(self):
        return self._fuel_type

    def start_engine(self):
        return f"{self.brand} {self.model} motor started silently"

# Penggunaan
car = Car("Toyota", "Camry")
electric = ElectricCar("Tesla", "Model 3")

print(f"{car.brand}: Max speed {car.max_speed} km/h, Fuel: {car.fuel_type}")
print(f"{electric.brand}: Max speed {electric.max_speed} km/h, Fuel: {electric.fuel_type}")

Interface Pattern

Python tidak memiliki keyword "interface" seperti Java, tetapi kita bisa membuat interface menggunakan abstract class di mana semua method adalah abstract:

interface_pattern.py
from abc import ABC, abstractmethod

class Drawable(ABC):
    """Interface untuk object yang bisa digambar"""

    @abstractmethod
    def draw(self):
        pass

    @abstractmethod
    def resize(self, scale):
        pass

class Clickable(ABC):
    """Interface untuk object yang bisa diklik"""

    @abstractmethod
    def on_click(self):
        pass

# Multiple interface implementation
class Button(Drawable, Clickable):
    def __init__(self, text, x, y):
        self.text = text
        self.x = x
        self.y = y
        self.scale = 1.0

    def draw(self):
        return f"Drawing button '{self.text}' at ({self.x}, {self.y}), scale: {self.scale}"

    def resize(self, scale):
        self.scale = scale
        return f"Button resized to {scale}x"

    def on_click(self):
        return f"Button '{self.text}' clicked!"

class Image(Drawable):
    def __init__(self, src, x, y):
        self.src = src
        self.x = x
        self.y = y
        self.scale = 1.0

    def draw(self):
        return f"Drawing image '{self.src}' at ({self.x}, {self.y})"

    def resize(self, scale):
        self.scale = scale
        return f"Image resized to {scale}x"

# Penggunaan
button = Button("Submit", 100, 200)
image = Image("logo.png", 50, 50)

print(button.draw())
print(button.resize(1.5))
print(button.on_click())

print(image.draw())
print(image.resize(2.0))

Multiple Inheritance

Python mendukung multiple inheritance, sehingga sebuah class bisa mengimplementasikan multiple interfaces (abstract classes). Ini sangat berguna untuk membuat sistem yang modular dan fleksibel.

Contoh Lengkap: Database Connection

Mari lihat contoh lengkap abstract class untuk database connection:

database_connection.py
from abc import ABC, abstractmethod
from datetime import datetime

class DatabaseConnection(ABC):
    """Abstract base class untuk database connection"""

    def __init__(self, host, port, database):
        self.host = host
        self.port = port
        self.database = database
        self.connected = False
        self.connection_time = None

    @abstractmethod
    def connect(self):
        """Connect to database"""
        pass

    @abstractmethod
    def disconnect(self):
        """Disconnect from database"""
        pass

    @abstractmethod
    def execute_query(self, query):
        """Execute a query"""
        pass

    @abstractmethod
    def fetch_results(self):
        """Fetch query results"""
        pass

    def get_connection_info(self):
        """Concrete method - available to all subclasses"""
        return {
            'host': self.host,
            'port': self.port,
            'database': self.database,
            'connected': self.connected,
            'connection_time': self.connection_time
        }

    def __str__(self):
        return f"{self.__class__.__name__}({self.host}:{self.port}/{self.database})"

class MySQLConnection(DatabaseConnection):
    """MySQL database connection implementation"""

    def __init__(self, host, port, database, username, password):
        super().__init__(host, port, database)
        self.username = username
        self._password = password  # Private
        self.last_query = None
        self.results = []

    def connect(self):
        """Simulate MySQL connection"""
        print(f"Connecting to MySQL: {self.host}:{self.port}/{self.database}")
        print(f"Username: {self.username}")
        # Simulate connection
        self.connected = True
        self.connection_time = datetime.now()
        print("MySQL connection established!")
        return True

    def disconnect(self):
        """Disconnect from MySQL"""
        if self.connected:
            print("Disconnecting from MySQL...")
            self.connected = False
            self.connection_time = None
            print("Disconnected successfully!")
            return True
        else:
            print("Already disconnected")
            return False

    def execute_query(self, query):
        """Execute MySQL query"""
        if not self.connected:
            raise Exception("Not connected to database")

        print(f"Executing MySQL query: {query}")
        self.last_query = query

        # Simulate query execution
        if "SELECT" in query.upper():
            self.results = [
                {'id': 1, 'name': 'John Doe', 'email': 'john@example.com'},
                {'id': 2, 'name': 'Jane Smith', 'email': 'jane@example.com'}
            ]
        else:
            self.results = []

        return True

    def fetch_results(self):
        """Fetch MySQL results"""
        return self.results

class PostgreSQLConnection(DatabaseConnection):
    """PostgreSQL database connection implementation"""

    def __init__(self, host, port, database, username, password):
        super().__init__(host, port, database)
        self.username = username
        self._password = password
        self.last_query = None
        self.results = []

    def connect(self):
        """Simulate PostgreSQL connection"""
        print(f"Connecting to PostgreSQL: {self.host}:{self.port}/{self.database}")
        print(f"Username: {self.username}")
        # Simulate connection
        self.connected = True
        self.connection_time = datetime.now()
        print("PostgreSQL connection established!")
        return True

    def disconnect(self):
        """Disconnect from PostgreSQL"""
        if self.connected:
            print("Disconnecting from PostgreSQL...")
            self.connected = False
            self.connection_time = None
            print("Disconnected successfully!")
            return True
        else:
            print("Already disconnected")
            return False

    def execute_query(self, query):
        """Execute PostgreSQL query"""
        if not self.connected:
            raise Exception("Not connected to database")

        print(f"Executing PostgreSQL query: {query}")
        self.last_query = query

        # Simulate query execution
        if "SELECT" in query.upper():
            self.results = [
                {'id': 1, 'product': 'Laptop', 'price': 15000000},
                {'id': 2, 'product': 'Mouse', 'price': 150000}
            ]
        else:
            self.results = []

        return True

    def fetch_results(self):
        """Fetch PostgreSQL results"""
        return self.results

class MongoDBConnection(DatabaseConnection):
    """MongoDB connection implementation"""

    def __init__(self, host, port, database):
        super().__init__(host, port, database)
        self.collections = {}

    def connect(self):
        print(f"Connecting to MongoDB: {self.host}:{self.port}/{self.database}")
        self.connected = True
        self.connection_time = datetime.now()
        print("MongoDB connection established!")
        return True

    def disconnect(self):
        if self.connected:
            print("Disconnecting from MongoDB...")
            self.connected = False
            print("Disconnected successfully!")
            return True
        return False

    def execute_query(self, query):
        """Execute MongoDB query (simplified)"""
        if not self.connected:
            raise Exception("Not connected to database")

        print(f"Executing MongoDB query: {query}")
        # Simulate MongoDB query
        return True

    def fetch_results(self):
        """Fetch MongoDB results"""
        return [
            {'_id': '507f1f77bcf86cd799439011', 'name': 'Document 1'},
            {'_id': '507f1f77bcf86cd799439012', 'name': 'Document 2'}
        ]

# Database Manager - polymorphic usage
class DatabaseManager:
    """Manager untuk mengelola berbagai jenis database connections"""

    def __init__(self):
        self.connections = []

    def add_connection(self, connection):
        """Add a database connection (polymorphic parameter)"""
        if isinstance(connection, DatabaseConnection):
            self.connections.append(connection)
            print(f"Added connection: {connection}")
        else:
            raise TypeError("Connection must be a DatabaseConnection instance")

    def connect_all(self):
        """Connect to all databases"""
        print("\n" + "="*60)
        print("Connecting to all databases...")
        print("="*60)
        for conn in self.connections:
            conn.connect()
            print()

    def execute_on_all(self, query):
        """Execute query on all connected databases"""
        print("\n" + "="*60)
        print(f"Executing on all databases: {query}")
        print("="*60)
        for conn in self.connections:
            if conn.connected:
                print(f"\n{conn}:")
                conn.execute_query(query)
                results = conn.fetch_results()
                print(f"Results: {len(results)} rows")
                for row in results:
                    print(f"  {row}")

    def disconnect_all(self):
        """Disconnect from all databases"""
        print("\n" + "="*60)
        print("Disconnecting from all databases...")
        print("="*60)
        for conn in self.connections:
            conn.disconnect()
            print()

# Penggunaan
manager = DatabaseManager()

# Add different types of database connections
mysql_conn = MySQLConnection("localhost", 3306, "users_db", "root", "password123")
postgres_conn = PostgreSQLConnection("localhost", 5432, "products_db", "admin", "pass456")
mongo_conn = MongoDBConnection("localhost", 27017, "documents_db")

manager.add_connection(mysql_conn)
manager.add_connection(postgres_conn)
manager.add_connection(mongo_conn)

# Connect to all
manager.connect_all()

# Execute queries on all
manager.execute_on_all("SELECT * FROM table")

# Disconnect from all
manager.disconnect_all()

Abstract Static Methods & Class Methods

Kalian juga bisa membuat abstract static methods dan class methods:

abstract_class_methods.py
from abc import ABC, abstractmethod

class DataParser(ABC):
    """Abstract base class untuk data parser"""

    @abstractmethod
    def parse(self, data):
        """Instance method - parse data"""
        pass

    @staticmethod
    @abstractmethod
    def validate_format(data):
        """Static method - validate data format"""
        pass

    @classmethod
    @abstractmethod
    def from_file(cls, filename):
        """Class method - create parser from file"""
        pass

class JSONParser(DataParser):
    def __init__(self):
        self.parsed_data = None

    def parse(self, data):
        """Parse JSON data"""
        import json
        self.parsed_data = json.loads(data)
        return self.parsed_data

    @staticmethod
    def validate_format(data):
        """Validate JSON format"""
        import json
        try:
            json.loads(data)
            return True
        except json.JSONDecodeError:
            return False

    @classmethod
    def from_file(cls, filename):
        """Create JSONParser from file"""
        with open(filename, 'r') as f:
            data = f.read()
        parser = cls()
        parser.parse(data)
        return parser

class XMLParser(DataParser):
    def __init__(self):
        self.parsed_data = None

    def parse(self, data):
        """Parse XML data"""
        # Simplified - in reality would use xml.etree
        self.parsed_data = data
        return self.parsed_data

    @staticmethod
    def validate_format(data):
        """Validate XML format"""
        return data.strip().startswith('<') and data.strip().endswith('>')

    @classmethod
    def from_file(cls, filename):
        """Create XMLParser from file"""
        with open(filename, 'r') as f:
            data = f.read()
        parser = cls()
        parser.parse(data)
        return parser

# Penggunaan
json_data = '{"name": "John", "age": 30}'
xml_data = '<person><name>John</name><age>30</age></person>'

print("JSON valid:", JSONParser.validate_format(json_data))
print("XML valid:", XMLParser.validate_format(xml_data))

json_parser = JSONParser()
print("Parsed JSON:", json_parser.parse(json_data))

Praktik Terbaik

1. Use Abstract Classes untuk Shared Behavior

from abc import ABC, abstractmethod

class Report(ABC):
    """Base class untuk semua report"""

    def __init__(self, title):
        self.title = title
        self.data = []

    def add_data(self, item):
        """Concrete method - shared behavior"""
        self.data.append(item)

    @abstractmethod
    def generate(self):
        """Abstract method - must be implemented by subclass"""
        pass

    def export(self, filename):
        """Template method pattern"""
        content = self.generate()
        with open(filename, 'w') as f:
            f.write(content)
        return f"Report exported to {filename}"

2. Keep Abstract Classes Focused

# Good - focused interface
class Sortable(ABC):
    @abstractmethod
    def sort(self):
        pass

class Searchable(ABC):
    @abstractmethod
    def search(self, query):
        pass

# Better than one large abstract class with many methods

3. Document Abstract Methods

from abc import ABC, abstractmethod

class Plugin(ABC):
    @abstractmethod
    def initialize(self):
        """
        Initialize the plugin.

        This method should set up any necessary resources
        and prepare the plugin for use.

        Returns:
            bool: True if initialization successful
        """
        pass

Kapan Menggunakan Abstract Class?

Gunakan Abstract ClassJangan Gunakan
Ada shared code antar subclassTidak ada kode yang dibagikan
Ingin memaksa implementasi method tertentuHanya butuh dokumentasi
Membuat hierarchy yang kompleksCukup dengan duck typing
Ingin type checking yang ketatFlexibilitas lebih penting

Latihan

  1. Buat abstract class FileHandler dengan methods:

    • read() - abstract
    • write() - abstract
    • close() - concrete
    • Implementasikan TextFileHandler dan BinaryFileHandler
  2. Buat interface Serializable dengan methods:

    • to_json() - abstract
    • from_json() - abstract class method
    • Implementasikan untuk minimal 2 class berbeda
  3. Buat abstract class GameCharacter dengan:

    • Abstract properties: health, attack_power
    • Abstract methods: attack(), defend()
    • Concrete method: is_alive()
    • Implementasikan Warrior, Mage, Archer