This episode builds your C# OOP foundation: classes, objects, properties, encapsulation, inheritance, polymorphism, abstract classes, and interfaces. You will also learn record types, tuples, pattern matching, and deconstruction for more modern code.

In episode 4 you mastered the basic syntax of C#. Now it's time to model the real world in code with OOP (Object-Oriented Programming). Episode 5 breaks down classes, encapsulation, inheritance, polymorphism, interfaces, and abstraction — then closes with modern features such as record types and tuples.
It is important to understand: OOP in C# is not just about syntax, but about design discipline. Encapsulation protects state, interfaces separate contracts from implementations, and record types make immutable data concise. These patterns will be used continuously in episode 8 (dependency injection) and episode 16 (advanced architecture).
A class is a blueprint, and an object is its instance. Properties expose state in a controlled way. Access modifiers determine the level of access:
public class Rekening
{
private decimal _saldo;
public string Pemilik { get; set; }
public decimal Saldo => _saldo;
public void Setor(decimal jumlah)
{
if (jumlah <= 0) throw new ArgumentException("Jumlah tidak valid");
_saldo += jumlah;
}
}The _saldo field is private — only methods inside the class can change it. The Saldo property is read-only from the outside. This is encapsulation: internal state is protected and every change goes through the Setor method, which validates input.
Objects are created with new, and property values can be set via an object initializer:
var rekening = new Rekening { Pemilik = "Arman" };
rekening.Setor(100_000m);
Console.WriteLine(rekening.Saldo);new Rekening { Pemilik = "Arman" } uses an object initializer to fill in properties at creation time. The underscore value 100_000 is a digit separator that makes large numbers easy to read.
Inheritance allows a class to inherit behavior from another class. Abstract classes define a partial contract — they contain shared implementations and abstract methods that must be overridden:
public abstract class Hewan
{
public abstract string Bersuara();
}
public class Kucing : Hewan
{
public override string Bersuara() => "Meow";
}
public class Anjing : Hewan
{
public override string Bersuara() => "Guk";
}Kucing : Hewan inherits from Hewan, and override replaces the abstract Bersuara behavior. Polymorphism means that code operating on Hewan automatically works on all of its subtypes.
An interface defines a contract without implementation. A single class can implement many interfaces, overcoming the limitation of single inheritance:
public interface IPembayaran
{
bool Proses(decimal jumlah);
}
public class PembayaranKartu : IPembayaran
{
public bool Proses(decimal jumlah) => true;
}Use interfaces when you want to separate contracts from implementations — this is the core material of dependency injection in episode 8. Use abstract classes to share code between closely related classes.
Records provide value equality and immutability concisely. With init-only properties, values can only be set at construction time:
public record Produk(int Id, string Nama, decimal Harga);
var produk = new Produk(1, "Laptop", 15_000_000m);
var produkBaru = produk with { Harga = 14_500_000m };The with expression creates a copy of the record with some values changed — the original record is unchanged. Two records with the same values are considered equal. This makes records ideal for value objects and DTOs sent between services.
A tuple groups several values without creating a class. Deconstruction breaks values into separate variables:
var hasil = (Lat: -6.2, Lng: 106.8);
var (lat, lng) = hasil;
Console.WriteLine(lat);
Console.WriteLine(lng);(Lat: -6.2, Lng: 106.8) creates a named tuple, and var (lat, lng) = hasil deconstructs it. Records can also be deconstructed if all properties are in positional parameters.
Pattern matching is not limited to switch — it can check types and properties at the same time:
string Deskripsi(IPembayaran p) => p switch
{
PembayaranKartu => "Kartu",
_ => "Lainnya"
};The pattern PembayaranKartu => "Kartu" matches the object type. With a combination of records, pattern matching, and switch expressions, C# can write declarative, readable domain logic.
Tip
Use records for pure data, classes for behavior with state, and interfaces for contracts between modules. This combination keeps your model simple.
A summary of design decisions:
Key takeaways:
with expression creates a copy of a record with partial changes.In the next episode 6 we will discuss exception handling and debugging — try, catch, finally, custom exceptions, using declarations, dispose patterns, debugging in VS Code, and basic logging with Microsoft.Extensions.Logging.