Programming Fundamentals and Object-Oriented Programming

Chapter 7
Classes, Objects and Encapsulation

Post Graduate Diploma in Full Stack Development
Programming Fundamentals and Object-Oriented Programming   •   Post Graduate

7. Classes, Objects and Encapsulation

Introduction and Context

Object-oriented programming (OOP) provides a way to design software around the entities that participate in a problem domain. Instead of treating a program only as a sequence of procedures operating on separate data structures, OOP combines state and behaviour into coherent units. A banking application may model an account, a retail system a product, and a learning platform an enrolment. These entities become classes and objects in the program.

Classes, objects and encapsulation are central to maintainable software. A class describes the structure and capabilities of a group of objects; an object is a concrete runtime instance of a class; and encapsulation controls how the object's internal state can be accessed or changed. Together, these ideas help developers represent domain concepts, protect invariants, reduce unintended dependencies and manage system complexity.

In earlier programming work, variables, expressions, control structures and functions were used to solve individual computational problems. This chapter extends those foundations by examining how larger programs can be organised around responsible, communicating objects. The discussion uses language-neutral examples with syntax resembling Java, C# and similar statically typed languages. Specific keywords vary between languages, but the underlying design principles are widely applicable.

Learning Objectives

After studying this chapter, the learner should be able to:

1. Distinguish classes, objects, fields, properties, methods and constructors.

2. Model domain entities by identifying their state, behaviour and responsibilities.

3. Explain object identity, equality, reference and lifecycle.

4. Apply access control and encapsulation to protect object invariants.

5. Use instance and static members appropriately.

6. Design constructors and methods that maintain valid object state.

7. Evaluate common object-oriented design mistakes and improve class interfaces.

8. Apply these ideas to professional software systems and collaborative development.

From Domain Concepts to Program Objects

A useful class begins with a meaningful concept in the problem domain. Consider a library system. A book has an ISBN, title and availability status. It may be borrowed, returned or reserved. A member has an identifier and borrowing privileges. These are not merely collections of variables; they have rules governing what may happen to them.

A class is a blueprint or type definition. It specifies the data an object may hold and the operations through which that data may be used. An object is an instance created from that definition. If `Book` is a class, then a particular book with ISBN `978...123` is an object. Many objects can be created from one class, each with its own instance state.

A simple conceptual declaration may be represented as follows:

```text

class BankAccount

private accountNumber

private balance

constructor(accountNumber, openingBalance)

validate openingBalance

accountNumber = accountNumber

balance = openingBalance

method deposit(amount)

require amount > 0

balance = balance + amount

method withdraw(amount)

require amount > 0 and amount <= balance

balance = balance - amount

```

The declaration expresses more than storage. It states that an account owns its balance and that changes occur through operations subject to rules. The class therefore becomes a boundary around a coherent responsibility.

State, Behaviour and Responsibility

The state of an object is the collection of values held by its fields at a particular time. Behaviour is represented by methods that inspect or modify that state. Responsibility describes what the object should know or do on behalf of the wider system.

For a `ShoppingCart`, state might include selected items and a discount code. Behaviour might include adding an item, removing an item and calculating a total. A poorly designed cart might expose its internal item list and allow any other component to insert invalid values. A better design makes the cart responsible for validating additions and calculating totals according to its own rules.

The principle of responsibility is important because classes should be meaningful units rather than arbitrary containers. A class that manages passwords, generates invoices, sends emails and connects to a database probably has too many unrelated responsibilities. Such a design becomes difficult to test and change. Responsibility-driven design asks: which object has the information and authority needed to perform this operation correctly?

The Anatomy of a Class

Fields and Instance State

A field is a variable declared as part of a class. An instance field belongs to a particular object, so two objects of the same class can contain different values. For example, every `Employee` may have a name and salary, but each employee object stores its own values.

Fields should generally be private or otherwise restricted. Public mutable fields allow external code to change state without validation. If an employee's salary must not be negative, exposing `salary` directly weakens the rule. A method such as `changeSalary(newSalary)` can validate the proposed value before storing it.

Properties and Controlled Access

Many languages provide properties as a controlled alternative to public fields. A property may have a getter, a setter, or both. A read-only property can expose information without permitting external modification. A setter can enforce validation or trigger related behaviour.

```text

class Temperature

private celsius

property Celsius

get return celsius

set

require value >= -273.15

celsius = value

```

A property is not automatically encapsulated merely because it uses special syntax. A public setter that accepts every value offers little protection. Encapsulation depends on the quality of the boundary and the rules enforced at that boundary.

Methods

