12 min read

Objekt Orienterad Programmering med C# Del 3: Kopplingar mellan klasser

Objekt Orienterad Programmering med C# Del 3: Kopplingar mellan klasser

Innehållsförteckning

Introduktion

Detta är del 3 i kursen objekt Objekt Orienterad Programmering med C#. Denna modul är steget innan vi ger oss in på Arv, Polymorphism och Interface begreppen.

Syftet med denna modul är att få en förståelse för de problem som vi kan stöta på när vi bygger en större applikationer genom att sätta ihop vara klasser(legobitar).

Vi kommer här gå igenom begrepp som Aggregation, Composition samt en snabb inblick i Inheritance(Arv).

Kopplingar mellan klasser

Som vi gick igenom i klasser så är våra klasser självförsörjande enheter och genom att sätta ihop dessa med varandra så skapar vi ett större system eller en större applikation. Då är det oundvikligt att koppla samman klasserna med varandra se nedan bild.

Coupling between classes

Vad vi ser på ovanstående bild är att Objekt-2 och Objekt-3 är beroende av Objekt-1. Vi kan även se att Objekt-4 är beroende av Objekt-3, Objekt-5 är beroende av Objekt-4 men även Objekt-6 är beroende av Objekt-4. Detta är inget ovanlig hierarki. På något sätt måste vi ansluta klasserna till varandra för att kunna kommunicera mellan klasserna.

Problemet är hur vi gör detta på ett korrekt sätt så att vi inte påverkar applikationen om ändring sker eller måste ske i någon av klasserna i hierarkin.

Vi brukar dela upp beroenden mellan klasserna i två kategorier

  • Tightly coupled
  • Loosely coupled

Tightly coupled

Tightly coupled eller hård koppling mellan klasserna betyder att vi kan få en domino effekt vid ändring i någon klass som påverkar de andra klasserna negativt.

Vi har ett beroende som är baserat på en viss implementering av klasserna i en klasshierarki.

Exempelvis:

Om vi leker med tanken att vi måste göra en ändring i klassen som representerar Objekt-1. Detta kommer förmodligen(garanterat) att påverka Objekt-2 och Objekt-3. Så att ändringar måste ske som anpassas efter ändringar gjorda i Objekt-1.

Tittar vi ytterligare i hierarkin så ser vi att Objekt-4 är beroende av Objekt-3 så där kommer vi också att behöva göra ändringar för att anpassas till de ändringar gjorda i Objekt-3 osv...

Detta leder alltid till att en applikation eller system kommer att landa i att vi får buggar och krascher. Samt leder till applikationer eller system som blir omöjliga att underhålla. Svåra att göra små ändringar i eller att byta ut någon del mot en ny komponent/klass.

Loosely coupled

Loosely coupled eller mjukt kopplade klasser är idén om att minimera påverkan på applikationen som sker vid en ändring i en klass.

Ändringar kommer alltid att behövas men vi vill att de påverkar resten av applikationen minimalt.

När vi ritar en klass eller objekt hierarki så representerar vi mjuka kopplingar med hjälp av streckade linjer.

Så en ändring i Objekt-1 ska/bör inte påverka de övrig klasserna i hierarkin.

Vi kommer att se hur detta ser ut i UML om lite längre fram.

Hur går vi tillväga?

För att skapa en applikation eller system som är konstruerat kring tankesättet loosely coupled/mjuka kopplingar måste vi förstå följande:

  • Inkapsling/Encapsulation
    • Detta har vi redan gått igenom när vi skapade klasser. Varje klass är ansvarig för sitt eget tillstånd/data. Varje klass hanterar internt varje ändring i tillståndet.
    • Varje klass har kunskap och beteenden som bara den egna klassen känner till.
    • Ett annat sätt är att se varje klass som en självförsörjande enhet.
  • Relationer mellan klasser
    • Som är fokus på denna modul
  • Interface
    • Är nog den enskilt viktigaste byggstenen i att skapa mjukt kopplade klasser. Vi kommer att spendera en hel del tid om detta i modulen om Interfaces.

