Introduction to Object Oriented Design
In the world of programming, object-oriented design (OOD) is a paradigm that aims to structure code in a way that mimics real-world objects and their interactions. At its core, OOD is all about modelling real-world entities as classes and creating instances of those classes, known as objects.
Key Concepts of Object-Oriented Design
To understand OOD, it's important to familiarize yourself with some key concepts:
1. Classes: Classes in OOD are used to define the blueprint or template for creating objects. They encapsulate data, properties (also known as attributes or fields), and behavior (also known as methods) for those objects.
1// Example of a Car class
2public class Car {
3 private String make;
4 private String model;
5 private int year;
6
7 // constructor
8 public Car(String make, String model, int year) {
9 this.make = make;
10 this.model = model;
11 this.year = year;
12 }
13
14 // method
15 public void drive() {
16 System.out.println("Driving the " + make + " " + model + "...");
17 }
18}
2. Objects: Objects are the instances of a class. They hold the data and have the ability to perform operations defined in the class. In the example above, myCar
is an object of the Car
class.
3. Encapsulation: Encapsulation is the practice of bundling the data and methods related to an object into a single unit. It allows for data hiding by making the internal state of an object private and providing controlled access through getters and setters.
4. Inheritance: Inheritance allows for the creation of hierarchical relationships between classes. A class can inherit properties and behavior from another class, known as the parent class or superclass. This promotes code reuse and modularity.
5. Polymorphism: Polymorphism is the ability of an object to take on different forms or behave differently based on the context. It allows objects of different classes to be treated as the same type. Polymorphism is achieved through method overriding and method overloading.
Benefits of Object-Oriented Design
The principles of OOD bring several benefits, including:
Modularity and Reusability: By organizing code into classes and objects, OOD promotes modularity, making it easier to maintain and update code. Reusable code components can be easily leveraged across different projects.
Abstraction and Encapsulation: OOD allows for the abstraction of complex systems by focusing on essential features and hiding unnecessary details. Encapsulation ensures that code is easily understandable and changes can be made without impacting other parts of the system.
Code Organization and Scalability: OOD provides a structured approach to code organization, making it more manageable, readable, and scalable. It allows teams to work collaboratively by defining clear boundaries between code modules.
Summary
Object-oriented design is a powerful paradigm that helps in designing and organizing code by mimicking real-world objects and their interactions. By using classes, objects, encapsulation, inheritance, and polymorphism, developers can create modular, reusable, and scalable software solutions. Let's take a look at a sample Java code snippet that demonstrates the use of object-oriented design principles:
1public class Main {
2 public static void main(String[] args) {
3 Car myCar = new Car("Toyota", "Camry", 2021);
4 myCar.drive();
5 }
6}
xxxxxxxxxx
// Object-Oriented Programming in Java
public class Car {
private String make;
private String model;
private int year;
public Car(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}
public void drive() {
System.out.println("Driving the " + make + " " + model + "...");
}
}
public class Main {
public static void main(String[] args) {
Car myCar = new Car("Toyota", "Camry", 2021);
myCar.drive();
}
}
Build your intuition. Click the correct answer from the options.
Which of the following is NOT a benefit of object-oriented design?
Click the option that best answers the question.
- Modularity and Reusability
- Abstraction and Encapsulation
- Code Obfuscation
- Code Organization and Scalability
Class and Object
In object-oriented design, a class is a blueprint or template for creating objects. It defines the structure and behavior that the objects of that class will have.
Let's take an example of a Car
class:
1public class Car {
2 private String make;
3 private String model;
4 private int year;
5
6 public Car(String make, String model, int year) {
7 this.make = make;
8 this.model = model;
9 this.year = year;
10 }
11
12 public void drive() {
13 System.out.println("Driving the " + make + " " + model + "...");
14 }
15}
In this example, the Car
class has three attributes: make
, model
, and year
. These attributes represent the characteristics of a car. Additionally, the class has a method drive()
, which defines the behavior of a car.
An object is an instance of a class. It represents a specific real-world entity or a concept. For example, we can create an object myCar
from the Car
class:
1Car myCar = new Car("Toyota", "Camry", 2021);
In this case, myCar
is an object of the Car
class, which has its own set of attribute values. We can call the drive()
method on myCar
to perform the specific behavior defined in the class.
1myCar.drive();
The output of the above code will be:
1Driving the Toyota Camry...
In summary, a class serves as a blueprint for creating objects, while objects are instances of a class that have their own attribute values and can perform specific behaviors defined in the class.
xxxxxxxxxx
public class Car {
private String make;
private String model;
private int year;
public Car(String make, String model, int year) {
this.make = make;
this.model = model;
this.year = year;
}
public void drive() {
System.out.println("Driving the " + make + " " + model + "...");
}
}
Build your intuition. Fill in the missing part by typing it in.
A class is a ___ or ___ for creating objects.
Write the missing line below.
Inheritance
In object-oriented design, inheritance is a mechanism that allows one class to inherit the properties and behaviors of another class. The class that inherits is called the child class or subclass, and the class being inherited from is called the parent class or superclass.
Inheritance facilitates code reuse and maintainability by allowing us to define common attributes and methods in a parent class and have them automatically available in the child class.
Let's consider the example of a Vehicle
class and a Car
class that inherits from it:
1class Vehicle {
2 protected String brand;
3 protected int year;
4
5 public Vehicle(String brand, int year) {
6 this.brand = brand;
7 this.year = year;
8 }
9
10 public void start() {
11 System.out.println("Starting the vehicle...");
12 }
13}
14
15class Car extends Vehicle {
16 private int numOfDoors;
17
18 public Car(String brand, int year, int numOfDoors) {
19 super(brand, year);
20 this.numOfDoors = numOfDoors;
21 }
22
23 public void drive() {
24 System.out.println("Driving the car...");
25 }
26}
27
28public class Main {
29 public static void main(String[] args) {
30 Car myCar = new Car("Toyota", 2021, 4);
31 myCar.start();
32 myCar.drive();
33 }
34}
In this example, the Vehicle
class serves as the parent class, and the Car
class inherits from it. The Car
class extends the Vehicle
class using the extends
keyword.
The Vehicle
class has properties such as brand
and year
, as well as a method start()
.
The Car
class adds an additional property numOfDoors
and a method drive()
. It can also access the properties and methods defined in the Vehicle
class using the super
keyword.
By creating an instance of the Car
class and calling its methods, we can see the inheritance in action. The output of the above code will be:
1Starting the vehicle...
2Driving the car...
In summary, inheritance allows classes to inherit properties and behaviors from other classes, facilitating code reuse and maintainability.
xxxxxxxxxx
}
class Vehicle {
protected String brand;
protected int year;
public Vehicle(String brand, int year) {
this.brand = brand;
this.year = year;
}
public void start() {
System.out.println("Starting the vehicle...");
}
}
class Car extends Vehicle {
private int numOfDoors;
public Car(String brand, int year, int numOfDoors) {
super(brand, year);
this.numOfDoors = numOfDoors;
}
public void drive() {
System.out.println("Driving the car...");
}
}
class Main {
public static void main(String[] args) {
Try this exercise. Is this statement true or false?
Inheritance allows a child class to inherit properties and methods from a parent class.
Press true if you believe the statement is correct, or false otherwise.
Polymorphism
In object-oriented design, polymorphism refers to the ability of objects of different classes to be treated as the same type. This allows for flexibility and extensibility in your code.
Let's consider an example with animals. Suppose we have a base Animal
class and two subclasses, Dog
and Cat
. Each class has its own makeSound()
method.
1class Animal {
2 public void makeSound() {
3 System.out.println("The animal makes a sound");
4 }
5}
6
7class Dog extends Animal {
8 public void makeSound() {
9 System.out.println("The dog barks");
10 }
11}
12
13class Cat extends Animal {
14 public void makeSound() {
15 System.out.println("The cat meows");
16 }
17}
18
19public class Main {
20 public static void main(String[] args) {
21 Animal animal1 = new Dog();
22 Animal animal2 = new Cat();
23
24 animal1.makeSound();
25 animal2.makeSound();
26 }
27}
In this example, we create objects of type Dog
and Cat
and assign them to variables of type Animal
. We can then call the makeSound()
method on these variables.
The output of the above code will be:
1The dog barks
2The cat meows
Even though the variables animal1
and animal2
are of type Animal
, the actual objects they refer to are Dog
and Cat
, respectively. The makeSound()
method is invoked based on the actual type of the object.
Polymorphism is particularly useful when working with collections of objects. For example, you can have an array or a list of Animal
objects that can contain instances of different subclasses.
By treating all the objects as Animal
, you can perform common operations on them without worrying about their specific types.
In summary, polymorphism in object-oriented design allows objects of different classes to be treated as the same type, providing flexibility and extensibility in your code.
xxxxxxxxxx
class Animal {
public void makeSound() {
System.out.println("The animal makes a sound");
}
}
class Dog extends Animal {
public void makeSound() {
System.out.println("The dog barks");
}
}
class Cat extends Animal {
public void makeSound() {
System.out.println("The cat meows");
}
}
public class Main {
public static void main(String[] args) {
Animal animal1 = new Dog();
Animal animal2 = new Cat();
animal1.makeSound();
animal2.makeSound();
}
}
Try this exercise. Fill in the missing part by typing it in.
In object-oriented design, ___ refers to the ability of objects of different classes to be treated as the same type.
Write the missing line below.
Encapsulation
In object-oriented design, encapsulation is the concept of hiding the internal state of an object and providing public methods to access and modify that state. It allows us to control the access to the data and ensure that it is used in a consistent and valid way.
Encapsulation is important for several reasons:
- Data Protection: By encapsulating the data within an object, we can protect it from being accessed or modified directly by external code. This helps maintain the integrity and consistency of the data.
- Modularity: Encapsulation allows us to divide our code into smaller modules, making it easier to read, understand, and maintain. Each object encapsulates its own data and behaviors, making it self-contained and reusable in different contexts.
- Flexibility: Encapsulated objects can have hidden implementation details. This means that we can change the internal implementation without affecting the code that uses the object. This provides flexibility and reduces the impact of changes.
Let's take an example to understand encapsulation in Java:
1public class Person {
2
3 private String name;
4 private int age;
5
6 public Person(String name, int age) {
7 this.name = name;
8 this.age = age;
9 }
10
11 public String getName() {
12 return name;
13 }
14
15 public void setName(String name) {
16 this.name = name;
17 }
18
19 public int getAge() {
20 return age;
21 }
22
23 public void setAge(int age) {
24 this.age = age;
25 }
26
27 public void printInformation() {
28 System.out.println("Name: " + name);
29 System.out.println("Age: " + age);
30 }
31
32 public static void main(String[] args) {
33 Person person = new Person("John", 25);
34 person.printInformation();
35
36 person.setName("Jane");
37 person.setAge(30);
38 person.printInformation();
39 }
40}
In this example, we have a Person
class with private instance variables name
and age
. These variables are encapsulated within the class, meaning that they cannot be accessed directly from outside the class.
To access and modify the name
and age
, we provide getter and setter methods. The getter methods (getName()
and getAge()
) allow other code to retrieve the values, while the setter methods (setName()
and setAge()
) provide a way to update the values.
The printInformation()
method is a public method that can be used to print the details of the person. It accesses the encapsulated data through the getter methods.
By encapsulating the data and providing controlled access through getter and setter methods, we ensure that the person's information is accessed and modified in a controlled and consistent manner.
Encapsulation is a fundamental principle of object-oriented design and plays a crucial role in creating well-designed, modular, and maintainable code.
xxxxxxxxxx
}
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public void printInformation() {
System.out.println("Name: " + name);
System.out.println("Age: " + age);
Try this exercise. Fill in the missing part by typing it in.
Encapsulation is the concept of hiding the _ of an object and providing public methods to access and modify that state.
Write the missing line below.
Abstraction
In object-oriented design, abstraction is the process of simplifying complex systems by breaking them down into smaller, more manageable units. It involves focusing on the essential features and behaviors of an object or system while hiding the irrelevant or implementation-specific details.
Abstraction helps in managing complexity by providing a high-level view of a system and allowing us to work with conceptual models rather than dealing with low-level implementation details. It enables us to focus on what an object does rather than how it does it.
Abstraction is achieved in object-oriented programming through the use of abstract classes and interfaces.
Abstract Classes
An abstract class is a class that cannot be instantiated and is meant to be subclassed. It serves as a blueprint for creating other classes and defines common attributes and behaviors that those classes should have.
Abstract classes can have both abstract and non-abstract methods. An abstract method is declared without an implementation and must be implemented by any concrete subclass.
Here's an example of an abstract class Animal
:
1public abstract class Animal {
2
3 protected String name;
4
5 public Animal(String name) {
6 this.name = name;
7 }
8
9 public abstract void makeSound();
10
11 public abstract void eat();
12
13}
In this example, the Animal
class is abstract and has an abstract method makeSound()
and eat()
. Any subclass of Animal
must provide an implementation for these methods.
Interfaces
An interface is a contract that defines a set of methods that a class must implement. It specifies the signature of the methods but not their implementation.
An interface can be implemented by multiple classes, allowing for polymorphism and achieving loose coupling between classes.
Here's an example of an interface Drawable
:
1public interface Drawable {
2
3 void draw();
4
5 void erase();
6
7}
In this example, the Drawable
interface defines two methods draw()
and erase()
. Any class that implements the Drawable
interface must provide an implementation for these methods.
Abstraction is a powerful concept in object-oriented design as it allows for creating flexible, reusable, and maintainable code. By abstracting away the implementation details, we can focus on the essential behaviors and interactions between objects. Abstraction helps in managing complexity and promotes code modularity and extensibility.
Let's take an example to understand abstraction in Java:
1public class Dog extends Animal {
2
3 public Dog(String name) {
4 super(name);
5 }
6
7 @Override
8 public void makeSound() {
9 System.out.println(name + " says: Woof!");
10 }
11
12 @Override
13 public void eat() {
14 System.out.println(name + " is eating.");
15 }
16
17 public static void main(String[] args) {
18 Dog dog = new Dog("Buddy");
19 dog.makeSound();
20 dog.eat();
21 }
22}
In this example, we have a Dog
class that extends the Animal
abstract class. The Dog
class provides an implementation for the abstract methods makeSound()
and eat()
. The makeSound()
method prints the sound the dog makes, and the eat()
method indicates that the dog is eating.
By using abstraction, we can create different subclasses of Animal
with specific behaviors while relying on the common attributes and behaviors defined in the abstract class.
Abstraction is a fundamental concept in object-oriented design and plays a vital role in creating modular, extensible, and maintainable code.
xxxxxxxxxx
}
// Example of abstraction in Java
public abstract class Animal {
protected String name;
public Animal(String name) {
this.name = name;
}
public abstract void makeSound();
public abstract void eat();
}
public class Dog extends Animal {
public Dog(String name) {
super(name);
}
public void makeSound() {
System.out.println(name + " says: Woof!");
}
public void eat() {
Let's test your knowledge. Fill in the missing part by typing it in.
Abstraction helps in managing complexity by providing a high-level view of a system and allowing us to work with conceptual models rather than dealing with low-level ___ details.
Write the missing line below.
Association, Aggregation, and Composition
In object-oriented design, classes can have different types of relationships with each other. Three common types of relationships are association, aggregation, and composition.
Association
Association represents a relationship between two classes where each class has an independent existence and there is no ownership between them. It can be a one-to-one, one-to-many, or many-to-many relationship.
For example, let's consider the relationship between a Person
class and a Company
class. Multiple persons can be associated with a company, and a person can be associated with multiple companies. In this case, the Person
and Company
classes are associated with each other.
To illustrate this association in Java, we can have a Person
class with a name
attribute and a Company
class with a list of employees
. The Company
class can have methods to hire employees and print the names of the employees:
1```java
2class Person {
3
4 private String name;
5
6 public Person(String name) {
7 this.name = name;
8 }
9
10 public String getName() {
11 return name;
12 }
13}
14
15class Company {
16
17 private String name;
18 private List<Person> employees;
19
20 public Company(String name) {
21 this.name = name;
22 this.employees = new ArrayList<>();
23 }
24
25 public void hireEmployee(Person person) {
26 employees.add(person);
27 }
28
29 public void printEmployees() {
30 for (Person employee : employees) {
31 System.out.println(employee.getName());
32 }
33 }
34}
35
36public class Main {
37 public static void main(String[] args) {
38 Person person1 = new Person("Alice");
39 Person person2 = new Person("Bob");
40 Person person3 = new Person("Charlie");
41
42 Company company = new Company("ABC Corp");
43 company.hireEmployee(person1);
44 company.hireEmployee(person2);
45 company.hireEmployee(person3);
46
47 company.printEmployees();
48 }
49}
xxxxxxxxxx
}
class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Company {
private String name;
private List<Person> employees;
public Company(String name) {
this.name = name;
this.employees = new ArrayList<>();
}
public void hireEmployee(Person person) {
employees.add(person);
}
public void printEmployees() {
for (Person employee : employees) {
Build your intuition. Click the correct answer from the options.
Which statement correctly describes the association relationship in object-oriented design?
Click the option that best answers the question.
- Association represents a strong ownership relationship between two classes.
- Association represents a relationship where each class has an independent existence and there is no ownership between them.
- Association allows objects of different classes to be treated as the same type.
- Association represents a parent-child relationship between classes.
Design Principles
When designing object-oriented systems, it is important to follow certain principles to ensure that the code is well-structured, maintainable, and scalable. Two commonly used design principles are SOLID and DRY.
SOLID
SOLID is an acronym for five design principles: Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, and Dependency Inversion.
Single Responsibility Principle (SRP) states that a class should have only one reason to change. It means that a class should have only one responsibility or job. This principle helps in making the code easier to understand, test, and maintain.
Open-Closed Principle (OCP) states that software entities (classes, modules, functions, etc.) should be open for extension but closed for modification. It means that the behavior of a module or class should be able to be changed without modifying its source code.
Liskov Substitution Principle (LSP) states that objects of a superclass should be able to be replaced with objects of its subclasses without affecting the correctness of the program. In other words, the subclasses should be substitutable for their base classes.
Interface Segregation Principle (ISP) states that clients should not be forced to depend on interfaces they do not use. It means that an interface should have only those methods that are relevant to the implementing classes. This principle helps in keeping the code modular and avoiding unnecessary dependencies.
Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions. It means that the code should depend on abstractions (interfaces or abstract classes) rather than concrete implementations. This principle helps in achieving loose coupling and making the code more flexible and maintainable.
DRY
DRY stands for Don't Repeat Yourself. It is a principle of software development that promotes the elimination of duplicated code. Repeated code increases the chances of introducing bugs and makes the code harder to maintain and understand. By following the DRY principle, developers can reduce redundancy by creating reusable and modular code.
By applying these design principles, you can create code that is modular, flexible, and easier to understand and maintain. Let's take a look at an example of applying the SOLID principles:
1// Example of applying SOLID design principles
2
3class Shape {
4 // ...properties and methods...
5}
6class Circle extends Shape {
7 // ...properties and methods specific to Circle...
8}
9class Rectangle extends Shape {
10 // ...properties and methods specific to Rectangle...
11}
12
13interface Drawable {
14 void draw();
15}
16
17class CircleDrawer implements Drawable {
18 private Circle circle;
19
20 public CircleDrawer(Circle circle) {
21 this.circle = circle;
22 }
23
24 public void draw() {
25 // Draw the circle
26 }
27}
28
29class RectangleDrawer implements Drawable {
30 private Rectangle rectangle;
31
32 public RectangleDrawer(Rectangle rectangle) {
33 this.rectangle = rectangle;
34 }
35
36 public void draw() {
37 // Draw the rectangle
38 }
39}
40
41public class Main {
42 public static void main(String[] args) {
43 Circle circle = new Circle();
44 Rectangle rectangle = new Rectangle();
45
46 List<Drawable> shapes = new ArrayList<>();
47 shapes.add(new CircleDrawer(circle));
48 shapes.add(new RectangleDrawer(rectangle));
49
50 for (Drawable shape : shapes) {
51 shape.draw();
52 }
53 }
54}
xxxxxxxxxx
}
// Example of applying SOLID design principles
class Shape {
// ...properties and methods...
}
class Circle extends Shape {
// ...properties and methods specific to Circle...
}
class Rectangle extends Shape {
// ...properties and methods specific to Rectangle...
}
interface Drawable {
void draw();
}
class CircleDrawer implements Drawable {
private Circle circle;
public CircleDrawer(Circle circle) {
this.circle = circle;
}
public void draw() {
// Draw the circle
}
}
class RectangleDrawer implements Drawable {
Let's test your knowledge. Fill in the missing part by typing it in.
The SOLID principles are an acronym for five design principles: Single Responsibility Principle (SRP), Open-Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), and ____. The Dependency Inversion Principle (DIP) states that high-level modules should not depend on low-level modules. Instead, both should depend on abstractions.
Write the missing line below.
Design Patterns
Design patterns are standardized solutions to recurring design problems in object-oriented systems. They capture best practices and provide a common language for software developers to discuss and apply design principles.
Some commonly used design patterns include:
- Singleton Pattern: Ensures that a class has only one instance, and provides a global point of access to that instance.
- Factory Pattern: Provides an interface for creating objects, but allows subclasses to decide which class to instantiate.
- Adapter Pattern: Allows objects with incompatible interfaces to work together by creating a common interface that both objects can use.
- Decorator Pattern: Allows behavior to be added to an individual object dynamically, without affecting the behavior of other objects. It provides a flexible alternative to subclassing for extending functionality.
Design patterns are not specific to any programming language and can be applied to any object-oriented system. They can improve code reusability, readability, and maintainability.
Let's take a look at an example of the Decorator pattern in Java:
xxxxxxxxxx
}
// Decorator pattern example
interface Beverage {
String getDescription();
double getCost();
}
class Espresso implements Beverage {
public String getDescription() {
return "Espresso";
}
public double getCost() {
return 1.99;
}
}
class Milk implements Beverage {
private Beverage beverage;
public Milk(Beverage beverage) {
this.beverage = beverage;
}
public String getDescription() {
return beverage.getDescription() + ", Milk";
}
public double getCost() {
return beverage.getCost() + 0.49;
Build your intuition. Click the correct answer from the options.
Which design pattern provides a flexible alternative to subclassing for extending functionality?
Click the option that best answers the question.
Generating complete for this lesson!