Keri sisuni

Pärilus

Pärilus (inheritance) tähendab seda, et üks klass võib pärida ennast teisest klassist. Klassi, millest päritakse, kutsutakse päriva klassi baasklassiks (base class). Pärimise käigus saab päriv klass endale kõik baasklassi liikmed, mille nähtavus on public, protected või internal. Baasklassi privaatseid osi päriv klass ei näe. Pärilust tähistatakse selliselt, et klassi nime järele kirjutatakse koolon ning peale seda baasklassi nimi.

public class Vehicle
{
    public string Name { get; set; }
}

public class Car : Vehicle // Car pärib klassist Vehicle
{
    public void Drive()
    {
        // ...
    }
}

public class Airplane : Vehicle // Airplain pärib klassist Vehicle
{
    public void Fly()
    {
        // ...
    }
}

var car = new Car { Name = "Ford Mustang" };
var plane = new Airplane { Name = "Airbus 320" };

Console.WriteLine(car.Name);
Console.WriteLine(plane.Name);

Pärilusseosed ei ole juhuslikud ega meelevaldsed. See, et kahel klassil on olemas mõned samanimelised liikmed, ei täheda seda, et need kaks klassi peaksid omama pärilusseoseid. Oleks ju päris jabur kui baasklassist Vehicle pärime näiteks klassi Dog või Cat. Klasside modelleerimisel me vaatame tavaliselt päris maailma pealt maha ja kasutame samu seoseid, mis seal eksisteerivad. Loomulikult tuleb teinekord ise leiutada ja luua abstraktsioone, kuid neil kõigil on kindel tähendus, mõte ja loogika.

Klasside modelleerimine on mõttestatud tegevus

Klasside modelleerimisel me tavaliselt ei katseta ja ei eksperimenteeri pärilusseostega. Enne tulevad mõtted ja ideed ning seejärel katsed, mis on põhjendatud. See ei tähenda, et mängida ja proovida ei tohiks. Tohib küll, nii võib isegi midagi avastada, kuid see on pime õnn.

Virtuaalsed liikmed

Pärilus ei tähenda ainult seda, et päriv klass saab kasutada baasklassi omadusi, meetode ja muid liikmeid. Võimalik on ka baasklassi liikmete asendamine kui baasklass seda võimaldab. See tähendab seda, et päriv klass defineerib oma versiooni mõnest baasklassi liikmest.

public class Vehicle
{
    public string Name { get; set; }

    // Virtuaalne meetod - seda saab päriv klass asendada
    public virtual void Move()
    {
        Console.WriteLine("Moving");
    }

    // Mittevirtuaalne meetod
    public void Stop()
    {
        Console.WriteLine("Stopped");
    }
}

// Kasutab kõiki baasklassi liikmeid
public class Bus : Vehicle
{
}

// Defineerib oma versiooni Move() meetodist
public class Car : Vehicle
{
    public override void Move()
    {
        Console.WriteLine("Driving");
    }
}    

// Defineerib oma versiooni Move() meetodist
public class Airplane : Vehicle
{
    public override void Move()
    {
        Console.WriteLine("Flying");
    }
}

var bus = new Bus { Name = "Setra" };
var car = new Car { Name = "Ford Mustang" };
var plane = new Airplane { Name = "Airbus 320" };

bus.Move();
car.Move();
plane.Move();
using System;

namespace OOP
{
    public class Vehicle
    {
        public string Name { get; set; }

        public virtual void Move()
        {
            Console.WriteLine("Moving");
        }

        public void Stop()
        {
            Console.WriteLine("Stopped");
        }
    }

    public class Bus : Vehicle
    {
    }

    public class Car : Vehicle
    {
        public override void Move()
        {
            Console.WriteLine("Driving");
        }
    }

    public class Airplane : Vehicle
    {
        public override void Move()
        {
            Console.WriteLine("Flying");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var bus = new Bus { Name = "Setra" };
            var car = new Car { Name = "Ford Mustang" };
            var plane = new Airplane { Name = "Airbus 320" };

            bus.Move();
            car.Move();
            plane.Move();
        }
    }
}

Polümorfism

Polümorfism on objekti võime olla mitmel erineval kujul. See tähendab seda, et me saame kõiki pärivaid klasse kasutada samas kohas, kus nende baasklasse.

