Skip to content

Introduktion Arv

Arv möjliggör återanvändning av kod genom att låta en klass ärva egenskaper och metoder från en annan klass. Klassen som ärver kallar vi subklass och klassen vi ärver ifrån basklass. Vi kan se det som att subklassen är en utökning eller en specialisering av basklassen.

Om vi tar klasserna DeckOfCards och Card från artikeln Introduktion till Objektorienterad programmering och funderar på hur vi kan utöka dem. Vi kan till exempel utöka klassen Card till att visa spelkorten grafiskt och skapa en klass CardGraphic som ärver från Card. Card är då basklass och CardGraphic subklass. Vi kan säga att ett grafiskt spelkort “är-ett” spelkort. Vi skulle också kunna göra en subklass till klassen DeckOfCards som innehåller jokrar och kalla den DeckOfCardsWithJokers. En kortlek med jokrar “är-en” kortlek.

För att visa arv så utökar vi Bankexemplet från artikel Introduktion till UML och klassdiagram från kmom02. Vi introducerar ett lönekonto och eftersom “lönekonto är-ett Bankkonto” så är det arv. Dessutom introducerar vi ett räntesparkonto, som också ärver från Bankkonto. De nya kontona har olika aktiviteter varje månad, på räntekontot så får vi ränta varje månad och på lönekontot tas en avgift varje månad.

Skapa gärna ett projekt MyBank under “kmom05/practice” och testkör koden.

Vi vill specialisera vårt bankkonto till att vara ett räntekonto eller ett lönekonto. Ett räntekonto “är-ett” bankkonto och därför kan räntekonto ärva bankkonto. Då återanvänder vi koden i klassen BankAccount och gör två subklasser InterestAccount och PayrollAccount och utökar dessa subklasser med det som är specifikt för respektive kontotyp. Om vi vill lägga till en tredje kontotyp är det enkelt att göra.

Klassdiagram för BankAccount, InterestAccount och PayrollAccount

Section titled “Klassdiagram för BankAccount, InterestAccount och PayrollAccount”

I ett klassdiagram visar vi arv med en pil från subklassen till basklassen. Pilspetsen är inte ifylld.

Image description
Bild: Klassdiagram BankAccount med arv.

Klassen BankAccount behöver vi inte uppdatera den skapade vi i kmom03 men vi behöver skapa de två subklasserna.

Skapa klassen InterestAccount i src-katalogen
namespace MyBank.src;
public class InterestAccount : BankAccount
{
public InterestAccount(string name, decimal initialBalance) : base(name, initialBalance)
{
}
public void PrintDescription()
{
Console.WriteLine($"Detta räntekonto skapades till {_owner} med saldot {_balance} kr.");
}
}
Skapa klassen PayrollAccount i src-katalogen
namespace MyBank.src;
public class PayrollAccount : BankAccount
{
public PayrollAccount(string name, decimal initialBalance) : base(name, initialBalance)
{
}
public void PrintDescription()
{
Console.WriteLine($"Detta lönekonto skapades till {_owner} med saldot {_balance} kr.");
}
}

Med konstruktionen : BankAccount visar vi att klassen ärver från BankAccount och med konstruktionen : base (direkt efter konstruktorns parameterlista) visar vi att vi anropar basklassens konstruktor.

Subklasserna kommer åt basklassens attribut och metoder som är publika (public) och som är skyddade (protected) men inte de som är privata (private). Nu testar vi att vi kommer åt skyddade attribut men inte privata attribut.

Lägg följande rader i huvudprogrammet i Program.cs
using MyBank.src;
var account = new BankAccount("Marie", 1000);
account.PrintAccountDescription();
var payroll = new PayrollAccount("Marie BTH", 10000);
payroll.PrintAccountDescription();
payroll.PrintDescription();
var interest = new InterestAccount("Marie savings", 120000);
interest.PrintAccountDescription();
interest.PrintDescription();

När vi försöker köra ovan så ser vi att det går bra att komma åt basklassens skyddade variabel men inte den privata. Vi får felmeddelandet “error CS0122: ‘BankAccount._balance’ is inaccessible due to its protection level”. Vi behöver komma åt beloppet på kontot i subklasserna och ändrar därför variabeln till att ha åtkomstnivån protected istället.