Methods define operations available on an object or class. An instance method operates in the context of a particular object and can use its instance state. A method such as `account.withdraw(100)` acts on one account. Methods should usually express meaningful domain actions rather than expose implementation details. `withdraw` is more informative than a generic `setBalance`, because it communicates an operation and allows the class to enforce the conditions associated with it.

Methods may return values, modify state, or both. A query method reports information without changing observable state; a command method requests a change. Keeping this distinction clear improves predictability and testing. Some designs use the command-query separation principle, which advises that a method should either return useful information or alter state, but not do both in an ambiguous way.

Constructors and Object Creation

A constructor establishes a new object's initial state. It is invoked when the object is created and normally ensures that mandatory fields are supplied and invariants hold from the beginning. An invariant is a condition that must remain true throughout an object's valid lifetime.

For a `Date` object, an invariant may require the month to be between 1 and 12 and the day to be valid for that month. For a `BankAccount`, the balance may be required to be non-negative. If an object can be created in an invalid state, every later method must defend against that invalidity, increasing complexity.

Constructors should avoid surprising side effects. Creating a `Customer` object should not ordinarily send an email, charge a credit card or make an irreversible network request. Such actions are better represented by explicit operations or application services.

Multiple constructors, called constructor overloading, can support different creation scenarios. However, excessive overloads may make object creation confusing. A factory method or builder can be preferable when an object has many optional settings or complicated validation rules.

Encapsulation and Access Control

Encapsulation means combining state and behaviour in a unit while controlling access to the unit's implementation and preserving its rules. It is often described as information hiding, although the two ideas are not identical. Encapsulation structures a responsibility; information hiding conceals design decisions that other parts of the system do not need to know.

Access modifiers commonly include:

  • `public`: accessible to permitted external clients and therefore part of the usable interface.
  • `private`: accessible only within the defining class.
  • `protected`: accessible within the class and, depending on the language, derived classes.
  • package, internal or module visibility: accessible within a defined organisational boundary.

The most restrictive visibility that supports legitimate use is generally the safest starting point. A private field can be changed only through methods that understand its meaning. This reduces coupling: clients depend on the public contract rather than on the internal representation.

Consider an order whose total depends on line items, taxes and discounts. If the item collection and total are publicly mutable, another component could change the items without recalculating the total. An encapsulated order might expose a read-only view of its lines and provide `addLine`, `removeLine` and `calculateTotal` operations. The internal representation can later change from a list to a database-backed structure without changing every client.

Encapsulation is not the same as preventing all access. A well-designed class exposes the operations clients genuinely need. Excessive restriction can make a class awkward to use; indiscriminate exposure makes it fragile. The design question is: what must clients know, and what should the object manage itself?

Invariants and Defensive Design

An invariant is a logical condition that remains true whenever an object is observable in a stable state. Examples include:

  • an account balance cannot be below an authorised overdraft limit;
  • a meeting's end time must be later than its start time;
  • a user role must belong to an approved set;
  • an order cannot be shipped before payment is confirmed.

Constructors establish invariants, while methods preserve them. Validation should occur at the boundary where invalid input enters. It is also useful to distinguish validation failures clearly, using suitable exceptions or result types rather than silently correcting important data.

An object may temporarily violate an invariant during a private operation, provided no external code can observe the intermediate state. Public methods should return only after restoring a valid state. This principle is especially important in multithreaded systems, where partial updates can produce inconsistent observations.

Identity, Equality and Lifecycle

Object identity answers the question, “Which particular object is this?” Two objects may contain identical values but still be distinct objects. For example, two separate `Customer` objects may both contain the name “Amina Khan”; they are not necessarily the same customer.

Equality answers whether two objects should be treated as equivalent for a particular purpose. Value objects such as money, coordinates or dates are commonly compared by their constituent values. Entity objects such as bank accounts or users are commonly identified by a stable identifier. A system might regard two user objects as equal if they represent the same user ID, even if their in-memory addresses differ.

Reference equality compares whether two references point to the same object. Value equality compares meaningful content or identity fields. Confusing these forms can cause defects in collections, caching and authorization logic. If a class overrides equality, it should normally provide a compatible hash function: equal objects must produce equal hash codes when used in hash-based collections.

Object lifecycle describes the stages from creation to disposal or abandonment. An object may be constructed, used by one or more components, transferred across a boundary and eventually released. In garbage-collected languages, memory reclamation is usually automatic, but external resources such as files, database connections and network sockets may still require explicit closure. Lifecycle management is therefore broader than memory management.