Relationer mellan klasser

I Objekt Orienterad Programmering talar vi ofta om tre olika typer av relationer

  • Arv
    • Jag kommer att introducera er till arv i denna modul
      • I nästa modul ska vi kika lite djupare och i mer detalj
  • Composition
  • Aggregation
    • Vi har faktiskt redan sett exempel på aggregering utan att egentligen nämna det vid dess rätta namn. Vi har i vårt Invoice exempel haft list/collections/arrays med data i en klass.

Arv/Inheritance

Jag kommer här endast introducera er till konceptet arv i nästa modul kommer vi att gå in på detaljerna. Med anledning av att vi i denna modul går igenom kopplingar mellan klasser måste jag även ta upp arv. Vilket är ett sätt att koppla samman klasser.

Vad är arv?

Arv är en typ av relation mellan två klasser som tillåter specialisering genom att en klass kan ärva en annan klass tillstånd och beteende.

Kallas ibland för "Är-En/Är-Ett" eller "Är av typen" förhållande.

Exempel:

  • Ett sparkonto är en typ av Bankkonto
  • En bil är en typ av fordon

Varför arv?

Med hjälp av arv kan vi på ett ganska enkelt sätt återanvända kod. Observera att jag säger kan, jag har sett alldeles för mycket kod där man använder arv men tyvärr gör det på ett felaktigt sätt så att återanvändningen blir obefintlig.

En annan stor fördel, förmodligen den största, är att korrekt gjort så kan vi uppnå det som kallas "Polymorfistiskt" beteende eller som det heter i OOP sammanhang Polymorphic behavior. Vilket är extremt kraftfullt och underlättar samt gör vår kod mer lättläst och vi slipper duplicerad kod i klienterna.

Vi kommer att gå igenom Polymorfism i nästa modul.

Design av Arvs hierarkier

Låt oss ta kika på ett nytt exempel som kommer ifrån min verksamhet. Jag har skapa och byggt ett system för ett europeiskt fordons företag. I det projektet så skulle vi skapa en hantering av olika typer av fordon:

  • Bilar
  • Lastbilar
  • Transport bilar
  • Dragbilar med släp

För att göra det enkelt så har jag här skapat en visualisering av en arvs hierarki. Det enda som jag har tagit med här är mini lastbil, lastbil med flak och tankbil. När vi då behöver skapa en design för arv så måste vi ställa oss två frågor.

  • Vilka är de generella funktionerna?
    • Vad har de olika fordonen gemensamt?
    • Det som är generellt placerar vi en egen klass(Truck).
  • Vilka är de unika funktionerna som finns?
    • Vad är de inte gemensamt som bara existerar för just en variant av fordon?
    • Det som är unikt placerar vi respektive barn klass.

När vi då gör en sådan analys så kommer vi fram till att vad alla har gemensamt är t ex vikt, registreringsnummer, antal hjul samt hjulstorlek.
Det finns säkert fler men vi håller det så enkelt som möjligt för detta exempel.

Innan vi fortsätter så kommer ni höra väldigt många olika namn eller benämningar på klasser som ärver ifrån en annan klass.

Terminologier

Klassen som vi ärver ifrån kallas oftast för

  • Parent/föräldra klass
  • Base/bas klass
  • Super klass

Klasser som vi ärver till kallas för

  • Child/barn klass
  • Derived/härledd klass
  • Specialised/Specialiserad klass.
  • Sub class

Exempel

Låt oss skapa ett enkelt exempel. Jag har skapat en ny console applikation och i den lägger vi till en ny klass Vehicle.

namespace inheritance;

public class Vehicle
{
  public string RegistrationNumber { get; set; } = "";
  public int Weight { get; set; }
  public int NumberOfWheels { get; set; }
  public int WheelSize { get; set; }

