Hoppa till innehåll

Abstrakta klasser och interface

↑

Exemplen är oberoende C#-utdrag för moderna .NET-konsolprojekt, om inget annat anges. Klassdeklarationer och toppnivåkod kan placeras i samma Program.cs-fil, med toppnivåkoden först.

Abstraktion beskriver vad något kan göra utan att avslöja alla implementationsdetaljer. Abstrakta klasser kan dela tillstånd, medan interface beskriver förmågor.

Du kan inte skapa en instans direkt av en abstrakt klass. Den kan innehålla gemensam kod och kräva att härledda klasser implementerar abstrakta medlemmar. C# tillåter fortfarande bara en basklass.

Example.cs
public abstract class Shape
{
public abstract double Area();
public string Label() => $"Area = {Area()}";
}
public sealed class Circle : Shape
{
public double Radius { get; init; }
public override double Area() => Math.PI * Radius * Radius;
}

Ett interface beskriver ett kontrakt för typer som implementerar det. En klass kan implementera flera interface och därmed kombinera oberoende förmågor utan multipelt klassarv. Moderna interface kan ha standardimplementationer, men börja med tydliga kontrakt.

Example.cs
public interface IDamageable { void TakeDamage(int amount); }
public interface IResettable { void Reset(); }
public class Target : IDamageable, IResettable
{
private int health = 100;
public void TakeDamage(int amount) => health -= amount;
public void Reset() => health = 100;
}

Programmera mot ett litet interface när flera implementationer eller testdubblar behövs. Skapa inte ett interface för varje klass av vana. En konkret klass kan vara tydligare.

Följande kod använder IDamageable och Target från det föregående exemplet.

Example.cs
public static void Hit(IDamageable target)
{
target.TakeDamage(10);
}
Hit(new Target());

Fördjupning: gemensam implementation eller kontrakt?

Section titled “Fördjupning: gemensam implementation eller kontrakt?”

En abstrakt klass kan ge både regler och färdig kod

Section titled “En abstrakt klass kan ge både regler och färdig kod”

En abstract klass kan inte instansieras direkt. Den kan deklarera abstrakta medlemmar som underklasser måste implementera (om de inte själva är abstrakta), men också innehålla vanliga fält, metoder och konstruktorer som återanvänds. Använd den när flera objekttyper verkligen delar en gemensam grund och samma implementation.

Ett interface anger vilka operationer något erbjuder, inte vilken klass det måste ärva från. En klass kan implementera flera interface. Därför kan en dörr, en förstörbar låda och en spelare alla implementera IDamageable trots att de har olika klasshierarkier. Moderna C#-interface kan även innehålla standardimplementationer och statiska medlemmar, men huvudsyftet är fortfarande ett stabilt kontrakt.

Program.cs
IDamageable target = new TrainingDummy();
target.TakeDamage(12);
public interface IDamageable
{
void TakeDamage(int amount);
}
public sealed class TrainingDummy : IDamageable
{
public int Health { get; private set; } = 50;
public void TakeDamage(int amount)
=> Health = Math.Max(0, Health - amount);
}

Varför hjälper detta testning och underhåll?

Section titled “Varför hjälper detta testning och underhåll?”

Kod som tar emot en IDamageable-parameter behöver inte veta om objektet är en spelare eller en träningsdocka. Ett test kan skicka in en enkel testimplementation och kontrollera att skada faktiskt efterfrågades. Det är beroendeinversion i praktiken: anroparen beror på beteendet den behöver och inte på en viss konkret klass. Skapa hellre små interface med tydliga ansvarsområden än enorma interface där de flesta metoder är irrelevanta för hälften av klasserna.

Använd abstrakt klass för gemensam grundimplementation och interface för en roll som flera olika typer kan fylla.

Interfacemetoder kan inte automatiskt anropas på vilka objekt som helst; referenstypen och kontraktet avgör.

Definiera ISaveable med Save() och implementera det i två klasser som inte ärver från varandra.

Visa ett lösningsförslag

interface ISaveable { void Save(); } och därefter separata implementationer.