school

YT-Video


# Softwarequalität

SOLID = steht für solide Grundprinzipien, an die man sich beim Schreiben von Code halten sollte

  1. Single Responsibility Prinzip
  2. Open Closed Prinzip
  3. L. Substitutions
  4. Interface Segregation Prinzip
  5. Dependency Inversion Prinzip

# Single Responsibility Prinzip

Jedes Softwareelement implementiert nur einen Aspekt. Siehe SEW_Softwaremetriken

  • erhöht Wartbarkeit
  • vermeiden v. Merge-Conflict

Eine Klasse kümmert sich nur um eine Aufgabe bzw. alles was die Klasse machen soll sollte von der Aufgabe her sehr nahe beinander liegen.

Dieses Beispiel würde das Single Responsibility Prinzip brechen:

public class Student
{
	public int StudentId { get; set; }
	public string Name { get; set; }
	public void SaveToDb() { ... }
	public void SendEmail() { ... }
	public void EnrolToCourse() { ... }
}

# Open Closed Prinzip

Open for Extension but Closed for Modification” Ziel: erweiterbarer Code, ohne dass vorher geschriebenes geändert wird.

Prinzip kann aufgelöst werden durch: Interfaces, Basisklassen.

Im Code-Bsp. in Skriptum>rechts: Es muss nur um eine Methode erweitert werden.

...
// Erweiterung
public class TypeFilter : Predicate<Apple> {...}
...
// Bestehender Code muss nicht verändert werden
// Verletzung v. Open Closed Prinzip
class Shape { }
 
class Circle : Shape
{
	public double Radius {get;set;}
}
 
class Square : Shape
{
	public double SideLength {get;set}
}
 
class AreaCalculator
{
	public double CalculateArea(Shape s)
	{
		if(s is Circle c)
		{
			return c.Radius * c.Radius * Math.PI;
		}
		else if(s is Square sq)
		{
			return sq.SideLength * sq.SideLength;
		}
		else
		{
			throw new Exception("Shape unknown");
		}
	}
}

mit abstrakter Basisklasse:

public abstract class Shape  
{  
    public abstract double CalculateArea();  
}  
  
public class Circle(double radius) : Shape  
{  
    private double Radius { get; set; } = radius;  
    public override double CalculateArea() => Radius * Radius * Math.PI;  
}  
  
public class Square(double sideLength) : Shape  
{  
    private double Sidelength { get; set; } = sideLength;  
    public override double CalculateArea() => Sidelength * Sidelength;  
}  
  
public static class AreaCalculator  
{  
    public static double CalculateArea(Shape shape) => shape.CalculateArea();  
}

mit Interface:

public interface IShape  
{  
    double CalculateArea();  
}  
  
public class Circle(double radius) : IShape  
{  
    private double Radius { get; set; } = radius;  
    public double CalculateArea() => Radius * Radius * Math.PI;  
}  
  
public class Shape(double sideLength) : IShape  
{  
    private double SideLength { get; set; } = sideLength;  
    public double CalculateArea() => SideLength * SideLength;  
}  
  
public static class CalcAreaOfShape  
{  
    public static double CalcArea(IShape shape) => shape.CalculateArea();  
}

# Liskovsche Substitutionsprinzip

Alles was in Basisklasse definiert/da ist, sollte in den abgeleiteten Klassen Sinn machen. Instanzen einer abgeleiteten Klasse müssen sich genauso verhalten wie Objekte der Basisklasse. Verwender muss es egal sein, ob Basisklasse oder abgeleitete Klasse.

Wird oft bei falschen Vererbungshierarchien verletzt.

Bsp. von Verstoß gg. Liskovsches Substitutions-Prinzip:

public class Parent
{
	public void Eat() {...}
	public void Sleep() {...}
	public void GoToWork() {...}
	public void MakeDinner() {...}
}
 
public class Child : Parent
{...}

korrekte Lösung:

public class Human
{
	public void Eat() {...}
	public void Sleep() {...}
}
 
public class Parent : Human
{
	public void GoToWork() {...}
	public void MakeDinner() {...}
}
 
public class Child : Human
{
	public void PlayWithToys() {...}
}

# Interface Segregation Prinzip

CQRS

Interface - so “schlank” wie nötig. Eine bestimmte Teilaufgabe soll abgedeckt werden.

Natürlich das ganze sollte man auch nicht übertreiben.

Eine Klasse könnte mehrere Interfaces implementieren.

Bsp.: Datenzugriffsschicht - CRUD-Methoden (wir geben CRUD-Methoden in Interface)

  • beide Methoden können gleiches Interface implementieren: IDatenzugriff
  • IDatenzugriff unterteilen - IReadDatenzugriff, IWriteDatenzugriff
  • je nachdem was Verwender benötigt, muss daher nicht alles implementiert werden

Anstatt einem Interface:

public interface IWorker 
{ 
	void Work(); 
	void Manage(); 
	void Report(); 
}

Unterteilen in mehrere Interfaces:

public interface IWorker 
{ 
	void Work(); 
} 
 
public interface IManager 
{ 
	void Manage(); 
} 
 
public interface IReporter 
{ 
	void Report(); 
}

# Dependency Inversion Prinzip

”Higher Level components should not depend on lower Level components - both should depend on the interface” Keine direkte Abhängigkeit zw. Data Access & Business Logic

SEW_SOLID 2024-11-19 09.09.20.excalidraw

⚠ Switch to EXCALIDRAW VIEW in the MORE OPTIONS menu of this document. ⚠ You can decompress Drawing data with the command palette: ‘Decompress current Excalidraw file’. For more info check in plugin settings under ‘Saving’

Excalidraw Data

Text Elements

Business Logic (Higher Order)

IDataAccess

Data Access (Lower Order)

Link to original

// Abstraktion
interface IMessageService
{
    void SendMessage(string message);
}
 
// lower order
class EmailService : IMessageService
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"Email sent: {message}");
    }
}
 
// lower order
class SmsService : IMessageService
{
    public void SendMessage(string message)
    {
        Console.WriteLine($"SMS sent: {message}");
    }
}
 
// higher order
class Notification
{
    private readonly IMessageService _messageService;
 
    public Notification(IMessageService messageService) // Abhängig von der Abstraktion
    {
        _messageService = messageService;
    }
 
    public void Notify(string message)
    {
        _messageService.SendMessage(message);
    }
}