Over 10 years we help companies reach their financial and branding goals. Engitech is a values-driven technology agency dedicated.

Gallery

Contacts

411 University St, Seattle, USA

+1 -800-456-478-23

Thinking in Objects: How Object-Oriented Programming Changes the Way You Solve Problems

Imagine you are asked to build software for a small delivery company.

At first, the problem sounds simple.

Customers place orders.

Drivers deliver packages.

Vehicles carry packages.

Payments are collected.

Managers track everything.

A beginner programmer may immediately think:

I need a database.

I need an API.

I need some loops.

I need some functions.

An object-oriented programmer asks a different question:

What are the things that exist in this world, and what can they do?

That small change in thinking is the beginning of the object-oriented paradigm.

Instead of seeing software as a long sequence of instructions, you begin seeing it as a collection of collaborating objects.

A customer.

An order.

A package.

A driver.

A vehicle.

A payment.

Each one has data.

Each one has behavior.

Each one has responsibilities.

And together, they form a model of the real problem.

That is the heart of object-oriented thinking.


Before Objects, There Were Procedures

Imagine programming a delivery system in a purely procedural way.

You might have data like:

customerName
customerAddress
orderTotal
driverName
vehicleNumber
paymentStatus

Then functions like:

CreateOrder()
AssignDriver()
CalculateDeliveryFee()
TakePayment()
CompleteDelivery()

This can work.

And for small programs, it may work very well.

But as the system grows, something happens.

More data appears.

More functions appear.

More rules appear.

Now you have:

CreateOrder()
UpdateOrder()
CancelOrder()
ValidateOrder()
AssignDriver()
ChangeDriver()
CalculateDeliveryFee()
CalculateDiscount()
RefundPayment()
CheckVehicleCapacity()
UpdateCustomerBalance()

Soon the program becomes a large collection of operations manipulating shared data.

The important relationships begin disappearing into the code.

Who owns the order status?

Who is allowed to cancel an order?

Who decides whether a vehicle can carry a package?

Who knows whether a payment can be refunded?

Object-oriented design tries to answer these questions by putting behavior closer to the data and concepts it belongs to.


Start With the World, Not the Code

Suppose we are designing a hotel booking system.

Do not begin with:

Controller
Repository
Service
DTO
Database table

Those are technical concerns.

Begin with the actual world.

What exists?

Perhaps:

Guest
Room
Reservation
Payment
Hotel
Invoice

Now ask what each thing knows.

A Room may know:

RoomNumber
Type
Capacity
Price
Status

A Reservation may know:

Guest
Room
CheckInDate
CheckOutDate
Status

A Payment may know:

Amount
Method
Status
TransactionId

Then ask:

What can each thing do?

A reservation can perhaps:

Confirm()
Cancel()
CheckIn()
CheckOut()

A room may:

MarkOccupied()
MarkAvailable()

A payment may:

Capture()
Refund()

Now the software begins resembling the actual business.

This is one of the most powerful benefits of object-oriented thinking.

The code becomes a model of the domain.


Objects Are More Than Data Containers

One of the most common mistakes in object-oriented programming is creating classes that contain only properties.

For example:

public class BankAccount
{
    public decimal Balance { get; set; }
}

Then somewhere else:

account.Balance -= amount;

Technically, you have an object.

But you are not really thinking in objects.

The object is merely a bag of data.

A stronger design might be:

public class BankAccount
{
    public decimal Balance { get; private set; }

    public void Withdraw(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException();

        if (amount > Balance)
            throw new InvalidOperationException();

        Balance -= amount;
    }
}

Now the account protects its own rules.

Instead of asking outside code to remember:

Never allow negative withdrawals.
Never allow withdrawing more than the balance.

the object itself enforces those rules.

That is called encapsulation.

And it is one of the foundations of object-oriented design.


Encapsulation Is About Protecting Meaning

People often describe encapsulation as:

"Make fields private."

That is only the mechanical part.

The deeper purpose is to protect meaning.

Imagine an order:

public class Order
{
    public OrderStatus Status { get; set; }
}

Any code can now do:

order.Status = OrderStatus.Completed;

Even if the order was never paid.

Even if it was cancelled.

Even if no shipment exists.

