Objektově orientované programování (OOP) je dnes jedním z nejrozšířenějších přístupů při tvorbě softwaru. Ať už je tvořena desktopová aplikaci, backendové služby nebo mobilní aplikaci, tak OOP nabízí nástroje, jak organizovat složitost velkých systémů.

Cílem OOP je modelovat svět kolem nás pomocí objektů — entit, které kombinují stav (data) a chování (metody). Bylo vytvořeno s cílem zlepšit čitelnost kódu, zvýšit jeho znovu použitelnost a usnadnit údržbu.

Datový typ object

Datový typ object (resp. System.Object) je v C# základní typ, z kterého všechny typy dědí a jakýkoliv typ je možné na něj převést.

Tento typ definuje základní metody:

  • ToString() - Převádí objekt na řetězec.
  • Equals() - Porovnává dva objekty na rovnost. Ve výchozím stavu se jedná o referenční porovnání. Pokud se přepisuje, tak by se měl přepsat i GetHashCode().
  • GetHashCode() - Vrací číselný hash, který se používá např. v Dictionary pro identifikaci a porovnávání objektů.
  • GetType() - Vrací objekt typu Type, který reprezentuje informaci o konkrétním typu instance (v době běhu aplikace).

Boxing

Pojem, který označuje převod jednoduchého datového typu na referenční. Je realizován obalením jednoduchého datového typu typem object. Unboxing je opačná operace - tzn. rozbalení obaleného jednoduchého datového typu.

Tímto způsobem je možné uložit hodnotový datový typ na haldu.

Ukázka boxing

int i = 42;
 
// Boxing: hodnota typu int se zabalí do objektu typu object
object boxed = i;
 
Console.WriteLine(boxed); // Výstup: 42
Console.WriteLine(boxed.GetType()); // Výstup: System.Int32

Ukázka unboxing

object boxed = 42;
 
// Unboxing: hodnota se vybalí zpět do typu int
int j = (int)boxed;
 
Console.WriteLine(j); // Výstup: 42

Pilíře OOP

OOP se skládá ze 3, resp. někde lze nalézt i 4 pilířů:

  1. Zapouzdření (Encapsulation)
  2. Dědičnost (Inheritance)
  3. Polymorfismus (Polymorphism)
  4. Abstrakce (Abstraction)

1. Zapouzdření (Encapsulation)

Zapouzdření slouží ke skrytí detailů implementace objektu před vnějším světem. Třída zveřejní pouze rozhraní (public členy - metody, vlastnosti, atd.), které dovoluje ostatním třídám s objektem pracovat, zatímco interní stav nebo implementace zůstávají chráněné. Ostatní vývojáři by pro využívání třídy neměli potřebovat vědět, jak třída funguje uvnitř.

Význam

  • Ochrana dat před nechtěnou manipulací
  • Umožňuje měnit implementaci bez dopadu na okolní kód
  • Snižuje propojení mezi částmi systému

Pro realizaci zapouzdření se v C# používají modifikátory přístupu:

  • public - přístupné odkudkoliv
  • internal - přístupné v rámci projektu
  • protected - přístupné v třídě a jejích potomcích
  • private - přístupné pouze v rámci třídy

Ukázka

public class BankAccount
{
    private decimal balance;
 
    public void Deposit(decimal amount)
    {
        if (amount <= 0)
            throw new ArgumentException("Amount must be positive");
 
        balance += amount;
    }
 
    public void Withdraw(decimal amount)
    {
        if (amount > balance)
            throw new InvalidOperationException("Insufficient funds");
 
        balance -= amount;
    }
 
    public decimal GetBalance()
    {
        return balance;
    }
}

2. Dědičnost (Inheritance)

Dědičnost umožňuje vytvořit novou třídu (potomka) na základě existující třídy (rodiče). Smyslem potomků je rozšířit funkce rodiče.

Potomek dědí členy rodiče a může:

  • přidávat nové vlastnosti/metody
  • měnit (přepisovat) chování zděděných metod

Syntaxe dědičnosti

public class Potomek : Rodic
{
}

