Object-Oriented Programming
Oh, good, old OOP 😌. Is it actually that good 🤔? Even though Object-Oriented Programming is one of the most widely used programming paradigms, there are always caveats.
Let’s briefly revisit the core principles of OOP: encapsulation, inheritance, abstraction, and polymorphism. For experienced developers, we’ll explore those topics in advance. Finally, we’ll cover key OOD practices, including rich vs. anemic design, various class associations, the composition vs. inheritance debate, and coupling and cohesion. In case you made to the very end, you will see what’s wrong with everyone’s beloved OOP 😅.
If you are interested in other programming paradigms, look here:
- Structured Programming
- Object-Oriented Programming
- Aspect-Oriented Programming
- Functional Programming
- Event-Driven Programming
- Reactive Programming
Let’s begin.
Object-Oriented Programming
Look around yourself. What do you see? A book? A table? A laptop? Everything you see around is an object. They consist of other objects, interact with each other, and behave differently.
Object-Oriented Programming (OOP) aims to emulate real-world entities in code. This approach mirrors how objects interact in the real world, making software design more intuitive, reusable, and scalable.
Object-Oriented Programming (OOP) is a programming paradigm based on the concept of objects, which encapsulate both data (fields/properties) and behavior (methods).
The OOP can be achieved by using four key principles:
- Encapsulation
- Inheritance
- Abstraction
- Polymorphism
Let’s discuss each of them.
Encapsulation
Encapsulation hides the internal state of an object and only allows controlled access via properties or methods.
class Cat
{
// private field (hidden from outside)
private int _energyLevel = 100;
// public method to play (reduces energy)
public void Play()
{
if (_energyLevel >= 20)
{
_energyLevel -= 20;
Console.WriteLine($"Cat is playing!");
}
else
{
Console.WriteLine($"Cat is too tired to play!");
}
}
// public method to eat (restores energy)
public void Eat()
{
_energyLevel = 100;
}
}Encapsulation has several benefits:
- it hides internal data and restricts direct access, preventing unintended modifications that can lead the class to an invalid state
- the internal state of the class can be changed without breaking public behavior
- it provides a simplified interface for the user of the class, hiding its internal complexity
Inheritance
Inheritance allows a class (child) to inherit properties and methods from another class (parent).
class Cat // base class/parent class
{
public void MakeSound() // inherited
{
Console.WriteLine("Meow!");
}
}
class DomesticCat : Cat // derived class/child class
{
public void Play() { . . . }
public void Eat() { . . . }
}It promotes code reusability since you no longer need to implement the same behavior multiple times.
DomesticCat myCat = new DomesticCat();
myCat.MakeSound(); // implemented in the parent Cat class
myCat.Play(); // defined in DomesticCatAbstraction
Instead of working with particular instances, we can define an abstraction.
Abstraction is just a general idea of something. It hides the complexity of implementation details and exposes only the necessary parts.
interface IAnimal // abstraction
{
void MakeSound(); // exposed behaviour
}
class Cat : IAnimal // concrete implementation
{
public void MakeSound()
{
Console.WriteLine("Meow!");
}
public void Scratch() { . . . }
}
class Dog : IAnimal // concrete implementation
{
public void MakeSound()
{
Console.WriteLine("Woof!");
}
}Using abstraction, we express the intent of the class rather than the actual implementation.
IAnimal animal = new Cat();
animal.MakeSound();
// animal.Scratch(); -- not availableAbstractions make code loosely coupled and allow you to substitute one implementation with another. This means extensibility since new classes can be added without modifying existing code.
Additionally, you achieve the same level of reusability as you would have with generics. Now operations can be performed on different object types through a common interface or base class, reducing code duplication.
// generic
void WriteToFile<T>(IEnumerable<T> collection, string fileName)
where T : IAnimal
{
. . .
}
// base-class
void WriteToFile(IEnumerable<IAnimal> collection, string fileName)
{
. . .
}Polymorphism
Polymorphism allows the use of a concrete implementation through abstraction.
IAnimal animal = new Cat();
animal.MakeSound(); // Meow!For example, animal is a polymorphic reference that can hold any instance of animal hierarchy (e.g., Cat, Dog). However, when called, it will invoke a method implementation of the actual instance it refers to.
Polymorphism brings flexibility. The same method can have different behaviors depending on the object type.
Object-Oriented Programming *
What you just witnessed are core concepts of OOP. However, there is also a more advanced and darker side of it, intended only for experts.
Let’s see those principles again, but with a deeper dive this time:
- Encapsulation
- Inheritance
- Abstraction
- Polymorphism
Encapsulation *
Encapsulations can be strong and weak.
Strong encapsulation means restricting access to class data as much as possible. Only essential operations are exposed through methods, while internal details remain private.
Its characteristics are:
privateorprotectedfields- existence of getters/setters: access to fields is provided through properties, which can enforce validation or control data access
class Employee
{
// private field
private int _name;
private int _age;
// public behavior
public void Greet()
{
Console.WriteLine($"Hi, my name is {_name}!");
}
public void SetAge(int newAge)
{
if (newAge >= 18) _age = newAge;
}
public int GetAge() => _age;
}Weak encapsulation means exposing internal class data or behavior more freely, potentially allowing direct access to fields or bypassing strict validation through public properties or methods.
It can be spotted by public fields:
class Employee
{
// public field, weak encapsulation
public string Name;
public int Age;
public void Greet()
{
Console.WriteLine($"Hi, my name is {Name}!");
}
}In this case, fields are still encapsulated in the class but are visible to the outside world.
Comparison:
- Strong Encapsulation focuses on controlling access to class data, making it more secure and maintainable, but potentially less flexible.
- Weak Encapsulation offers easier access to class data, which might be useful in some cases, but risks compromising data integrity and security.
Inheritance *
Some languages support the inheritance of multiple classes. However, it may cause the “diamond problem”:
Let’s say there is a .MakeSound() method in the Animal class. The method is overridden in both LandAnimal and WaterAnimal. Which version of .MakeSound() should the Duck use?
While some programming languages offer a workaround for this problem, C# is not one of them. In C#, it is prohibited to inherit multiple classes, and only the inheritance of multiple interfaces is allowed.
class Duck : LandAnimal, WaterAnimal // ❌ a compile-time error
{
. . .
}This may occasionally cause friction for you, and you would have to rely on patterns like Servant, Twin, etc.
Abstraction *
The abstraction can be described in the form of “inheritance” and “implementation”:
- A class inherits from another class when it derives from a base class, gaining its properties, methods, and behaviors. Inheritance establishes an “is-a” relationship.
- A class implements an interface, meaning it provides the specific behavior defined by that interface, but it does not provide any implementation. This way “can-do” relationship is established.
The difference between an interface and abstract class varies by programming language and the line gets blurred every day. Just make sure you are using the one that best fits the design and requirements of your application.
Polymorphism *
Polymorphism can be dynamic and static.
Dynamic polymorphism occurs when the method to be called is determined at runtime. It is typically achieved through method overriding.
interface IAnimal
{
void MakeSound();
}
class Cat: IAnimal { . . . }
class Dog: IAnimal { . . . }
// dynamic polymorphism
IAnimal animal = new Dog();
animal.MakeSound(); // Woof!Static polymorphism is determined at compile-time and is typically achieved through method overloading.
class Dog
{
public void MakeSound()
{
Console.WriteLine("Woof!");
}
// overloaded method
public void MakeSound(int volume)
{
if (volume > 8) Console.WriteLine("Bark!");
else MakeSound();
}
}
// static polymorphism
dog.MakeSound(); // Woof!
dog.MakeSound(10); // Bark!Object-Oriented Design (OOD)
Now that you have learned the main concepts of OOP, it is time to see how objects can be designed and interact with each other.
Primitive Obsession Issue
When designing your domain is not always clear what should be defined as a separate entity and what could stay a primitive type. Poor understanding of the domain (or just laziness 🙃) often leads to the primitive obsession issue.
Primitive obsession refers to the tendency of using primitive data types (
int,string,enum, etc) for representing domain concepts instead of creating dedicated domain-specific types.
For example, using an int for Salary can be considered a primitive obsession:
class Employee
{
public string Name { get; set; }
public int Salary { get; set; } // Salary is just an integer
}In general, it is better to replace primitive types with corresponding domain models:
class Salary
{
public required decimal Amount { get; init; }
public required string Currency { get; init; }
// additional behavior, like tax calculation or salary validation, could go here
}
class Employee
{
public string Name { get; set; }
public Salary Salary { get; set; } // Salary is represented by a domain object
}However, you should stay pragmatic and don’t bloat your system with tons of classes for nothing. For example, sometimes it is fine to use a string for email address instead of a dedicated class🙃.
Rich vs Anemic
The object itself can be designed differently: with a rich and anemic model.
Get iamprovidence’s stories in your inbox
Join Medium for free to get updates from this writer.
The rich model is an object-oriented approach where the domain objects (entities) encapsulate both data and behavior.
The anemic model implies two classes: one class contains the data (dto), and another has all the behavior (service).
Both are widely used in various patterns. You have the rich model in prototype, template method, while anemic is implemented by visitor, strategy, command, factory, chain of responsibility, etc.
Association (Composition and Aggregation)
Inheritance is not the only relation that can exist between objects.
In general, the relationship between two objects where an object “has-a” another is called an association. It can be one-to-one, one-to-many, or many-to-many.
In OOP, we have two types of association: Composition and Aggregation.
In Composition, one object owns another object. The owned object cannot exist on its own.
In Aggregation, objects are related to each other in a way that allows them to exist independently of each other.
The choice between composition and aggregation depends on your domain. The same entities in different systems can have different relations.
By the way, often, the word “Composition” is used to describe “Association” and can be used interchangeably.
Favor Composition over Inheritance
There is a popular rumor that you should favor composition over inheritance.
The problem with inheritance is that it would be hard to navigate through the hierarchy of 20 classes. Maintaining all overridden methods is even more of a nightmare. In practice, though, this is rarely the case.
Moreover, most of the time, the choice between “is-a” and “has-a” is dictated by the nature of the relationship. For example, try telling the relation between DomesticCat and Cat, Library and Book, Car and Engine, AdminUser and User, etc.
Still, there are cases when both can be applied and justified:
The problem with inheritance is more visible through patterns. For example, compare the strategy pattern and template method.
You can see that the composition in the strategy pattern allows you to change the behavior of objects dynamically at runtime by replacing or adding components.
class AppartmentTaxCalculator : ITaxCalculator { . . . }
class PrivateHouseTaxCalculator : ITaxCalculator { . . . }
class RealEstate
{
// strategy
public ITaxCalculator TaxCalculator { get; set; }
public double GetPrice()
{
var squarePrice = 15;
return squarePrice + TaxCalculator.GetTaxes();
}
}Whereas inheritance binds a class to a fixed parent-child hierarchy.
abstract class RealEstateBase
{
// template method
protected abstract double GetTaxes();
public double GetPrice()
{
var squarePrice = 15;
return squarePrice + GetTaxes();
}
}
class Appartment : RealEstateBase
{
protected override double GetTaxes() => 15;
}
class PrivateHouse : RealEstateBase
{
protected override double GetTaxes() => 25;
}This makes the composition more adaptable to change.
Another example is a bridge pattern, where composition significantly decreases the number of classes.
Overall, as always, you are the one to decide. Do not use a composition just because somebody told you to. Use what leads to a better design.
Remember, without inheritance, you don’t have polymorphism, meaning no OOP, no design patterns, no DDD, and you are left just with DTOs and functions that can work for small programs but are unlikely to succeed in complex domains.
Proper use of the Virtual keyword
Although the point of virtual keyword to alter a method’s behavior, it can easily introduce bugs.
abstract class VehicleBase
{
. . .
public virtual void StartEngine()
{
_engineStatus = EngineStatus.Working;
}
}In a derived class, you may want to reuse the base implementation, which raises questions: Should it be called before my logic, after it, in the middle, or perhaps not at all?
class Car : VehicleBase
{
. . .
public override void StartEngine()
{
_oilChecked = true;
_fuelChecked = true;
base.StartEngine(); // call base logic
_kilometersDrivenSinceLastService = 0;
}
}A common design approach is to use the Template Method pattern, allowing consumers of the class to hook additional behavior around the base method’s definition.
abstract class VehicleBase
{
. . .
public /*virtual*/ void StartEngine() // no longer virtual
{
BeforeStartEngine();
_engineStatus = EngineStatus.Working;
AfterStartEngine()
}
// template methods
public virtual void BeforeStartEngine() { /* empty */ }
public virtual void AfterStartEngine() { /* empty */ }
}You no longer need to worry about the order of calling the base method or risk introducing bugs by forgetting to call it.
class Car : VehicleBase
{
. . .
public override void BeforeStartEngine()
{
_oilChecked = true;
_fuelChecked = true;
}
public override void AfterStartEngine()
{
_kilometersDrivenSinceLastService = 0;
}
}Alternatively, C# events could be used in place of abstract methods.
Invariance/Covariance/Contravariance
Polymorphism allows objects of different types to be treated through the same interface — but it has limits, especially with generics😒.
When you have generic types, the relationship between the generic wrappers does not necessarily follow the inheritance relationship of the underlying types.
Variance defines how types related by inheritance behave when used as generics.
It tells you whether a generic type can safely substitute for another which result in invariant, covariant, or contravariant relation.
Coupling
Coupling refers to the degree of dependency between different classes or modules in a system.
Tight coupling occurs when a class is highly dependent on another class:
- Changes in one class may require changes in another.
- Harder to modify, test, and reuse.
class Order
{
private Invoice _invoice = new Invoice(); // direct dependency
}Low coupling is preferable because classes depend on abstractions (which OOP encourages) and are easier to modify, test, and maintain.
class Order
{
private readonly IInvoice _invoice; // coupled to the interface
public Order(IInvoice invoice)
{
_invoice = invoice; // inject dependency
}
}Favor low coupling by using interfaces, dependency injection, and composition.
Cohesion
Cohesion refers to how strongly related and focused the responsibilities of a class or module are.
Low cohesion often means a class tries to do too many unrelated things, which is harder to maintain and understand.
class Utility
{
public void Log(string message) { /* logging logic */ }
public void SendEmail(string email) { /* email logic */ }
public void ProcessOrder() { /* order logic */ }
}Your design should target high cohesion:
- a class has a well-defined single responsibility
- it leads to better readability, maintainability, and testability
class Logger
{
public void Log(string message)
{
Console.WriteLine(message);
}
}
class EmailSender
{
public void Send(string email) { /* email logic */ }
}
class OrderProcessor
{
public void Process() { /* order logic */ }
}Web-Developer Thoughts 🤔
So, yes OOP 😌. And you get to the point where I'm going to criticize it 😅.
Don’t get me wrong, the encapsulation is a great concept. OOP helps to reason and design your system according to real-world concepts. And I am happy like a child every time I get to use design patterns. Buuuut….
The enterprise is dominated by web. While OOP is useful in game or native-app development, on the web you would have entities, DTOs, controllers, and anemic services that are closer to functional programming rather than OOP. If you get to apply DDD, you can call yourself lucky 😁.
Moreover, some problems can be solved better with functional or procedural approaches. OOP can feel forced in cases where simple data transformations are needed.
Additionally, many developers overuse design patterns and abstractions, making code harder to read and maintain.
That being said, OOP is still widely used and useful in many cases. Just don’t overengineer 😉.
Conclusion
Object-oriented programming (OOP) is a programming paradigm that teaches us how to model real-world entities using objects.
It promotes modularity and reusability. OOP helps manage complexity by encapsulating state and behavior within objects.
The benefits can be listed endlessly:
- encapsulation:
- bundling data and behavior together restricts direct access, reducing unintended modifications
- the internal state of the class can be changed without breaking public behavior
- an object exposes a simplified interface for the user of the class, hiding its internal complexity - inheritance
- code reuse is facilitated by allowing new classes to derive behavior from existing ones - abstraction
- hiding implementation details simplifies interaction with complex systems
- the code is loosely coupled, meaning easier testing and maintaining
- the code can be extended without modifications - polymorphism
- objects of different types can be treated uniformly, making code more flexible and scalable
💬 Let me know in the comments, are you team “OOP” or team “against OOP”?
👏 Clap if you find something new here
☕️ Support me if you want
✅ Follow
😉 And stay awesome