The object has no control over its own state.

A more meaningful model might be:

public void Complete()
{
    if (Status != OrderStatus.Shipped)
        throw new InvalidOperationException();

    Status = OrderStatus.Completed;
}

Now the object defines what "complete" actually means.

The object is no longer merely storing state.

It is protecting business truth.


Ask: Who Should Know This?

This is one of the most useful questions in object-oriented design.

Suppose you need to calculate an order total.

Who should know how?

The controller?

The database?

A utility class?

The order?

Perhaps the order contains items.

Each item knows:

Quantity
UnitPrice

Then an OrderItem may know how to calculate its own subtotal.

public decimal GetSubtotal()
{
    return Quantity * UnitPrice;
}

And the order can calculate:

public decimal GetTotal()
{
    return Items.Sum(x => x.GetSubtotal());
}

Notice the thinking.

Each object handles the knowledge closest to itself.

An order item knows how much it costs.

The order knows which items belong to it.

This creates a natural distribution of responsibility.


Don't Ask for Data and Then Do the Object's Job

Consider this code:

if (employee.Type == EmployeeType.Hourly)
{
    salary = employee.HoursWorked * employee.HourlyRate;
}
else if (employee.Type == EmployeeType.Monthly)
{
    salary = employee.MonthlySalary;
}

The calling code retrieves information from the employee and decides how salary works.

Now imagine ten places doing the same thing.

A more object-oriented approach may be:

employee.CalculateSalary();

Different employee types can implement that behavior differently.

The caller asks the object to perform its responsibility.

This principle is sometimes summarized as:

Tell, don't ask.

Instead of asking an object for all its internal data and then making decisions externally, tell the object what you want done.

This does not mean getters are always bad.

It means behavior should live where the relevant knowledge lives.


Objects Should Collaborate

An object-oriented program should not become one giant object containing everything.

Imagine:

EnterpriseSystemManager

with methods:

CreateCustomer()
CalculatePayroll()
GenerateInvoice()
SendEmail()
UpdateInventory()
RunProduction()
ProcessPayment()

That object knows too much.

It has too many responsibilities.

Good object-oriented design is about collaboration.

Imagine an order checkout:

Order
PaymentService
Inventory
Shipment

The process might look conceptually like:

Order asks Inventory to reserve items.

PaymentService processes payment.

Order confirms itself.

Shipment is created.

Each object or service plays a specific role.

Complex behavior emerges from collaboration.

This resembles real organizations.

The accountant does not repair delivery trucks.

The driver does not calculate payroll.

Responsibilities are distributed.


Single Responsibility Is About Reasons to Change

A common object-oriented principle is the Single Responsibility Principle.

It is often simplified to:

"A class should do one thing."

That phrase can be misleading.

Consider:

Order

An order may:

AddItem()
RemoveItem()
CalculateTotal()
Cancel()
Confirm()

That is more than one method.

But all of these behaviors belong to the same concept.

A better way to think about responsibility is:

A class should have a focused reason to change.

If your Order class must change when:

pricing rules change
email templates change
PDF layout changes
database provider changes
authentication changes

then it probably owns too much.

The order should change when order business rules change.

That is a much healthier boundary.


Objects Need Boundaries

Suppose you are building an inventory system.

An InventoryItem might contain:

Product
Quantity
Warehouse

You might be tempted to put everything there:

GenerateInvoice()
SendNotification()
CheckUserPermission()
SaveToDatabase()
ExportToExcel()

But these responsibilities belong to different parts of the system.

Object-oriented design requires boundaries.

Ask:

What is part of the object's identity?

What is part of its behavior?

What is infrastructure?

What is orchestration?

The answer is rarely perfect.

But asking the question prevents classes from becoming dumping grounds.


Think in Roles

Sometimes the easiest way to discover objects is to imagine a conversation.

Suppose a customer wants to buy a product.

Who participates?

Customer
Cart
Product
Inventory
Payment
Order

Now imagine the interaction:

Customer adds Product to Cart.

Cart calculates total.

Checkout asks Inventory to reserve Product.

Payment processes money.

Order records the purchase.

This is almost like writing a small play.

Each participant has a role.

This technique makes object-oriented systems easier to reason about.