Význam

  • Opakované využití kódu
  • Sdílení společného chování mezi více třídami
  • Umožňuje hierarchickou organizaci tříd

Typy

  • Jednoduchá dědičnost - jedna třída může mít nejvýše jednoho rodiče
    • Pokud není rodič explicitně nastaven, využívá se implicitní rodič - object
    • Reprezentanti: C#, Java…
  • Násobná dědičnost - jedna třída může mít více rodičů
    • Větší volnost v nastavování rodičovství tříd
    • Přináší problémy jak správně sestavit hierarchii tříd
    • Reprezentanti: C++, Python

Ukázka

public class Animal
{
    private int _age;
 
    public Animal(int age)
    {
        _age = age;
    }
 
    public string Name { get; set; }
 
    public void Eat()
    {
        Console.WriteLine($"{Name} is eating.");
    }
}
 
public class Dog : Animal
{
    public Dog(int age) : base(age) // předání age konstruktoru rodiče
    {
    }
    
    public void Bark()
    {
        Console.WriteLine($"{Name} says Woof!");
    }
}

Ukázka použití:

var dog = new Dog(7);
dog.Name = "Buddy";
dog.Eat();     // zděděno z Animal
dog.Bark();    // definováno v Dog

Klíčové slovo base

Pro volání konstuktoru rodiče nebo explicitní specifikaci, že se má volat metoda v rodiči lze využít klíčové slovo base.

base.ParentMethodName();

3. Polymorfismus (Polymorphism)

Polymorfismus znamená česky mnohotvárnost a umožňuje použít různé objekty stejným způsobem. Jinými slovy: různé třídy implementují stejné rozhraní nebo přepisují metody rodiče, ale každá se chová jinak.

Například v e-shopu mohou různé platební metody (karta, převod, kryptoměna) sdílet stejnou metodu Pay(), ale každá ji provede jinak. Program přitom neřeší, jaká konkrétní metoda se používá – pracuje s nimi jednotně.

Obsahuje dva základní přístupy:

  1. Přetěžování metod
  2. Přepisování metod

Info

Takovéto dělení polymorfismu je velké zjednodušení pro účely těchto stránek. Pro správné dělení by bylo nutné zavést mnoho nových pojmů a jednalo by se o zbytečnou komplikaci velmi nad rámec těchto stránek.

3.1 Přetěžování metod (Method overloading)

Umožňuje vytvořit více metod se stejným názvem a různým počtem nebo datovými typy parametrů. Návratový datový typ musí být vždy stejný. Jednotlivé metody musí být od sebe kompilátorem odlišitelné.

public Color GetColor(int r, int g, int b)
{
	//...
}
 
public Color GetColor(string hex)
{
	// ...
}

3.2 Přepisování metod (Method overriding)

Potomek nahradí implementaci metody v rodiči vlastní implementací. Přepisovat je možné pouze metody, které jsou:

  • virtuální
    • Klíčové slovo virtual
    • Obsahuje výchozí implementaci
    • Přepsání takovéto metody není povinné. Pokud není přepsána, tak se využije výchozí implementace.
    • Např. metoda ToString() na datovém typu object
  • abstraktní
    • Klíčové slovo abstract
    • Neobsahuje výchozí implementaci
    • Nutné ji přepsat vždy ve třídě, která není abstraktní a dědí z abstraktní třídy

Ukázka výchozí implementace metody ToString

public virtual string ToString()
{
   return this.GetType().FullName;
}

Význam

  • Umožňuje tvorbu obecného kódu pracujícího s různými typy
  • Zvyšuje flexibilitu architektury
  • Podporuje princip open/closed – systém je otevřený pro rozšíření, ale uzavřený pro změny

Ukázka

public class Animal
{
    public virtual void Speak()
    {
        Console.WriteLine("Some generic animal sound.");
    }
}
 
public class Dog : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Woof!");
    }
}
 
public class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine("Meow!");
    }
}
 
public class Parrot : Animal { }

Ukázka použití:

Animal myAnimal;
 