Ownership is also significant. If an `Invoice` creates and controls its line items, it may own their lifecycle. If line items are shared among several objects, ownership is less direct and coordination becomes necessary. Clear ownership reduces resource leaks and accidental modification.

Instance Members, Static Members and Shared State

Instance members belong to a particular object. Static, class or shared members belong to the class itself and exist independently of any one instance. A `Math` utility class may provide a static method for calculating a value. A `Company` class might maintain a static count of created employees.

Static members are useful for constants, stateless utility operations and carefully managed class-wide policies. They become problematic when used for mutable global state. Global mutable data creates hidden dependencies, complicates testing, and may introduce race conditions when multiple requests execute concurrently.

For example, a static `currentUser` variable in a web application is unsafe if requests from different users can overlap. Request-specific data should be associated with the request or an appropriate scoped object. Similarly, a static database connection may be inappropriate if the framework expects managed connection pooling.

A static method cannot normally access instance state without receiving an object reference. This distinction helps clarify design: if the operation depends on the identity or state of one object, it is probably an instance method; if it is independent of individual state, a static method may be suitable.

A Practical Design Approach

Class design is an iterative modelling activity rather than a mechanical conversion of every noun into a class. A useful workflow is as follows:

1. Identify important domain concepts and external actors.

2. Record the information each concept must preserve.

3. Identify actions, rules and decisions associated with that information.

4. Assign each responsibility to the object best positioned to perform it.

5. Define a small public interface and keep representation private.

6. State invariants explicitly and enforce them in constructors and methods.

7. Decide whether equality represents identity, value or neither.

8. Consider ownership, lifecycle, persistence and concurrency.

9. Test valid, invalid and boundary cases.

10. Refactor when responsibilities, names or dependencies become unclear.

This workflow combines domain-driven thinking with practical encapsulation. It does not require that every class map directly to a database table. A domain class may represent a business rule, a value, a workflow state or an interaction. Conversely, persistence models and user-interface models may require different structures from domain objects.

Illustrative Case Study: Encapsulating a Digital Wallet

Background

The following is a fictional but realistic illustrative case. A fintech development team is building a digital wallet for a subscription platform. Each wallet stores a balance and supports deposits, withdrawals and transfers.

Situation and Problem

The first implementation exposes a mutable `balance` field. Payment code subtracts money directly, refund code adds money directly, and reporting code changes the balance while correcting a display error. After several releases, negative balances and unexplained discrepancies appear. The team also discovers that a wallet can be created with a negative opening balance.

Analysis

The central problem is not merely incorrect arithmetic. The wallet's state has no protective boundary, so unrelated components can modify it without applying business rules. The invariant is that the balance must not fall below zero unless an explicitly supported credit facility exists. Every balance-changing operation must also record an appropriate transaction or at least follow a consistent policy.

The team distinguishes wallet identity from wallet value. Two wallet objects loaded at different times may represent the same wallet if they share a stable wallet ID. They should not be considered equal merely because their balances happen to match.

Approach and Solution

The redesigned class has private fields for wallet ID and balance. Its constructor rejects an invalid ID and negative opening balance. Public methods include `deposit(amount)` and `withdraw(amount)`. Both reject non-positive amounts; `withdraw` also rejects amounts greater than the available balance. A read-only `balance` property permits display and reporting without allowing assignment.

```text

class Wallet

private id

private balance

constructor(id, openingBalance)

require id is valid

require openingBalance >= 0

this.id = id

this.balance = openingBalance

method deposit(amount)

require amount > 0

balance = balance + amount

method withdraw(amount)

require amount > 0

require amount <= balance

balance = balance - amount

property Balance

get return balance

```

The application service responsible for transfers coordinates two wallets, verifies that the source and destination are different, and calls the wallets' public operations. It does not alter their private balances. In a production system, the transfer would also require transactional persistence, authorization, audit records and concurrency control.

Outcome

The redesigned interface prevents several classes of defect. Invalid balances cannot be assigned directly, and business rules are concentrated in one location. Unit tests can test deposits, withdrawals and boundary conditions without involving the user interface. Later, the team can change the internal representation from a numeric balance to a ledger-derived balance while preserving the public wallet contract.

Learning Points

The case demonstrates that encapsulation is a design strategy, not simply a matter of marking fields private. It requires meaningful operations, explicit invariants, suitable ownership and a distinction between domain responsibilities and application coordination. It also shows that object-oriented design must be complemented by persistence and concurrency controls in real financial systems.

Common Errors, Risks and Limitations

A frequent error is creating classes that contain only public data and no meaningful behaviour. Such structures may be appropriate as simple data-transfer objects, but they do not protect domain rules. The opposite error is excessive encapsulation that produces dozens of trivial wrappers and makes ordinary use unnecessarily indirect.