Byt från private till protected i BankAccount på _owner och _balance
namespace MyBank.src;
public class BankAccount
{
private static int _nextAccountNumber = 100000;
protected string _owner;
protected decimal _balance;
...

När vi nu testkör programmet så får vi följande utskrift:

dotnet run ger följande utskrifter i terminalen
Kontot med nummer 100000 skapades till Marie med saldot 1000 kr.
Kontot med nummer 100001 skapades till Marie BTH med saldot 10000 kr.
Detta lönekonto skapades till Marie BTH med saldot 10000 kr.
Kontot med nummer 100002 skapades till Marie savings med saldot 120000 kr.
Detta räntekonto skapades till Marie savings med saldot 120000 kr.

Raderna som börjar med “Kontot…” kommer från basklassen och raderna som börjar med “Detta…” kommer från subklassen. Åtkomstnivån protected används i arv då subklasserna ska komma åt basklassens metoder och attribut.

De olika kontotyperna har olika aktiviteter i slutet av månaden men själva aktiviteten är olika för de olika kontona. Då kan vi skapa en metod, PerformMonthEndTransactions(), i basklassen som vi sedan överskuggar (override) i subklasserna. För att visa att vi vill att en metod ska överskuggas så markerar vi den virtual i basklassen. När metoden är virtual i basklassen så är det ett erbjudande om att subklassen kan överskugga metoden. Om metoden ska överskuggas i subklassen så används override subklassens metod.

Klassdiagram för BankAccount, InterestAccount och PayrollAccount

Section titled “Klassdiagram för BankAccount, InterestAccount och PayrollAccount”
Image description
Bild: Klassdiagram BankAccount med metoden PerformMonthEndTransactions markerad virual och subklasserna InterestAccount and PayrollAccount med PerformMonthEndTransactions markerad override.

Vi skapar en virtuell metod PerformMonthEndTransactions() i klassen BankAccount som returnerar false som standard. Eftersom vi har en returtyp på PerformMonthEndTransactions() så måste vi ha en return-sats i den.

Lägg till en virtual metod PerformMonthEndTransactions i klassen BankAccount
namespace MyBank.src;
public class BankAccount
{
private static int _nextAccountNumber = 100000;
protected string _owner;
protected decimal _balance;
private string _accountNumber;
public BankAccount(string name, decimal initialBalance)
{
_owner = name;
_balance = initialBalance;
_accountNumber = _nextAccountNumber.ToString();
_nextAccountNumber++;
}
...
public virtual bool PerformMonthEndTransactions() { return false; }
}

Nu ska vi implementera metoden i subklasserna. I klassen InterestAccount så får alla en liten ränta och de som har mer pengar får mer ränta. Räntan sätts även in på kontot.

Klassen InterestAccount gör override på metoden PerformMonthEndTransactions
namespace MyBank.src;
public class InterestAccount : BankAccount
{
public InterestAccount(string name, decimal initialBalance) : base(name, initialBalance)
{
}
public override bool PerformMonthEndTransactions()
{
decimal interest = 0.0015m;
if (this._balance > 100m)
{
interest = this._balance * 0.02m;
}
return Deposit(interest);
}
}

På lönekontot erbjuder Banken olika tjänster och därför tar de ut en liten avgift varje månad. De som har mycket pengar på lönekontot betalar en större avgift. Notera att vi lägger till ett m efter flyttalet för att få typen “decimal” som används vid beräkningar med stor precision.

Klassen PayrollAccount gör override på metoden PerformMonthEndTransactions
namespace MyBank.src;
public class PayrollAccount : BankAccount
{
public PayrollAccount(string name, decimal initialBalance) : base(name, initialBalance)
{
}
public override bool PerformMonthEndTransactions()
{
decimal fee = 0.0m;
if (this._balance > 100m)
{
fee = 0.001m;
}
return Withdraw(fee);
}
}

För att testa implementationen har vi denna kod i huvudprogrammet:

Vi testar metoden PerformMonthEndTransactions i huvudprogrammet
using MyBank.src;
var account = new BankAccount("Marie", 1000);
account.PrintAccountDescription();
var payroll = new PayrollAccount("Marie BTH", 10000);
payroll.PerformMonthEndTransactions();
payroll.PrintAccountDescription();
var interest = new InterestAccount("Marie savings", 120000);
interest.PerformMonthEndTransactions();
interest.PrintAccountDescription();
När vi nu testkör programmet så får vi följande utskrift:
Kontot med nummer 100000 skapades till Marie med saldot 1000 kr.
Kontot med nummer 100001 skapades till Marie BTH med saldot 9999.999 kr.
Kontot med nummer 100002 skapades till Marie savings med saldot 122400.00 kr.

“Exponera inte data — exponera beteende.” Fundera alltid på vad subklassen behöver göra, inte vilka attribut den ska kunna ändra.

Varför protected attribut ofta är dålig design?

Section titled “Varför protected attribut ofta är dålig design?”

Att exponera attribut direkt (även som protected till subklassen) skapar flera problem:

  1. Subklasserna blir hårt beroende av basklassens implementation. Om basklassen ändrar hur datan ska lagras eller valideras så riskerar du att subklasser går sönder.

  2. En basklass vill ofta garantera att dess tillstånd alltid är “giltigt”. Om subklasser kan manipulera attribut direkt är det svårt att upprätthålla dessa regler.

  3. Arv gör klasser mer sammanbundna. Ju mer du ger subklassen direkt åtkomst till basklassens data, desto mer låser du designen.

private attribut i kombination med protected metoder ger bättre kontroll.

Skydda tillstånd via:

  • private attribut
  • protected metoder, om subklassen bara ska utöka beteende

I den här artikeln har vi tittat på arv i C# och hur vi visar det i ett UML klassdiagram. Vi har lärt oss begrepp som basklass och subklass samt metodöverskuggning.
Om du vill titta på koden i sin helhet och ladda ner den så kör du task download-code -- kmom05/MyBank.