Instead of thinking:

Which function should I call?

think:

Which object should be responsible for this action?

Inheritance Is Not the First Tool

When people first learn object-oriented programming, inheritance often receives enormous attention.

You learn:

Animal
  ├── Dog
  ├── Cat
  └── Bird

Then everything starts becoming an inheritance hierarchy.

But real software rarely fits perfectly into trees.

Suppose you have:

Employee

and then:

Manager
Developer
Accountant

What happens when one person is both:

Manager + Developer

Inheritance can become awkward.

A better question is often:

What can this object do?

This leads to composition and interfaces.

For example:

Employee
  has PayPolicy
  has AccessPolicy
  has JobRole

Now capabilities can be composed rather than forced into rigid inheritance structures.


Prefer "Has a" When It Models Reality Better

A car has an engine.

A car is not an engine.

So:

Car
 └── Engine

makes more sense than:

Car : Engine

This sounds absurd when stated this way.

But software often contains equivalent mistakes.

Inheritance represents:

is-a

Composition represents:

has-a

When modeling relationships, ask which one is actually true.

Composition is often more flexible because objects can be assembled from smaller behaviors.


Interfaces Represent Capabilities

Suppose your application sends notifications.

You may have:

public interface INotificationSender
{
    Task SendAsync(Notification notification);
}

Then implementations:

EmailSender
SmsSender
PushNotificationSender

The rest of the application does not need to care which one is used.

It only cares that the object can:

Send notification

This is a powerful object-oriented idea.

An interface describes a capability or contract.

It says:

I don't care what you are internally.

If you can perform this behavior, I can work with you.

This reduces coupling.

And lower coupling makes change easier.


Polymorphism Replaces Repeated Decisions

Imagine a payment system.

You have:

Cash
Card
BankTransfer
MobilePayment

Without polymorphism, you might write:

switch (payment.Type)
{
    case PaymentType.Cash:
        ProcessCash(payment);
        break;

    case PaymentType.Card:
        ProcessCard(payment);
        break;

    case PaymentType.BankTransfer:
        ProcessBankTransfer(payment);
        break;
}

Then this switch starts appearing everywhere.

A more object-oriented design might be:

payment.Process();

Each payment type knows how to process itself.

The caller does not need to know every possible implementation.

This is polymorphism.

Different objects respond to the same operation in different ways.

That allows behavior to grow without constantly modifying every caller.


But Don't Force Everything Into Classes

Object-oriented thinking can become dogmatic.

Not every operation needs a class.

Not every value needs an interface.

Not every five-line function needs a design pattern.

Suppose you need:

Convert Celsius to Fahrenheit.

A simple function may be perfectly fine.

Object-oriented design is valuable when there are:

concepts
state
behavior
relationships
rules
changing responsibilities

If the problem is simply a calculation, forcing it into a large class hierarchy usually makes things worse.

Paradigms are tools.

Not religions.


Find the Invariants

One of the strongest object-oriented techniques is identifying what must always remain true.

Suppose an invoice has rules:

Total cannot be negative.

Paid invoices cannot be edited.

Cancelled invoices cannot be paid.

Invoice must contain at least one item before approval.

These are invariants.

An object should protect them.

If outside code can freely manipulate internal state, those invariants become fragile.

Good objects make invalid states difficult—or ideally impossible—to create.

That is far more valuable than simply having nice class diagrams.


Constructors Can Protect Valid State

Consider:

var order = new Order();

What does that mean?

Does it have a customer?

Does it have a date?

Is it valid?

Perhaps a valid order requires certain information.

Then the constructor can enforce it.

public Order(Customer customer)
{
    Customer = customer
        ?? throw new ArgumentNullException(nameof(customer));

    Status = OrderStatus.Draft;
}

Now an order cannot exist without a customer.

You have reduced the number of invalid situations the rest of the application must handle.

Object-oriented design often improves software by moving validation closer to creation and state transitions.


Value Objects Help Model Meaning

Imagine this:

decimal price;
decimal tax;
decimal discount;
decimal balance;

They are all decimal.

But they do not mean the same thing.

Or consider:

string email;
string phone;
string countryCode;
string currency;

They are all strings.

But semantically they are completely different.