myAnimal = new Dog();
myAnimal.Speak(); // Woof!
 
myAnimal = new Cat();
myAnimal.Speak(); // Meow!
 
myAnimal = new Parrot();
myAnimal.Speak(); // Some generic animal sound.

Chybějící použití polymorfismu

Při každém přidání dalšího potomka třídy Animal je nutné projít všechen kód a přidat do if/switch potřebné větve kódu s tím, co se má stát, když se metoda zavolá pro nový typ

using System;
 
public class Animal
{
    public string Name { get; set; }
}
 
public class Dog : Animal { }
 
public class Cat : Animal { }
 
public class AnimalSpeaker
{
    public void MakeAnimalSpeak(Animal animal)
    {
        if (animal is Dog)
        {
            Console.WriteLine("Woof!");
        }
        else if (animal is Cat)
        {
            Console.WriteLine("Meow!");
        }
        else
        {
            Console.WriteLine("Some generic animal sound.");
        }
    }
}

Abstrakce

Abstrakce umožňuje skrýt složitost a nabídnout pouze nezbytné rozhraní pro použití funkcionality. V OOP se to často realizuje pomocí:

  • abstraktních tříd (abstract class)
  • rozhraní (interface)

Význam

  • Definuje smlouvu (anglicky: contract), kterou musí třídy implementovat
  • Usnadňuje testování a výměnu komponent
  • Odděluje „co děláme” od „jak to děláme”

Abstraktní třídy

Abstraktní třídy jsou třídy, které jsou označeny pomocí klíčového slova abstract. Společného mají to, že podporují všechny funkce tříd - i např. konstruktory. Na rozdíl od běžných tříd se ale jedná o:

  • nedokončený typ, který nelze přímo instanciovat
  • mohou obsahovat:
    • abstraktní členy (bez implementace) → potomek je musí implementovat
    • implementované členy (tj. metody s kódem)
  • umožňuje sdílet společné chování (logiku) mezi různými potomky

Abstraktní metody

Abstraktní metody jsou metody, u kterých není definována implementace v rodičovi, ale potomci si ji musí dodefinovat.

Kdy musí být třída abstraktní?

Pokud má třída alespoň jeden abstraktní člen (metodu, vlastnost, atd.), tak musí být také abstraktní.

Ukázka

public abstract class Shape
{
    public string Color { get; set; }
 
    // abstraktní metoda - potomek MUSÍ implementovat
    public abstract double GetArea();
 
    // běžná metoda - potomek může použít rovnou
    public void PrintColor()
    {
        Console.WriteLine($"Shape color is {Color}");
    }
}
 
public class Circle : Shape
{
    public double Radius { get; set; }
 
    public override double GetArea()
    {
        return Math.PI * Radius * Radius;
    }
}

Ukázka použití:

Shape shape = new Circle
{
    Radius = 5,
    Color = "Red"
};
 
shape.PrintColor(); // Shape color is Red
Console.WriteLine(shape.GetArea()); // 78.5398...

Rozhraní

Rozhraní se označuje pomocí klíčového slova interface a:

  • definuje pouze smlouvu (signatury metod, vlastností, eventů)
  • neobsahuje žádnou implementaci (výjimkou jsou default metody od C# 8.0)
  • třída nebo struktura může implementovat více rozhraní najednou

Dědit vs. implementovat

Třídy se dědí, ale rozhraní se implementují!

Ukázka

public interface ILogger
{
    void Log(string message);
}
 
public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[LOG] {message}");
    }
}

Ukázka použití:

ILogger logger = new ConsoleLogger();
logger.Log("Application started.");

Srovnání abstraktních tříd a rozhraní

VlastnostAbstraktní třídaRozhraní
Instanciovatelná?NeNe
Může obsahovat implementaci?AnoOd C# 8.0 (default)
Může obsahovat uložení stav (např. pole, proměnná…)?AnoNe
Vícenásobná “dědičnost”?NeAno (více rozhraní)
Slouží ke sdílení implementace?AnoNe
Slouží k definici smlouvy?Ano i NeAno