Selle olulise reegli valguses saame kirjutada ülaltoodud koodi ka foreach-tsükli abil. Me loome massiivi Vehicle-tüüpi objektidest ning lisame sinna sisse Bus, Car ja Airplane objektid.

var vehicles = new Vehicle[] 
{
    new Bus { Name = "Setra" },
    new Car { Name = "Ford Mustang" },
    new Airplane { Name = "Airbus 320" }
};

foreach(var vehicle in vehicles)
{
    vehicle.Move();
}

Mittevirtuaalsete liikmete asendamine

Mittevirtuaalsete liikmete asendamine pärivates klassides võimalik ei ole, kuigi tundub, et ega keegi ei keela. Me ei saa küll override märksõna kasutada, kuid kui selle ära jätame, siis saame näiteks Bus-klassile luua tema enda Stop() meetodi ja see tundub esmapilgul töötavat ka.

public class Vehicle
{
    public string Name { get; set; }

    // Virtuaalne meetod - seda saab päriv klass asendada
    public virtual void Move()
    {
        Console.WriteLine("Moving");
    }

    // Mittevirtuaalne meetod
    public void Stop()
    {
        Console.WriteLine("Vehicle stopped");
    }
}

public class Bus : Vehicle
{
    public void Stop()
    {
        Console.WriteLine("Bus stopped");
    }
}

var bus = new Bus { Name = "Setra" };
bus.Stop();

Tulemus on ootuspärane:

Bus stopped

Järgmine koodijupp aga annab ootamatud tulemused

Vehicle bus1 = new Bus { Name = "Setra" };
Bus bus2 = new Bus { Name = "Scania" };

bus1.Stop();
bus1.Stop();

Tulemuseks on:

Vehicle stopped
Bus stopped

Mis juhtus? Kui me ei kasuta virtual ja override, siis puudub meil kahe meetodi vahel pärilusseos ning kasutatava meetodi määrab ära muutuja tüüp. Esimesel juhul oli tüübiks Vehicle ja Stop() meetod kutsuti Vehicle küljest. Teisel juhul oli muutuja defineeritud kui Bus ja sel juhul kutsuti Move() meetod Bus küljest.

Abstraktne klass

Baasklasside abil saame luua pärilushierarhiaid. Praktikas on tihti vaja baasklasse, millest uusi instantse luua ei saa. Sellisel juhul kasutatakse abstraktseid klasse. Teeme eelmistest näidetest pärit Vehicle klassi abstraktseks. Selleks kasutame abstract märksõna.

public abstract class Vehicle
{
    public string Name { get; set; }

    // Virtuaalne meetod - seda saab päriv klass asendada
    public virtual void Move()
    {
        Console.WriteLine("Moving");
    }

    // Mittevirtuaalne meetod
    public void Stop()
    {
        Console.WriteLine("Vehicle stopped");
    }
}

Muus osas jäi Vehicle klass samaks.

Abstraktne klass võib defineerida ka abstraktsed liikmed. Muudame Vehicle klassi selliselt, et Name omadus on abstraktne, kuid ei ole omistatav.

public abstract class Vehicle
{
    public abstract string Name { get; }

    // Virtuaalne meetod - seda saab päriv klass asendada
    public virtual void Move()
    {
        Console.WriteLine("Moving");
    }

    // Mittevirtuaalne meetod
    public void Stop()
    {
        Console.WriteLine("Vehicle stopped");
    }
}

Nüüd peavad kõik pärivad klassid defineerima ise omaduse Name.

public abstract class Vehicle
{
    public abstract string Name { get; }

    public virtual void Move()
    {
        Console.WriteLine("Moving");
    }

    public void Stop()
    {
        Console.WriteLine("Stopped");
    }
}

public class Bus : Vehicle
{
    public override string Name 
    { 
        get { return "Setra"; } 
    }
}

public class Car : Vehicle
{
    public override string Name
    {
        get { return "Ford Mustang"; }
    }

    public override void Move()
    {
        Console.WriteLine("Driving");
    }
}

public class Airplane : Vehicle
{
    public override string Name
    {
        get { return "Airbus 320"; }
    }

    public override void Move()
    {
        Console.WriteLine("Flying");
    }
}

var vehicles = new Vehicle[] { new Bus(), new Car(), new Airplane() };