Object-oriented modeling can create value objects:

Money
EmailAddress
PhoneNumber
DateRange
Address

A Money object might contain:

Amount
Currency

Now this is harder to accidentally do:

USD + BDT

without explicit conversion.

Good modeling lets the type system help protect business meaning.


Object Orientation Is Really About Managing Complexity

This is the deeper lesson.

Object-oriented programming is not mainly about classes.

Classes are just one mechanism.

The real goal is to manage complexity by dividing a large system into understandable pieces.

Without structure, you may have:

50,000 lines of code

that all potentially interact.

With good object boundaries, perhaps you can think:

I only need to understand Order right now.

Then:

I only need to understand Payment.

Then:

I only need to understand Inventory.

The system may still contain 50,000 lines.

But your brain does not need to understand all 50,000 simultaneously.

That is the power of abstraction.


Let's Model a Real Problem

Imagine we are building a small library system.

Requirement:

Members borrow books. A member cannot borrow more than five books. A book cannot be borrowed if already loaned. Members can return books.

A procedural mindset might immediately start building database queries.

An object-oriented mindset first identifies concepts:

Member
Book
Loan
Library

Now ask about responsibilities.

Book knows:

Title
ISBN
Availability

Member knows:

Name
CurrentLoans

A member could have:

CanBorrow()

A book could have:

IsAvailable

Perhaps a library service coordinates:

Borrow(Member, Book)
Return(Member, Book)

The borrowing process becomes:

Check whether member can borrow.

Check whether book is available.

Create loan.

Mark book borrowed.

Associate loan with member.

Now the implementation grows from the model.

Instead of forcing the business into technical structures, the technical structures follow the business.


Then Ask What Could Change

Today the rule is:

Maximum five books.

Tomorrow:

Students can borrow five.

Teachers can borrow fifteen.

Premium members can borrow ten.

Now ask:

Where should that rule live?

Perhaps inside a borrowing policy.

IBorrowingPolicy

Different policies can determine limits.

Now the member does not need a giant chain of conditions.

You are modeling variation deliberately.

This is where object-oriented thinking becomes powerful.

Not just modeling what exists today.

But identifying where behavior varies.


State Transitions Are Often Better Than Setters

Suppose a production order has:

Draft
Approved
InProgress
Completed
Cancelled

A weak design exposes:

order.Status = ProductionStatus.Completed;

Anything can jump directly from:

Draft → Completed

A stronger object exposes operations:

Approve()
Start()
Complete()
Cancel()

Each method validates allowed transitions.

Now the object describes a workflow.

You begin thinking about behavior, not property mutation.

That is much closer to how businesses actually work.


The Database Is Not Your Domain Model

This is a common trap in enterprise software.

You look at the database:

Customers
Orders
OrderItems
Payments

Then create one class for every table.

Now your object model is simply a copy of the database.

Sometimes that is fine.

But object-oriented modeling asks a different question:

What concepts exist in the business?

A database is designed around storage.

Objects are designed around behavior and meaning.

Those goals overlap, but they are not identical.

A good domain model should not blindly inherit every compromise made for relational storage.


Don't Start With Design Patterns

Someone learns object orientation.

Then discovers:

Factory.

Strategy.

Observer.

Mediator.

Decorator.

Command.

Adapter.

Visitor.

Suddenly every problem needs a pattern.

This reverses the process.

You should not begin with:

Which design pattern should I use?

Begin with:

What problem am I trying to solve?

Patterns are names for solutions that repeatedly appear.

They should emerge because the problem calls for them.

Not because you want to use them.

A Strategy pattern becomes useful when behavior varies.

An Observer becomes useful when multiple components react to events.

A Factory becomes useful when object creation becomes complex.

Patterns are vocabulary.

Not architecture by decoration.


Object-Oriented Design Starts With Conversation

One of the simplest ways to model a system is to talk about it using ordinary language.

Suppose a business expert says:

"A customer creates a sales order. The sales order contains products. Once approved, warehouse staff reserve stock. After dispatch, the order becomes shipped."

Listen carefully.

The nouns suggest objects:

Customer
SalesOrder
Product
Warehouse
Stock

The verbs suggest behavior:

Create
Approve
Reserve
Dispatch
Ship

The rules suggest invariants:

Cannot dispatch without stock.

Cannot edit after shipment.

Cannot approve an empty order.

You are already designing.

Good object-oriented systems often use the same language the business uses.

That reduces the translation gap between software and reality.


When the Model Is Wrong, the Code Feels Difficult

This is an important signal.

Suppose implementing a simple business rule requires:

six services
four DTO conversions
three switch statements
two database queries
and a utility class

Perhaps the problem is not the rule.

Perhaps the model is wrong.

A good model often makes common operations feel natural.

If the business says:

Invoice approves itself

and your code requires manipulating unrelated structures everywhere, the responsibilities may be misplaced.

When code constantly fights you, reconsider the model.

Not every difficulty is solved by adding another abstraction.

Sometimes the abstraction itself is the problem.


Think About Behavior Before Data

Beginners often design classes by listing properties first.

Order:
Id
CustomerId
Date
Status
Total

Then stop.

Try reversing the process.

Ask:

What can an order do?

Add item
Remove item
Approve
Cancel
Calculate total
Dispatch

Now ask:

What information does it need to perform those behaviors?

This often creates a much richer model.

Object-oriented design is fundamentally about objects that act, not objects that merely exist.


The Goal Is Not a Perfect Model

Real businesses are messy.

A customer may also be a supplier.

An employee may belong to multiple teams.

A product may behave differently depending on context.

Rules change.

Exceptions appear.

No object model perfectly represents reality.

The goal is not to create a philosophical simulation of the universe.

The goal is to create a model that makes the software's important behaviors understandable and maintainable.

That means compromises.

Sometimes a service is better than putting behavior on an entity.

Sometimes a simple data structure is enough.

Sometimes functional techniques fit better.

Sometimes inheritance is appropriate.

Sometimes it is not.

Object orientation gives you a way to organize thinking.

It should never prevent you from thinking.


A Simple Mental Checklist

When approaching a problem in an object-oriented way, ask yourself:

What are the important concepts?

What does each concept know?

What can each concept do?

What rules must always remain true?

Which object owns each rule?

Which objects collaborate?

Where does behavior vary?

What should be hidden?

What should be exposed?

Which relationships are "is-a"?

Which relationships are "has-a"?

Which responsibilities should remain separate?

You do not need UML before writing code.

You do not need twenty interfaces.

You need clarity about responsibility.


The Moment Object Orientation Clicks

At first, object-oriented programming feels like syntax.

class Order
{
}

Then inheritance.

Then interfaces.

Then dependency injection.

Then design patterns.

But eventually something changes.

You stop asking:

"Which class should I create?"

And begin asking:

"Who should be responsible for this?"

That is the real transition.

You stop seeing software as instructions flowing through a machine.

You start seeing a society of components.

Each one knows certain things.

Each one hides certain things.

Each one performs certain actions.

Each one collaborates with others through clear boundaries.

A customer creates an order.

An order manages its items.

A payment processes money.

Inventory reserves stock.

A shipment delivers goods.

The system begins telling a story.

And when the story matches the business, the code becomes easier to reason about.


Objects Are a Way of Thinking

Object-oriented programming is often taught with dogs, cats, vehicles, and shapes.

But its real value becomes visible in much larger systems.

ERP.

Banking.

Healthcare.

Manufacturing.

Logistics.

E-commerce.

Games.

Business platforms containing thousands of rules.

Those systems are too large to understand as one giant algorithm.

We need boundaries.

Names.

Responsibilities.

Collaborations.

Objects provide one powerful way to create them.

The best object-oriented code is not the code with the most classes.

It is not the code with the deepest inheritance hierarchy.

It is not the code using every SOLID principle visibly.

It is the code where another developer can look at a business problem and say:

"Yes. I can see where that behavior belongs."

That is object-oriented thinking.

Not turning the world into classes.

But turning complexity into understandable responsibilities.

And once you begin seeing problems that way, programming changes.

Instead of asking the computer:

"What steps should you execute?"

you begin asking your software:

"Who are you, what do you know, and what are you responsible for?"

Very often, the solution starts appearing in the answer.

Leave a comment

Your email address will not be published. Required fields are marked *