This guide delves into advanced concepts within Object-Oriented Programming (OOP), providing a comprehensive understanding of design patterns, SOLID principles, inheritance, polymorphism, and abstract classes. Mastering these concepts is crucial for building robust, maintainable, and scalable software systems.
1. Introduction: The Pillars of OOP Refined
OOP is based on four core principles:
- Abstraction: Hiding complex implementation details and exposing only essential information to the user. This is typically achieved through interfaces and abstract classes.
- Encapsulation: Bundling data and methods that operate on that data within a single unit (a class). This controls access to the internal state of an object and prevents direct modification from outside the object.
- Inheritance: Creating new classes (derived or child classes) based on existing classes (base or parent classes). This promotes code reuse and establishes an “is-a” relationship.
- Polymorphism: The ability of objects of different classes to respond to the same method call in their own specific ways. This is crucial for writing flexible and extensible code.
This guide builds upon these fundamentals, exploring advanced techniques that leverage these principles.
2. Design Patterns: Reusable Solutions
Design patterns are proven solutions to commonly occurring software design problems. They are not concrete implementations, but rather templates or blueprints that can be adapted to specific situations. They promote code reusability, reduce complexity, and improve maintainability.
2.1. Creational Patterns
Creational patterns deal with object creation mechanisms, aiming to create objects in a manner suitable to the situation.
- Singleton: Ensures that a class has only one instance and provides a global point of access to that instance.
- Use Cases: Managing a database connection, logging, configuration settings.
- Example (Python):
class Singleton:
_instance = None
def __new__(cls, *args, **kwargs):
if not cls._instance:
cls._instance = super().__new__(cls, *args, **kwargs)
return cls._instance
def __init__(self): #Optional init, will run once.
if not hasattr(self, 'initialized'):
self.initialized = True
self.data = [] #Example Data
def add_data(self, item):
self.data.append(item)
Factory: Provides an interface for creating objects, but lets subclasses decide which class to instantiate.
- Use Cases: Creating objects based on configuration files, data from a database, or user input.
- Example (Python):
class Animal:
def make_sound(self):
pass
class Dog(Animal):
def make_sound(self):
return "Woof!"
class Cat(Animal):
def make_sound(self):
return "Meow!"
class AnimalFactory:
def create_animal(self, animal_type):
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()
else:
return None # or raise an exception
Abstract Factory: Provides an interface for creating families of related or dependent objects without specifying their concrete classes.
- Use Cases: Creating UI components for different operating systems (e.g., Windows, macOS).
- Example (Simplified Python, using the Animal Factory example):
# Extending our previous example...
class AnimalKingdomFactory:
def create_animal(self, animal_type): # This is the same method
if animal_type == "dog":
return Dog()
elif animal_type == "cat":
return Cat()
else:
return None
class Food: # Example Food
def __init__(self, name):
self.name = name
class FoodFactory:
def create_food(self, animal): #Creates Food based on the animal type
if isinstance(animal, Dog):
return Food("Dog Food")
elif isinstance(animal, Cat):
return Food("Cat Food")
else:
return None
Builder: Separates the construction of a complex object from its representation so that the same construction process can create different representations.
- Use Cases: Creating complex objects with many optional parameters.
- Example (Python):
class Pizza:
def __init__(self):
self.dough = None
self.sauce = None
self.toppings = []
def __str__(self):
return f"Pizza with {self.dough} dough, {self.sauce} sauce, and toppings: {', '.join(self.toppings)}"
class PizzaBuilder:
def __init__(self):
self.pizza = Pizza()
def set_dough(self, dough):
self.pizza.dough = dough
return self
def set_sauce(self, sauce):
self.pizza.sauce = sauce
return self
def add_topping(self, topping):
self.pizza.toppings.append(topping)
return self
def get_pizza(self):
return self.pizza
# Example Usage
builder = PizzaBuilder()
pizza = builder.set_dough("Thin crust").set_sauce("Tomato").add_topping("Pepperoni").add_topping("Cheese").get_pizza()
print(pizza) # Output: Pizza with Thin crust dough, Tomato sauce, and toppings: Pepperoni, Cheese
Prototype: Creates new objects by copying an existing object.
- Use Cases: When the cost of creating a new object from scratch is high.
- Example (Python):
import copy class Sheep: def __init__(self, name, color): self.name = name self.color = color def clone(self): return copy.deepcopy(self) # Use deepcopy for nested objects # Example usage original_sheep = Sheep("Dolly", "White") clone_sheep = original_sheep.clone() clone_sheep.name = "Dolly 2" print(original_sheep.name, clone_sheep.name) # Output: Dolly Dolly 2
2.2. Structural Patterns
Structural patterns deal with the composition of classes and objects to form larger structures.
- Adapter: Converts the interface of a class into another interface clients expect. Allows classes with incompatible interfaces to work together.
- Use Cases: Integrating legacy code, providing a consistent interface to different APIs.
- Example (Python):
class LegacySystem:
def legacy_method(self, data):
return f"Legacy: {data}"
class Adapter:
def __init__(self, legacy_system):
self.legacy_system = legacy_system
def new_method(self, data):
return self.legacy_system.legacy_method(data)
# Example usage
legacy = LegacySystem()
adapter = Adapter(legacy)
print(adapter.new_method("Hello from the new interface")) # Output: Legacy: Hello from the new interface
Bridge: Decouples an abstraction from its implementation so that the two can vary independently.
- Use Cases: Creating platform-independent code, separating concerns.
- Example (Python):
class DrawingAPI: # Abstraction of how to draw
def draw_circle(self, x, y, radius):
pass
class ConcreteDrawingAPI1(DrawingAPI): # Implementations
def draw_circle(self, x, y, radius):
print(f"API 1: Drawing circle at ({x}, {y}) with radius {radius}")
class ConcreteDrawingAPI2(DrawingAPI): # Another Implementation
def draw_circle(self, x, y, radius):
print(f"API 2: Drawing circle at ({x}, {y}) with radius {radius}")
class Circle: # Refined Abstraction using the drawing abstraction.
def __init__(self, x, y, radius, drawing_api):
self.x = x
self.y = y
self.radius = radius
self.drawing_api = drawing_api
def draw(self):
self.drawing_api.draw_circle(self.x, self.y, self.radius)
Composite: Composes objects into tree structures to represent part-whole hierarchies. Allows clients to treat individual objects and compositions of objects uniformly.
- Use Cases: Representing file systems, organizational charts, graphical user interfaces.
- Example (Python):
class Component:
def operation(self):
pass
class Leaf(Component):
def __init__(self, name):
self.name = name
def operation(self):
return f"Leaf: {self.name}"
class Composite(Component):
def __init__(self, name):
self.name = name
self.children = []
def add(self, component):
self.children.append(component)
def remove(self, component):
self.children.remove(component)
def operation(self):
results = [f"Composite: {self.name}"]
for child in self.children:
results.append(child.operation())
return "\n".join(results)
# Example Usage
leaf1 = Leaf("Leaf 1")
leaf2 = Leaf("Leaf 2")
composite1 = Composite("Composite 1")
composite1.add(leaf1)
composite1.add(leaf2)
leaf3 = Leaf("Leaf 3")
composite2 = Composite("Composite 2")
composite2.add(composite1)
composite2.add(leaf3)
print(composite2.operation())
Decorator: Attaches additional responsibilities to an object dynamically. Provides a flexible alternative to subclassing for extending functionality.
- Use Cases: Adding features to UI components, adding logging or security features.
- Example (Python):
class Component:
def operation(self):
pass
class ConcreteComponent(Component):
def operation(self):
return "ConcreteComponent"
class Decorator(Component):
def __init__(self, component):
self._component = component
def operation(self):
return self._component.operation()
class ConcreteDecoratorA(Decorator):
def operation(self):
return f"ConcreteDecoratorA({super().operation()})"
class ConcreteDecoratorB(Decorator):
def operation(self):
return f"ConcreteDecoratorB({super().operation()})"
Facade: Provides a simplified interface to a complex subsystem.
- Use Cases: Simplifying interactions with complex APIs, hiding the complexities of a subsystem.
- Example (Python):
class Subsystem1:
def operation1(self):
return "Subsystem1: Operation 1"
class Subsystem2:
def operation2(self):
return "Subsystem2: Operation 2"
class Facade:
def __init__(self):
self.subsystem1 = Subsystem1()
self.subsystem2 = Subsystem2()
def do_something(self):
result = self.subsystem1.operation1()
result += "\n" + self.subsystem2.operation2()
return result
# Example usage
facade = Facade()
print(facade.do_something())
Flyweight: Uses sharing to support large numbers of fine-grained objects efficiently.
- Use Cases: Managing a large number of similar objects (e.g., characters in a word processor).
- Example (Python):
class Flyweight:
def __init__(self, intrinsic_state):
self.intrinsic_state = intrinsic_state # Shared state
def operation(self, extrinsic_state):
return f"Flyweight: {self.intrinsic_state}, {extrinsic_state}"
class FlyweightFactory:
def __init__(self):
self.flyweights = {}
def get_flyweight(self, intrinsic_state):
if intrinsic_state not in self.flyweights:
self.flyweights[intrinsic_state] = Flyweight(intrinsic_state)
return self.flyweights[intrinsic_state]
# Example usage
factory = FlyweightFactory()
flyweight1 = factory.get_flyweight("state1")
flyweight2 = factory.get_flyweight("state2")
flyweight3 = factory.get_flyweight("state1") # Reuse
print(flyweight1.operation("extrinsic1"))
print(flyweight2.operation("extrinsic2"))
print(flyweight3 is flyweight1) #True - reused
Proxy: Provides a surrogate or placeholder for another object to control access to it.
- Use Cases: Controlling access to expensive resources, implementing lazy initialization, adding security checks.
- Example (Python):
class RealSubject: def request(self): return "RealSubject: Handling request" class Proxy: def __init__(self, real_subject): self._real_subject = real_subject def request(self): # Add some checks, logging, etc. before or after calling the real subject print("Proxy: Performing some checks...") result = self._real_subject.request() print("Proxy: Done.") return result # Example usage real = RealSubject() proxy = Proxy(real) print(proxy.request())
2.3. Behavioral Patterns
Behavioral patterns are concerned with algorithms and the assignment of responsibilities between objects.
- Chain of Responsibility: Avoids coupling the sender of a request to its receiver by giving multiple objects a chance to handle the request. The request is passed along a chain until an object handles it.
- Use Cases: Handling events, processing requests in a chain, implementing event handlers.
- Example (Python):
class Handler:
def __init__(self, successor=None):
self.successor = successor
def handle(self, request):
if self.can_handle(request):
return self.process(request)
elif self.successor:
return self.successor.handle(request)
else:
return "No handler can process the request"
def can_handle(self, request):
pass # To be implemented by subclasses
def process(self, request):
pass # To be implemented by subclasses
class ConcreteHandler1(Handler):
def can_handle(self, request):
return request == "request1"
def process(self, request):
return "ConcreteHandler1: Processed request1"
class ConcreteHandler2(Handler):
def can_handle(self, request):
return request == "request2"
def process(self, request):
return "ConcreteHandler2: Processed request2"
# Example Usage
handler1 = ConcreteHandler1()
handler2 = ConcreteHandler2(handler1) # Chain
print(handler2.handle("request2")) # Output: ConcreteHandler2: Processed request2
print(handler2.handle("request1")) # Output: ConcreteHandler1: Processed request1
print(handler2.handle("request3")) # Output: No handler can process the request
Command: Encapsulates a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.
- Use Cases: Implementing undo/redo functionality, queuing commands, creating macros.
- Example (Python):
class Command:
def execute(self):
pass
class Receiver:
def action(self):
return "Receiver: Action performed"
class ConcreteCommand(Command):
def __init__(self, receiver):
self.receiver = receiver
def execute(self):
return self.receiver.action()
class Invoker:
def __init__(self):
self._command = None
def set_command(self, command):
self._command = command
def execute_command(self):
if self._command:
return self._command.execute()
return "No command set"
# Example usage:
receiver = Receiver()
command = ConcreteCommand(receiver)
invoker = Invoker()
invoker.set_command(command)
print(invoker.execute_command())
Interpreter: Given a language, define a representational grammar for its elements and provide an interpreter to deal with this grammar.
- Use Cases: Building a simple programming language interpreter, parsing mathematical expressions.
- Example (Python):
class Expression:
def interpret(self, context):
pass
class TerminalExpression(Expression):
def __init__(self, data):
self.data = data
def interpret(self, context):
return context.get(self.data)
class NonTerminalExpression(Expression):
def __init__(self, expr1, expr2, operation):
self.expr1 = expr1
self.expr2 = expr2
self.operation = operation
def interpret(self, context):
val1 = self.expr1.interpret(context)
val2 = self.expr2.interpret(context)
if self.operation == "+":
return val1 + val2
# Other operations can be implemented here
# Example usage:
context = {"a": 10, "b": 20}
expression = NonTerminalExpression(TerminalExpression("a"), TerminalExpression("b"), "+")
result = expression.interpret(context)
print(result) # Output: 30
Iterator: Provides a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
- Use Cases: Traversing collections, data structures, and streams.
- Example (Python, using built-in iterators):
class Aggregate:
def __init__(self, items):
self.items = items
def __iter__(self): #Implementing Iterator Protocol
self.index = 0 # Reset the index
return self
def __next__(self): #Implementing Iterator Protocol
if self.index < len(self.items):
item = self.items[self.index]
self.index += 1
return item
else:
raise StopIteration # End the iteration
# Example Usage
aggregate = Aggregate(["item1", "item2", "item3"])
for item in aggregate:
print(item)
Mediator: Defines an object that encapsulates how a set of objects interact. Mediator promotes loose coupling by keeping objects from referring to each other explicitly, and it lets you vary their interaction independently.
- Use Cases: Managing interactions between components in a GUI, coordinating communication between modules in a system.
- Example (Python):
class Mediator:
def __init__(self):
self.colleagues = []
def add_colleague(self, colleague):
self.colleagues.append(colleague)
def send(self, message, sender):
for colleague in self.colleagues:
if colleague != sender:
colleague.receive(message)
class Colleague:
def __init__(self, mediator):
self.mediator = mediator
def send(self, message):
self.mediator.send(message, self)
def receive(self, message):
pass # To be implemented by subclasses
class ConcreteColleague1(Colleague):
def receive(self, message):
print(f"ConcreteColleague1 received: {message}")
class ConcreteColleague2(Colleague):
def receive(self, message):
print(f"ConcreteColleague2 received: {message}")
# Example Usage
mediator = Mediator()
colleague1 = ConcreteColleague1(mediator)
colleague2 = ConcreteColleague2(mediator)
mediator.add_colleague(colleague1)
mediator.add_colleague(colleague2)
colleague1.send("Hello from Colleague 1") # Output: ConcreteColleague2 received: Hello from Colleague 1
Memento: Without violating encapsulation, capture and externalize an object’s internal state so that the object can be restored to this state later.
- Use Cases: Implementing undo/redo functionality, storing snapshots of an object’s state.
- Example (Python):
import copy
class Memento:
def __init__(self, state):
self._state = copy.deepcopy(state) # Store a copy
def get_state(self):
return self._state
class Originator:
def __init__(self, state="Initial State"):
self._state = state
def get_state(self):
return self._state
def set_state(self, state):
self._state = state
def create_memento(self):
return Memento(self._state)
def restore_memento(self, memento):
self._state = memento.get_state()
class Caretaker:
def __init__(self):
self._mementos = []
def add_memento(self, memento):
self._mementos.append(memento)
def get_memento(self, index):
return self._mementos[index]
# Example Usage
originator = Originator()
caretaker = Caretaker()
caretaker.add_memento(originator.create_memento()) # Save Initial
originator.set_state("State 1")
caretaker.add_memento(originator.create_memento()) # Save State 1
originator.set_state("State 2")
caretaker.add_memento(originator.create_memento()) # Save State 2
print("Current state:", originator.get_state()) # Output: Current state: State 2
originator.restore_memento(caretaker.get_memento(1)) # Restore to state 1
print("Restored state:", originator.get_state()) # Output: Restored state: State 1
Observer: Defines a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.
- Use Cases: Implementing event handling, updating views in a GUI.
- Example (Python):
class Subject:
def __init__(self):
self._observers = []
self._state = None
def attach(self, observer):
self._observers.append(observer)
def detach(self, observer):
self._observers.remove(observer)
def notify(self):
for observer in self._observers:
observer.update(self) #Pass the subject itself
@property
def state(self):
return self._state
@state.setter
def state(self, value):
self._state = value
self.notify()
class Observer:
def update(self, subject):
pass #To be implemented by Subclasses.
class ConcreteObserverA(Observer):
def update(self, subject):
print(f"ConcreteObserverA: Notified with state = {subject.state}")
class ConcreteObserverB(Observer):
def update(self, subject):
print(f"ConcreteObserverB: Notified with state = {subject.state}")
# Example Usage
subject = Subject()
observerA = ConcreteObserverA()
observerB = ConcreteObserverB()
subject.attach(observerA)
subject.attach(observerB)
subject.state = "State 1" # Observer is notified.
subject.detach(observerA)
subject.state = "State 2" # Only Observer B is Notified
State: Allows an object to alter its behavior when its internal state changes. The object will appear to change its class.
- Use Cases: Implementing state machines, managing object behavior based on its state.
- Example (Python):
class State:
def handle(self, context):
pass #To be implemented by subclasses
class ConcreteStateA(State):
def handle(self, context):
print("ConcreteStateA: Handling request")
context.state = ConcreteStateB() # Transition to another state
class ConcreteStateB(State):
def handle(self, context):
print("ConcreteStateB: Handling request")
context.state = ConcreteStateA() # Transition back to a previous state
class Context:
def __init__(self):
self.state = ConcreteStateA() # Initial State
def request(self):
self.state.handle(self)
# Example usage
context = Context()
context.request() #ConcreteStateA: Handling request
context.request() #ConcreteStateB: Handling request
context.request() #ConcreteStateA: Handling request
Strategy: Defines a family of algorithms, encapsulates each one, and makes them interchangeable. Lets the algorithm vary independently from the clients that use it.
- Use Cases: Implementing different sorting algorithms, choosing different validation strategies.
- Example (Python):
class Strategy:
def execute(self, data):
pass # To be implemented by subclasses
class ConcreteStrategyA(Strategy):
def execute(self, data):
return f"Strategy A: {data}"
class ConcreteStrategyB(Strategy):
def execute(self, data):
return f"Strategy B: {data}"
class Context:
def __init__(self, strategy):
self._strategy = strategy
@property
def strategy(self):
return self._strategy
@strategy.setter
def strategy(self, strategy):
self._strategy = strategy
def execute_strategy(self, data):
return self._strategy.execute(data)
# Example usage
strategy_a = ConcreteStrategyA()
context = Context(strategy_a)
print(context.execute_strategy("data1")) # Output: Strategy A: data1
strategy_b = ConcreteStrategyB()
context.strategy = strategy_b # Dynamically switch strategy
print(context.execute_strategy("data2")) # Output: Strategy B: data2
Template Method: Defines the skeleton of an algorithm in an operation, deferring some steps to subclasses. Template Method lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.
- Use Cases: Implementing common algorithms with customizable steps.
- Example (Python):
class AbstractClass:
def template_method(self):
self.primitive_operation1()
self.primitive_operation2()
self.concrete_operation() # A hook that might be optional
def primitive_operation1(self):
print("AbstractClass: Performing primitive operation 1")
def primitive_operation2(self):
print("AbstractClass: Performing primitive operation 2")
def concrete_operation(self):
print("AbstractClass: concrete_operation (default implementation)")
class ConcreteClass(AbstractClass):
def primitive_operation2(self): # Override
print("ConcreteClass: Performing primitive operation 2 (modified)")
def concrete_operation(self): # Optional override
print("ConcreteClass: Performing concrete operation (overridden)")
# Example usage:
concrete = ConcreteClass()
concrete.template_method()
Visitor: Represents an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
- Use Cases: Applying different operations to elements of a data structure, implementing data processing tasks.
- Example (Python):
class Element: def accept(self, visitor): pass # To be implemented by subclasses class ConcreteElementA(Element): def accept(self, visitor): visitor.visit_concrete_element_a(self) def operation_a(self): return "ConcreteElementA: Operation A" class ConcreteElementB(Element): def accept(self, visitor): visitor.visit_concrete_element_b(self) def operation_b(self): return "ConcreteElementB: Operation B" class Visitor: def visit_concrete_element_a(self, element): pass # To be implemented by subclasses def visit_concrete_element_b(self, element): pass # To be implemented by subclasses class ConcreteVisitor1(Visitor): def visit_concrete_element_a(self, element): print(f"ConcreteVisitor1: {element.operation_a()}") def visit_concrete_element_b(self, element): print(f"ConcreteVisitor1: {element.operation_b()}") class ConcreteVisitor2(Visitor): def visit_concrete_element_a(self, element): print(f"ConcreteVisitor2: {element.operation_a()}") def visit_concrete_element_b(self, element): print(f"ConcreteVisitor2: {element.operation_b()}") # Example Usage element_a = ConcreteElementA() element_b = ConcreteElementB() visitor1 = ConcreteVisitor1() visitor2 = ConcreteVisitor2() element_a.accept(visitor1) # Output: ConcreteVisitor1: ConcreteElementA: Operation A element_b.accept(visitor1) # Output: ConcreteVisitor1: ConcreteElementB: Operation B element_a.accept(visitor2) # Output: ConcreteVisitor2: ConcreteElementA: Operation A element_b.accept(visitor2) # Output: ConcreteVisitor2: ConcreteElementB: Operation B
3. SOLID Principles: Building Maintainable Code
SOLID is an acronym representing five design principles intended to make software designs more understandable, flexible, and maintainable. These principles are fundamental to good object-oriented design.
- S – Single Responsibility Principle: A class should have one, and only one, reason to change. Each class should have a single, well-defined responsibility.
- Benefits: Reduces complexity, makes classes easier to understand and test.
- Example (Violating): A
User
class that handles both user data and database interactions. - Example (Applying): Separate the user data (e.g., name, email) in a
UserData
class and the database interactions (e.g., saving, retrieving) in aUserDatabaseRepository
class.
- O – Open/Closed Principle: Software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. You should be able to add new functionality without changing existing code.
- Benefits: Reduces the risk of introducing bugs when adding new features.
- Example (Violating): A function that uses
if/else
statements to handle different types of objects. - Example (Applying): Use inheritance or interfaces to allow new types to be added without modifying the original function. (Polymorphism is key here).
- L – Liskov Substitution Principle: Subtypes must be substitutable for their base types without altering the correctness of the program. If you have a class
A
, and classB
inherits fromA
, then you should be able to useB
anywhere you can useA
without unexpected behavior.- Benefits: Ensures that inheritance is used correctly and that subclasses behave as expected.
- Example (Violating): Having a
Bird
class and then inheriting from it with aPenguin
class, where thePenguin
class overrides thefly()
method and throws an exception. This violates the principle, as aPenguin
is not a perfect substitute for aBird
. - Example (Applying): Design the base class carefully to ensure that all subclasses can provide meaningful implementations of the base class’s methods.
- I – Interface Segregation Principle: Clients should not be forced to depend on methods they do not use. Avoid large interfaces; break them down into smaller, more specific interfaces.
- Benefits: Makes interfaces more focused and easier to implement.
- Example (Violating): A large interface that contains methods that only some classes need to implement.
- Example (Applying): Create multiple, smaller interfaces that address specific needs. Classes should implement only the interfaces that are relevant to them.
- D – Dependency Inversion Principle: High-level modules should not depend on low-level modules. Both should depend on abstractions. Abstractions should not depend on details. Details should depend on abstractions.
- Benefits: Reduces coupling between modules, making them more flexible and easier to test.
- Example (Violating): A high-level class directly creates instances of low-level classes.
- Example (Applying): Use interfaces or abstract classes to define the dependencies, and inject the concrete implementations into the high-level classes (Dependency Injection).
4. Inheritance
Inheritance is a powerful mechanism for code reuse and establishing relationships between classes. It allows a class (the subclass or derived class) to inherit properties and methods from another class (the superclass or base class).
- Types of Inheritance:
- Single Inheritance: A class inherits from only one base class.
- Multiple Inheritance: A class inherits from multiple base classes (supported by some languages like Python, C++ but not Java, C#). This can lead to complexity and the “diamond problem” (ambiguity in method resolution).
- Multilevel Inheritance: A class inherits from a class that itself inherits from another class.
- Access Modifiers: (Language specific) Control the visibility of members (attributes and methods):
- Public: Accessible from anywhere.
- Protected: Accessible within the class and its subclasses.
- Private: Accessible only within the class.
- Method Overriding: Subclasses can provide their own implementation of methods inherited from the base class.
super()
Keyword (or similar): Used to call the superclass’s methods (e.g., to initialize inherited attributes or extend functionality).
Залишити відповідь