Another mistake is the “god class”: one class knows too much and performs too many unrelated operations. This increases coupling and creates a bottleneck for change. Long parameter lists, repeated conditional logic and frequent direct access to another object's fields are signs that responsibilities may be misplaced.

Constructors can also become overloaded with unrelated work. Dependency injection is often preferable to having a class construct every service it needs. This improves testing and makes dependencies visible.

Inheritance introduces additional risks. A subclass must honour the behavioural expectations of its base type; otherwise substituting the subclass can break client code. When the relationship is not genuinely “is-a,” composition is usually safer. For example, a `Car` may contain an `Engine` rather than inherit from one.

Mutable objects create aliasing problems when several parts of a system hold references to the same instance. Defensive copying, immutable value objects and read-only views can reduce accidental changes. Encapsulation also does not automatically provide thread safety. Shared objects may require synchronization, immutability or carefully designed concurrent operations.

Professional, Ethical and Technical Considerations

Class design affects security and governance. Access modifiers should prevent accidental misuse, but authorization must not be confused with language-level visibility. A private field does not by itself ensure that only an authorised user can change a business record. Authorization belongs to the appropriate application or domain policy, and sensitive actions should be audited.

Developers should avoid exposing personal data through debugging methods, object serialization or public properties. Equality and logging implementations must not leak passwords, tokens or confidential identifiers. In regulated environments, lifecycle design should account for retention, deletion and traceability requirements.

Modern frameworks generate classes, properties, constructors and serialization code automatically. This improves productivity but can weaken invariants if generated setters expose fields indiscriminately. Object-relational mappers may also create partially initialized objects or bypass normal constructors. Teams should understand framework lifecycle behaviour and place validation where it will reliably execute.

Automated tests are especially valuable for encapsulated classes. Unit tests should cover normal operations, invalid input, boundary conditions, equality and lifecycle behaviour. Static analysis, code review and mutation testing can further reveal weak validation or untested branches. AI-assisted coding tools may suggest class structures, but developers remain responsible for checking invariants, security, ownership and domain correctness.

Emerging Directions

Contemporary software increasingly combines traditional objects with immutable data types, records, functional techniques and distributed services. Immutable objects are attractive for concurrent programs because their state cannot change after construction. Records and value types provide concise representations for values whose equality is structural.

In microservice systems, an object usually cannot protect a business invariant across multiple services by itself. Transactions, messaging, optimistic locking and idempotent operations may be required. Domain models therefore work alongside architectural mechanisms rather than replacing them.

Reflection, code generation and serialization make object state easier to inspect and transfer, but they can weaken information hiding. A professional design evaluates not only whether a class is convenient to instantiate, but also how it behaves across process boundaries, persistence layers, version changes and failures.

Best Practices for Class and Object Design

  • Give each class a clear purpose and a coherent set of responsibilities.
  • Keep fields private unless public data is deliberately part of the contract.
  • Expose domain actions rather than generic setters where rules matter.
  • Establish valid state in constructors or controlled factory methods.
  • Document and enforce invariants at public boundaries.
  • Prefer immutable value objects for concepts such as money, dates and coordinates where practical.
  • Distinguish entity identity from value equality and implement hashing consistently.
  • Minimise mutable static state and make dependencies explicit.
  • Clarify ownership and release external resources reliably.
  • Design for testing, including invalid and boundary cases.
  • Use composition when it expresses the relationship more accurately than inheritance.
  • Treat public interfaces as contracts that should evolve carefully.

Chapter Summary

A class defines the structure and operations of a category of objects, while an object is a concrete instance with its own identity and state. Fields represent state, methods express behaviour, properties provide controlled access, and constructors establish valid initial conditions. Encapsulation combines data and behaviour while hiding implementation details and protecting invariants.

Sound object design requires more than applying access modifiers. Developers must allocate responsibilities carefully, distinguish identity from equality, manage object lifecycle and ownership, and use static members only where class-wide behaviour is genuinely appropriate. In professional systems, encapsulation supports maintainability, testing, security and controlled change, but it must be complemented by persistence, authorization, concurrency and governance mechanisms.

Key Terms

Class: A type definition describing the data and behaviour of objects.

Object: A concrete runtime instance of a class.

Instance: A particular object created from a class.

Field: A variable declared as part of a class.

Property: A controlled interface for reading or changing a value.

Method: An operation defined by a class.

Constructor: An operation invoked to establish a new object's initial state.

Encapsulation: Combining state and behaviour while controlling external access.