foreach(var vehicle in vehicles)
{
    Console.WriteLine(vehicle.Name);
}
using System;

namespace OOP
{
    public abstract class Vehicle
    {
        public abstract string Name { get; }

        public virtual void Move()
        {
            Console.WriteLine("Moving");
        }

        public void Stop()
        {
            Console.WriteLine("Stopped");
        }
    }

    public class Bus : Vehicle
    {
        public override string Name 
        { 
            get { return "Setra"; } 
        }
    }

    public class Car : Vehicle
    {
        public override string Name
        {
            get { return "Ford Mustang"; }
        }

        public override void Move()
        {
            Console.WriteLine("Driving");
        }
    }

    public class Airplane : Vehicle
    {
        public override string Name
        {
            get { return "Airbus 320"; }
        }

        public override void Move()
        {
            Console.WriteLine("Flying");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var vehicles = new Vehicle[] 
            { 
                new Bus(), new Car(), new Airplane() 
            };

            foreach (var vehicle in vehicles)
            {
                Console.WriteLine(vehicle.Name);
            }
        }
    }
}

Koodi käivitamisel saame tulemuseks

Setra
Ford Mustang
Airbus 320

Mittepäritav klass

On juhtumeid, kus me soovime, et mõnda klassi baasklassina kasutada ei saaks. Ehk siis meie sooviks on keelata antud klassist pärimine. Selleks kasutame sealed märksõna klassid definitsioonis. Järgnevas näiteks ei saa baasklassina kasutada klassi Bus.

public abstract class Vehicle
{
    public abstract string Name { get; }

    public virtual void Move()
    {
        Console.WriteLine("Moving");
    }

    public void Stop()
    {
        Console.WriteLine("Stopped");
    }
}

public sealed class Bus : Vehicle
{
    public override string Name 
    { 
        get { return "Setra"; } 
    }
}

Haruldane juhtum

Üldiselt luuakse mittepäritavaid klasse harva ja enamasti on põhjuseks keerukamad tehnilised piirangud. Igapäevases arendustöös nendega tihti kokku ei puutu.

Praktiline näide

Praktilise näitena vaatame lihtsat arvete genereerimise klassi, mis kirjutab logisse oma tegemiste kohta infot. Sõltuvalt rakenduse seadetest soovime kasutada erinevaid logimise klasse. Olgu meil neid kaks tükki:

  • DebugLogger - kirjutab logiteated Visual Studio debug-aknasse
  • ConsoleLogger - kirjutab logiteated konsoolile
public class DebugLogger
{
    public void Info(string message)
    {
        Debug.WriteLine("INFO: " + message);
    }

    public void Error(string message)
    {
        Debug.WriteLine("ERROR: " + message);
    }
}

public class ConsoleLogger
{
    public void Info(string message)
    {
        Console.WriteLine("INFO: " + message);
    }

    public void Error(string message)
    {
        Console.WriteLine("ERROR: " + message);
    }
}

Kui need klassid omavahel pärilusseoste abil ühendatud pole, siis peaksime kirjutama kas kaks arvete genereerimise klassi või looma sellise arvete genereerimise klassi, mis mõlemat loggerit kasutada oskab. Mõlemad mainitud lähenemised oleksid äärmiselt kohmakad. Lisaks tekiks tänu nendele hiljem hulganisti koodi, mida saaksime päriluse abil vältida. Kui tegemist oleks nüridate otsustega, vaatame siiski hilisema võrdluse huvides ära, milline see halb kood välja näeks.

public class InvoiceGeneratorWithDebugLogger
{
    private readonly DebugLogger _logger;

    public InvoiceGenerator(DebugLogger logger)
    {
        _logger = logger;
    }

    public void Run()
    {
        _logger.Info("Alustasin arvete genereerimisega");

        // Genereeri arved

        _logger.Info("Arved on genereeritud");
    }
}

public class InvoiceGeneratorWithConsoleLogger
{
    private readonly ConsoleLogger _logger;

    public InvoiceGenerator(ConsoleLogger logger)
    {
        _logger = logger;
    }

    public void Run()
    {
        _logger.Info("Alustasin arvete genereerimisega");

        // Genereeri arved

        _logger.Info("Arved on genereeritud");
    }
}