  public void Break()
  {
    Console.WriteLine("Fordonet bromsar!");
  }
  public void Accelerate()
  {
    Console.WriteLine("Fordonet ökar hastigheten");
  }
}
Observera att jag använder auto-implementerade egenskaper för att slippa skapa extra fält i klassen.

Just nu så skriver vi bara lite fejk kod i metoderna, fokus är på är arv.

Om vi nu skapar ytterligare en klass Car, så vill vi att Car ärver allt ifrån Vehicle, för att göra detta så anger vi ett semikolon efter och sedan klassen som vi ärver vi ifrån.

public class Car : Vehicle {}

Låt oss nu göra en mycket enkel implementering av klassen Car.

namespace inheritance;

public class Car : Vehicle
{
  public string VinNumber { get; set; } = "";
  public int NumberOfDoors { get; set; }
}

Innan vi skapar vår Truck klass, låt oss gå till Program.cs filen och i klassen Program och använda vår nya Car klass.

namespace inheritance;
internal class Program
{
  private static void Main(string[] args)
  {
    var myCar = new Car();
  }
}

Inga konstigheter här, vi skapar en ny referens variabel och skapa en ny instans av Car klassen.

Men om vi nu tar och använder vår nya referens och anger en punkt efter myCar.

I vår klass Car hade vi enbart definierat två stycken egenskaper, resten vår vi ifrån arvet ifrån Vehicle klassen.

Dessutom ser vi andra metoder som vi inte har definierat i vår Vehicle klass. Vi ser bland annat:

  • Equals
  • GetHashCode
  • GetType
  • ToString

Dessa kommer ifrån en klass som vi inte har gått igenom och det är klassen Objekt som finns i .NET ramverket och är basklassen för alla typer i .NET. Den här klassen behöver vi inte explicit ange när vi skapar nya klasser. Den läggs till automatiskt av C# kompilatorn.

Låt oss nu bygga ut vår applikation att även kunna hantera lastbilar. I vår applikation lägger vi till ytterligare en klass Truck.

namespace inheritance;

public class Truck : Car
{
  public int Length { get; set; }
  public int Width { get; set; }
  public int Height { get; set; }
  
  public int CalculateVolume(){
    return Length * Width * Height;
  }
}

I klassen Truck ärver vi inte ifrån Vehicle utan istället så ärver vi ifrån klassen Car. En lastbil är en typ av bil fast större😁.
Vi lägger även till tre stycken egenskaper för att kunna räkna ut lastvolymen. Vi har även en fejk metod som räknar lastvolymen.

Om vi nu går tillbaka till vår Program klass och skriver om den till att använda vår nya klass Truck.

namespace inheritance;
internal class Program
{
  private static void Main(string[] args)
  {
    var myTruck = new Truck();
  }
}

Om vi nu gör samma sak som vi gjorde med myCar det vill säga att vi på en ny rad skriver myTruck och en punkt så kommer vi att se en lista som innehåller alla egenskaper som vi ärver ifrån Car men även alla egenskaper och metoder som Car ärver ifrån Vehicle samt de unika egenskaper och metoder som är definierade i Truck.

En sista sak innan vi går vidare.
C# har inte stöd för multipla arv, det vill säga att ärva ifrån flera klasser samtidigt. Det är viktigt att komma ihåg när vi designar våra arvshierarkier.

Det var introduktionen till arv i C# och .NET. Vi kommer tillbaka till arv i nästa modul där vi ska titta på lite mer avancerade möjligheter med arv. Låt oss nu titta på nästa typ av relation mellan klasser.

Composition

Jag kommer att använda mig av det engelska uttrycket composition i denna diskussionen. En svensk översättning är sammansättning vilket jag skulle kunnat använda men jag har valt att använda mig av det objekt orienterade begreppet composition.

Vad är composition?

Composition är även det en typ av relation mellan klasser. Skillnaden mot arv är att composition tillåter en klass att innehålla en annan klass.