Information hiding: Concealing implementation decisions that clients do not need to know.

Invariant: A condition that must remain true for a valid object.

Access modifier: A language feature controlling visibility of members.

Object identity: The characteristic that distinguishes one particular object or entity from another.

Reference equality: Equality based on whether two references point to the same object.

Value equality: Equality based on meaningful values or defined identity fields.

Static member: A member associated with a class rather than one instance.

Instance member: A member associated with a particular object.

Lifecycle: The stages an object passes through from creation to disposal or release.

Composition: Building a class by containing or collaborating with other objects.

Immutable object: An object whose observable state cannot change after construction.

Aliasing: The situation in which multiple references access the same mutable object.

Review Questions

Short Answer Questions

1. What is the difference between a class and an object?

2. Why should a constructor normally establish a valid object state?

3. Distinguish a field from a property.

4. What is an invariant? Give one example.

5. How does a static member differ from an instance member?

Long Answer Questions

1. Explain encapsulation and information hiding, and discuss how they contribute to maintainable software.

2. Describe the relationship among constructors, methods and invariants in a well-designed class.

3. Compare object identity, reference equality and value equality, including their importance in collections.

4. Discuss the benefits and risks of mutable static state in a professional application.

5. Explain how ownership and lifecycle management apply to objects that use external resources.

Analytical / Application Questions

1. A `Student` class exposes public fields for grades and allows any component to replace the grade list. Identify the design risks and propose an encapsulated interface.

2. Two separate `Customer` objects have the same name and email address but different customer IDs. Should they necessarily be equal? Justify your answer using identity and value concepts.

3. Design the key fields, methods, constructors and invariants for a `Meeting` class that supports scheduling, cancellation and rescheduling. Explain which members should be public and why.

22. Multiple Choice Questions with Answers

Question 1

Which statement best describes encapsulation?

A. Creating many objects from one class

B. Combining state and behaviour while controlling access to implementation details

C. Using only static methods in a program

D. Comparing two references for equality

Correct Answer: B. Combining state and behaviour while controlling access to implementation details

Question 2

What is the primary purpose of a constructor?

A. To destroy an object immediately after use

B. To establish the initial state of a new object

C. To make every field public

D. To compare two objects by value

Correct Answer: B. To establish the initial state of a new object

Question 3

A bank account exposes a public balance field, allowing any component to assign -500. Which principle is most directly violated?

A. Encapsulation and invariant protection

B. Constructor overloading

C. Reference equality

D. Static dispatch

Correct Answer: A. Encapsulation and invariant protection

Question 4

Two separately loaded objects represent the same customer ID but are stored at different memory locations. Which concept may regard them as the same customer?

A. Reference equality only

B. Identity-based equality using the customer ID

C. Constructor equality

D. Field visibility

Correct Answer: B. Identity-based equality using the customer ID

Question 5

Which member is most appropriate as an instance method?

A. A method calculating a value using no object state

B. A method withdrawing money from one specific account

C. A constant representing the value of pi

D. A utility method formatting any independent string

Correct Answer: B. A method withdrawing money from one specific account

Question 6

Why can mutable static state be dangerous in a web application?

A. It cannot be accessed by any method

B. It creates hidden shared dependencies and may mix data across concurrent requests

C. It automatically makes all objects immutable

D. It prevents constructors from running

Correct Answer: B. It creates hidden shared dependencies and may mix data across concurrent requests

Question 7

Which design best protects an order total from inconsistent direct updates?

A. Make items and total public and mutable

B. Expose a generic setTotal method to all clients

C. Keep representation private and provide controlled operations such as addLine and calculateTotal

D. Store the total in a global variable

Correct Answer: C. Keep representation private and provide controlled operations such as addLine and calculateTotal

Question 8

What must generally be true when a class overrides equality for use in a hash-based collection?

A. Unequal objects must always have different memory addresses

B. Equal objects must produce equal hash codes

C. Every object must have a static field

D. All fields must be public

Correct Answer: B. Equal objects must produce equal hash codes

Question 9

Which situation is the clearest example of an invariant?

A. A method has a short name

B. A meeting's end time must be later than its start time

C. Two references point to one object

D. A class has three methods

Correct Answer: B. A meeting's end time must be later than its start time

Question 10

Why might composition be preferred to inheritance for a Car and Engine design?

A. A car is not an engine; it contains or uses one

B. Composition makes all fields public

C. Inheritance cannot define methods

D. Composition eliminates the need for constructors

Correct Answer: A. A car is not an engine; it contains or uses one

Programming Fundamentals and Object-Oriented Programming
Chapter 7 of 15