var generator1 = new InvoiceGeneratorWithDebugLogger(new DebugLogger());
var generator2 = new InvoiceGeneratorWithConsoleLogger(new ConsoleLogger());

generator1.Run();
generator2.Run();
public class InvoiceGenerator
{
    private readonly DebugLogger _debugLogger;
    private readonly ConsoleLogger _consoleLogger;

    public InvoiceGenerator(DebugLogger logger)
    {
        _debugLogger = logger;
    }

    public InvoiceGenerator(ConsoleLogger logger)
    {
        _consoleLogger = logger;
    }

    public void Run()
    {
        if(_consoleLogger == null)
        {
            _debugLogger.Info("Alustasin arvete genereerimisega");
        }
        else
        {
            _consoleLogger.Info("Alustasin arvete genereerimisega");
        }

        // Genereeri arved

        if(_consoleLogger == null)
        {
            _debugLogger.Info("Arved on genereeritud");
        }
        else 
        {
            _consoleLogger.Info("Arved on genereeritud");
        }
    }
}

var generator1 = new InvoiceGenerator(new DebugLogger());
var generator2 = new InvoiceGenerator(new ConsoleLogger());

generator1.Run();
generator2.Run();

Optimeerimisest pole abi

Kuigi teises näites toodud koodi annaks optimeerida ja muuta lühemaks, ei lahenda see ikkagi ära probleemi. Arvete klass tunneb mõlemat loggerit ja sõltuvalt sellest on klassi mõnes meetodis ikkagi olemas kontrollid selle kohta, millist loggerit kasutada. Me ei pääse keerukustest, mis kaasnevad näiteks sellega kui tuleb juurde uusi logimise klasse (näiteks failidesse logimine, logiserverisse logimine, mõnda telemeetriateenusesse logimine).

Me saame logimise klassid viia samasse pärilushierarhiasse kui loome baasklassi, mida kõik logimise klassid laiendavad. Loome klassi nimega BaseLogger.

public abstract class BaseLogger
{
    public abstract void Info(string message);
    public abstract void Error(string message);
}

public class DebugLogger : BaseLogger
{
    public void Info(string message)
    {
        Debug.WriteLine("INFO: " + message);
    }

    public void Error(string message)
    {
        Debug.WriteLine("ERROR: " + message);
    }
}

public class ConsoleLogger : BaseLogger
{
    public void Info(string message)
    {
        Console.WriteLine("INFO: " + message);
    }

    public void Error(string message)
    {
        Console.WriteLine("ERROR: " + message);
    }
}

Nüüd saame arvete genereerimise klassi kirjutada selliselt, et täpsem loggeri tüüp seda ei huvita.

public class InvoiceGenerator
{
    private readonly BaseLogger _logger;

    public InvoiceGenerator(BaseLogger logger)
    {
        _logger = logger;
    }

    public void Run()
    {
        _logger.Info("Alustasin arvete genereerimisega");

        // Genereeri arved

        _logger.Info("Arved on genereeritud");
    }
}

var generator1 = new InvoiceGenerator(new DebugLogger());
var generator2 = new InvoiceGenerator(new ConsoleLogger());

generator1.Run();
generator2.Run();
using System;
using System.Diagnostics;

namespace OOP
{
    public abstract class BaseLogger
    {
        public abstract void Info(string message);
        public abstract void Error(string message);
    }

    public class DebugLogger : BaseLogger
    {
        public override void Info(string message)
        {
            Debug.WriteLine("INFO: " + message);
        }

        public override void Error(string message)
        {
            Debug.WriteLine("ERROR: " + message);
        }
    }

    public class ConsoleLogger : BaseLogger
    {
        public override void Info(string message)
        {
            Console.WriteLine("INFO: " + message);
        }

        public override void Error(string message)
        {
            Console.WriteLine("ERROR: " + message);
        }
    }

    public class InvoiceGenerator
    {
        private readonly BaseLogger _logger;

        public InvoiceGenerator(BaseLogger logger)
        {
            _logger = logger;
        }

        public void Run()
        {
            _logger.Info("Alustasin arvete genereerimisega");

            // Genereeri arved

            _logger.Info("Arved on genereeritud");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {
            var generator1 = new InvoiceGenerator(new DebugLogger());
            var generator2 = new InvoiceGenerator(new ConsoleLogger());

            generator1.Run();
            generator2.Run();
        }
    }
}

Viited