Kallas även för "Has-a" relation. I vårt enkla exempel ovan med fordon såsom bilar och lastbilar så skulle vi kunna säga att en bil har en motor. Vi ska strax se ett exempel på detta.

UML representation av Composition

När vi beskriver composition med UML så använder vi en relations linje med en fylld diamant som pekar på klassen som ska innehålla en referens av angiven klass.
I vårt fall så är klassen Car den klass som ska utnyttja en instans av klassen Engine.

Varför ska vi använda det?

Med hjälp av composition får vi följande fördelar:

  • Vi uppnår återanvändning av kod
  • Vi får en flexibilitet som tillåter oss att byta ut en en klass i en relation utan att bryta koden
  • Vi får med andra ord ett sätt att uppnå mjuka kopplingar mellan klasser, "loose-coupling"

Hur gör vi?

Enklast är att se på ett exempel. Vi använder vår Car klass för detta exempel och vi utgår ifrån att en bil "Har en" motor. Så tillbaka till vår kod i applikationen skapar vi nu en ny klass Engine.

public class Engine
{
  public double EngineSize { get; set; }
  public int EnginePower { get; set; }
  public int EngineEffect { get; set; }
  public string FuelType { get; set; } = "";
}
Bry er inte så mycket om ifall ni inte kan något om bilar utan jag använder detta endast för att visualisera composition.

För att nu utnyttja composition i vår Car klass så går vi till klassen och lägger till en constructor metod som tar en instans av klassen Engine som argument.

private readonly Engine engine;

public Car(Engine engine)
{
    engine = engine;
}

Argument lagrar vi i ett privat skrivskyddat fält engine.

Vad som vi gjort här nu är att skapat ett beroende/koppling mellan Engine och Car via dess constructor metod. Så nu tvingas vi att skicka in en instans av klassen Engine för att kunna skapa en instans av vår Car klass.

Så låt oss nu använda detta kunskap och öppna upp vår Program klass och lägg till följande kod.

internal class Program
{
  private static void Main(string[] args)
  {
    var carEngine = new Engine
    {
      EngineEffect = 1100,
      EnginePower = 140,
      EngineSize = 2.0,
      FuelType = "Petrol"
    };

    var myCar = new Car(carEngine);
  }
}

Vad vi gör här är att först och främst skapa en instans av klassen Engine med de egenskaper som vi behöver. I nästa steg skapar vi en instans av vår klass Car och skickar med vår nya instans av Engine till dess constructor.

Aggregation

Denna typ av relation har vi redan sett i föregående modul. Där vi hade en klass Customer som innehåll en lista med Invoice instanser. Detta är relationen aggregation. Vi representerar detta med UML på följande sätt:

Aggregation

Aggregering är symboliserad med en öppen diamant form, samt ett namn för att förtydliga vad det är som är aggregerat i klassen Car. Dessutom så används en multiplyer för att indikera minsta antalet samt maximum antal av instanser som kan finnas. I ovan exempel så säger vi att en bil kanske inte har något tillbehör men kan också ha hur många som helst.

Exempel:
Som sista del i denna modulen ska vi lägga till en klass Equipment i vår applikation och sedan koppla den till vår Car klass. Så skapa en ny klass Equipment.

public class Equipment
{
  public string Name { get; set; } = "";
  public double Price { get; set; }
}

I vår Car klass lägger vi till följande egenskap

public List<Equipment> Equipments { get; set; }

Problemet som vi nu får är att vår constructor metod klagar på att egenskapen Equipments inte är nullable, men enligt vårt UML diagram så kan vi ha det så att en bil inte har några tillbehör. Så vad vi måste göra nu är att definiera att egenskapen Equipments kan var null.

Vi gör det genom att lägga till ett frågetecken(?) efter deklarationen av listan.

public List<Equipment>? Equipments { get; set; }

Låt oss nu prova vår nya aggregering genom att öppna upp vår Program klass och addera följande kod:

var equipmentList = new List<Equipment>{
      new Equipment { Name = "Navigator", Price = 5000 },
      new Equipment { Name = "AC", Price = 15000 }
    };

    var myCar = new Car(carEngine);
    myCar.Equipments = equipmentList;

Låt oss gå ett steg till och skapa en ny instans av Car klassen utan att lägga till några tillbehör.

var mySecondCar = new Car(carEngine);

Nu är det dags att testa vår logik. Det viktiga här är att vi har skapat egenskapen Equipments som nullable, så vi måste kontrollera att Equipments inte är null innan vi försöker få åtkomst till den.

Så nu lägger vi till följande logik för att iterera igenom en eventuell lista av tillbehör.

if (myCar.Equipments is not null)
{
  foreach (var equipment in myCar.Equipments)
  {
    Console.WriteLine("{0} - {1}", equipment.Name, equipment.Price);
  }
}

if (mySecondCar.Equipments is not null)
{
  foreach (var equipment in mySecondCar.Equipments)
  {
    Console.WriteLine("{0} - {1}", equipment.Name, equipment.Price);
  }
}

Vad vi gör är att innan vi försöker få åtkomst till listan så kontrollerar vi listan verkligen existerar, det vill säga att den inte är null.

Vi gör detta med en ny syntax som implementerades i version 7 av C# is not null.

Om vi nu kör applikationen som kommer vi se att vi kommer att få utskrivet tillbehören för vår "myCar" instans men vi ser ingen utskrift för vår "mySecondCar" för där lade vi aldrig till några tillbehör.

Här är den kompletta koden för det vi gått igenom

Klassen Vehicle...

public class Vehicle
{
  public string RegistrationNumber { get; set; } = "";
  public int Weight { get; set; }
  public int NumberOfWheels { get; set; }
  public int WheelSize { get; set; }

  public void Break()
  {
    Console.WriteLine("Fordonet bromsar!");
  }
  public void Accelerate()
  {
    Console.WriteLine("Fordonet ökar hastigheten");
  }
}

Klassen Car...

public class Car : Vehicle
{
  private readonly Engine _engine;
  public string VinNumber { get; set; } = "";
  public int NumberOfDoors { get; set; }
  public List<Equipment>? Equipments { get; set; }

  public Car(Engine engine)
  {
    _engine = engine;
  }
}

Klassen Engine...

public class Engine
{
  public double EngineSize { get; set; }
  public int EnginePower { get; set; }
  public int EngineEffect { get; set; }
  public string FuelType { get; set; } = "";
}

Klassen Equipment...

public class Equipment
{
  public string Name { get; set; } = "";
  public double Price { get; set; }
}

Klassen Program...

internal class Program
{
  private static void Main(string[] args)
  {
    var carEngine = new Engine
    {
      EngineEffect = 1100,
      EnginePower = 140,
      EngineSize = 2.0,
      FuelType = "Petrol"
    };

    var equipmentList = new List<Equipment>{
      new Equipment { Name = "Navigator", Price = 5000 },
      new Equipment { Name = "AC", Price = 15000 }
    };

    var myCar = new Car(carEngine);
    myCar.Equipments = equipmentList;

    var mySecondCar = new Car(carEngine);

    if (myCar.Equipments is not null)
    {
      foreach (var equipment in myCar.Equipments)
      {
        Console.WriteLine("{0} - {1}", equipment.Name, equipment.Price);
      }
    }

    if (mySecondCar.Equipments is not null)
    {
      foreach (var equipment in mySecondCar.Equipments)
      {
        Console.WriteLine("{0} - {1}", equipment.Name, equipment.Price);
      }
    }
  }
}

Slutsats

Så då har vi gått igenom grunderna om kopplingar mellan klasser. Vi har gått igenom arv, composition samt aggregation för att se exempel på hur vi kan koppla samman klasser för att kunna skapa en större applikation.

Vi kommer i nästa modul att gå ytterligare på djupet angående arv i objekt orienterade applikation och få se ytterligare fördelar med att använda arv.