Grundlagen des Software-Designs 2024
Wenn du dich fragst, was Software-Design eigentlich ist und warum es so wichtig ist, dann bist du hier genau richtig. Als Anwendungsentwickler möchte ich dir die Grundlagen näherbringen.
Was ist Software-Design?
Software-Design ist der Prozess, bei dem du planst, wie deine Software aufgebaut sein soll. Es geht nicht nur darum, was die Software tun soll, sondern vor allem, wie sie es tun soll. Ein gutes Design ist der Schlüssel zu einer effizienten, wartbaren und skalierbaren Software.
Die drei Ebenen des Software-Designs:
- Architektur-Design: Grobe Struktur der gesamten Anwendung
- Detail-Design: Konkrete Implementierung von Komponenten
- Interface-Design: Definition von APIs und Benutzeroberflächen
Die Bedeutung von gutem Design
Gutes Design ist entscheidend für die Qualität deiner Software. Es sorgt dafür, dass deine Software nicht nur jetzt funktioniert, sondern auch in Zukunft erweiterbar und wartbar bleibt.
Vorteile guten Designs:
- Wartbarkeit: Einfacher zu verstehen und zu ändern
- Erweiterbarkeit: Neue Features können leicht hinzugefügt werden
- Testbarkeit: Komponenten können isoliert getestet werden
- Wiederverwendbarkeit: Module können in anderen Projekten genutzt werden
- Performance: Effiziente Nutzung von Ressourcen
SOLID-Prinzipien
Die SOLID-Prinzipien sind fünf grundlegende Designprinzipien für objektorientierte Softwareentwicklung:
1. Single Responsibility Principle (SRP)
Eine Klasse sollte nur eine Verantwortlichkeit haben.
// ❌ Schlechtes Beispiel: Klasse hat mehrere Verantwortlichkeiten
public class UserService {
public void saveUser(User user) { /* speichert User */ }
public void sendEmail(User user) { /* sendet Email */ }
public void generateReport(User user) { /* erstellt Report */ }
}
// ✅ Gutes Beispiel: Jede Klasse hat eine Verantwortlichkeit
public class UserService {
public void saveUser(User user) { /* speichert User */ }
}
public class EmailService {
public void sendEmail(User user) { /* sendet Email */ }
}
public class ReportService {
public void generateReport(User user) { /* erstellt Report */ }
}
2. Open/Closed Principle (OCP)
Software-Komponenten sollten offen für Erweiterungen, aber geschlossen für Modifikationen sein.
// ❌ Schlechtes Beispiel: Muss für jeden neuen Rabatttyp angepasst werden
public class DiscountCalculator {
public double calculateDiscount(String type, double amount) {
if (type.equals("STUDENT")) return amount * 0.1;
if (type.equals("SENIOR")) return amount * 0.2;
// neuer Typ erfordert Code-Änderung
return 0;
}
}
// ✅ Gutes Beispiel: Offen für Erweiterungen
public interface Discount {
double calculate(double amount);
}
public class StudentDiscount implements Discount {
public double calculate(double amount) { return amount * 0.1; }
}
public class SeniorDiscount implements Discount {
public double calculate(double amount) { return amount * 0.2; }
}
3. Liskov Substitution Principle (LSP)
Unterklassen sollten durch ihre Basisklassen ersetzbar sein.
// ❌ Schlechtes Beispiel: Verletzt LSP
public class Rectangle {
protected int width, height;
public void setWidth(int width) { this.width = width; }
public void setHeight(int height) { this.height = height; }
public int getArea() { return width * height; }
}
public class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // Verletzt Rectangle-Verhalten
}
}
// ✅ Gutes Beispiel: Respektiert LSP
public abstract class Shape {
public abstract int getArea();
}
public class Rectangle extends Shape {
private int width, height;
public int getArea() { return width * height; }
}
public class Square extends Shape {
private int side;
public int getArea() { return side * side; }
}
4. Interface Segregation Principle (ISP)
Kleine, spezifische Interfaces sind besser als große, allgemeine.
// ❌ Schlechtes Beispiel: Großes Interface
public interface Worker {
void work();
void eat();
void sleep();
}
public class Robot implements Worker {
public void work() { /* ... */ }
public void eat() { /* Robot isst nicht! */ }
public void sleep() { /* Robot schläft nicht! */ }
}
// ✅ Gutes Beispiel: Spezielle Interfaces
public interface Workable {
void work();
}
public interface Eatable {
void eat();
}
public class Robot implements Workable {
public void work() { /* ... */ }
}
5. Dependency Inversion Principle (DIP)
Hochrangige Module sollten nicht von niedrigrangigen Modulen abhängen. Beide sollten von Abstraktionen abhängen.
// ❌ Schlechtes Beispiel: Direkte Abhängigkeit
public class LightSwitch {
private LightBulb bulb;
public LightSwitch() {
this.bulb = new LightBulb(); // Direkte Abhängigkeit
}
public void flip() {
bulb.turnOn();
}
}
// ✅ Gutes Beispiel: Abhängigkeit von Interface
public interface Switchable {
void turnOn();
void turnOff();
}
public class LightBulb implements Switchable {
public void turnOn() { /* ... */ }
public void turnOff() { /* ... */ }
}
public class LightSwitch {
private Switchable device;
public LightSwitch(Switchable device) {
this.device = device; // Abhängigkeit von Abstraktion
}
public void flip() {
device.turnOn();
}
}
Weitere wichtige Design-Prinzipien
KISS (Keep It Simple, Stupid)
Halte deinen Code einfach und verständlich. Einfache Lösungen sind oft die besten.
DRY (Don’t Repeat Yourself)
Vermeide Code-Duplizierung. Wiederhole dich nicht im Code.
YAGNI (You Ain’t Gonna Need It)
Entwickle nicht für hypothetische, zukünftige Szenarien. Konzentriere dich auf das, was jetzt benötigt wird.
Separation of Concerns
Trenne verschiedene Aspekte deiner Anwendung in unterschiedliche Module.
Architektur-Muster
Layered Architecture
Schichtenarchitektur organisiert Code in logische Schichten:
- Presentation Layer: Benutzeroberfläche
- Business Layer: Geschäftslogik
- Data Access Layer: Datenzugriff
- Database Layer: Datenbank
MVC (Model-View-Controller)
Trennt Daten, Präsentation und Steuerung:
- Model: Daten und Geschäftslogik
- View: Benutzeroberfläche
- Controller: Vermittlung zwischen Model und View
Microservices Architecture
Aufteilung in kleine, unabhängige Services:
- Jeder Service hat eine eigene Datenbank
- Kommunikation über APIs
- Unabhängige Deployment
Event-Driven Architecture
Systeme kommunizieren über Events:
- Lose Kopplung zwischen Komponenten
- Asynchrone Kommunikation
- Gute Skalierbarkeit
Design-Patterns (GoF Patterns)
Creational Patterns (Erzeugungsmuster)
Factory Method
Erstellt Objekte ohne deren genaue Klasse zu kennen.
public interface Vehicle {
void drive();
}
public class Car implements Vehicle {
public void drive() { System.out.println("Car drives"); }
}
public class Motorcycle implements Vehicle {
public void drive() { System.out.println("Motorcycle drives"); }
}
public abstract class VehicleFactory {
public abstract Vehicle createVehicle();
}
public class CarFactory extends VehicleFactory {
public Vehicle createVehicle() { return new Car(); }
}
Singleton
Stellt sicher, dass eine Klasse nur einmal instanziiert wird.
public class DatabaseConnection {
private static DatabaseConnection instance;
private DatabaseConnection() { /* private constructor */ }
public static DatabaseConnection getInstance() {
if (instance == null) {
instance = new DatabaseConnection();
}
return instance;
}
}
Structural Patterns (Strukturmuster)
Adapter
Ermöglicht die Zusammenarbeit von inkompatiblen Interfaces.
public interface MediaPlayer {
void play(String audioType, String fileName);
}
public interface AdvancedMediaPlayer {
void playVlc(String fileName);
void playMp4(String fileName);
}
public class MediaAdapter implements MediaPlayer {
private AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer = new VlcPlayer();
}
}
public void play(String audioType, String fileName) {
if (audioType.equalsIgnoreCase("vlc")) {
advancedMusicPlayer.playVlc(fileName);
}
}
}
Behavioral Patterns (Verhaltensmuster)
Observer
Ermöglicht Benachrichtigungen bei Zustandsänderungen.
import java.util.ArrayList;
import java.util.List;
public interface Observer {
void update(String message);
}
public interface Subject {
void registerObserver(Observer observer);
void removeObserver(Observer observer);
void notifyObservers();
}
public class NewsAgency implements Subject {
private List<Observer> observers = new ArrayList<>();
private String news;
public void registerObserver(Observer observer) {
observers.add(observer);
}
public void notifyObservers() {
for (Observer observer : observers) {
observer.update(news);
}
}
public void setNews(String news) {
this.news = news;
notifyObservers();
}
}
Strategy
Definiert eine Familie von Algorithmen und macht sie austauschbar.
public interface PaymentStrategy {
void pay(int amount);
}
public class CreditCardPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using Credit Card");
}
}
public class PayPalPayment implements PaymentStrategy {
public void pay(int amount) {
System.out.println("Paid " + amount + " using PayPal");
}
}
public class ShoppingCart {
private PaymentStrategy paymentStrategy;
public void setPaymentStrategy(PaymentStrategy strategy) {
this.paymentStrategy = strategy;
}
public void checkout(int amount) {
paymentStrategy.pay(amount);
}
}
Refactoring-Techniken
Extract Method
Ziehe Code in eine separate Methode aus, um die Lesbarkeit zu verbessern.
// Vorher
public void processOrder(Order order) {
// Validierung
if (order == null) throw new IllegalArgumentException();
if (order.getItems().isEmpty()) throw new IllegalArgumentException();
// Berechnung
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
// Speicherung
order.setTotal(total);
orderRepository.save(order);
}
// Nachher
public void processOrder(Order order) {
validateOrder(order);
double total = calculateTotal(order);
saveOrder(order, total);
}
private void validateOrder(Order order) {
if (order == null) throw new IllegalArgumentException();
if (order.getItems().isEmpty()) throw new IllegalArgumentException();
}
private double calculateTotal(Order order) {
double total = 0;
for (Item item : order.getItems()) {
total += item.getPrice() * item.getQuantity();
}
return total;
}
private void saveOrder(Order order, double total) {
order.setTotal(total);
orderRepository.save(order);
}
Replace Conditional with Polymorphism
Ersetze Bedingungen durch Polymorphismus.
// Vorher
public class Bird {
public void fly(String type) {
if (type.equals("Eagle")) {
System.out.println("Eagle flies high");
} else if (type.equals("Penguin")) {
System.out.println("Penguin cannot fly");
}
}
}
// Nachher
public abstract class Bird {
public abstract void fly();
}
public class Eagle extends Bird {
public void fly() {
System.out.println("Eagle flies high");
}
}
public class Penguin extends Bird {
public void fly() {
System.out.println("Penguin cannot fly");
}
}
Code Quality Metrics
Cyclomatic Complexity
Misst die Komplexität eines Codes durch die Anzahl der Entscheidungspunkte.
Maintainability Index
Bewertet wie leicht Code zu warten ist.
Test Coverage
Prozent des Codes, der durch Tests abgedeckt ist.
Wichtigkeit der Dokumentation
Gute Dokumentation ist entscheidend für die Wartbarkeit:
Types of Documentation
- API Documentation: Beschreibung von Interfaces
- Architecture Documentation: Übersicht der Systemarchitektur
- Code Comments: Erklärungen komplexer Code-Teile
- User Documentation: Bedienungsanleitungen
Documentation Best Practices
- Dokumentiere das “Warum”, nicht nur das “Was”
- Halte Dokumentation aktuell
- Verwende klare und verständliche Sprache
- Nutze Diagramme zur Visualisierung
Praxisbeispiel: E-Commerce System
Hier ist ein praktisches Beispiel, das viele der besprochenen Prinzipien anwendet:
// Interfaces für lose Kopplung
public interface OrderService {
Order createOrder(List<Item> items);
void processPayment(Order order, PaymentStrategy strategy);
}
public interface InventoryService {
boolean checkAvailability(Item item);
void reserveItem(Item item);
}
// Implementierung mit SOLID-Prinzipien
@Service
public class OrderServiceImpl implements OrderService {
private final InventoryService inventoryService;
private final NotificationService notificationService;
public OrderServiceImpl(InventoryService inventoryService,
NotificationService notificationService) {
this.inventoryService = inventoryService;
this.notificationService = notificationService;
}
@Override
public Order createOrder(List<Item> items) {
validateItems(items);
reserveItems(items);
Order order = new Order(items);
order.calculateTotal();
return order;
}
@Override
public void processPayment(Order order, PaymentStrategy strategy) {
strategy.pay(order.getTotal());
notificationService.sendOrderConfirmation(order);
}
private void validateItems(List<Item> items) {
for (Item item : items) {
if (!inventoryService.checkAvailability(item)) {
throw new ItemNotAvailableException(item);
}
}
}
private void reserveItems(List<Item> items) {
for (Item item : items) {
inventoryService.reserveItem(item);
}
}
}
Prüfungsrelevante Fragen und Antworten
1. Was sind die SOLID-Prinzipien und warum sind sie wichtig?
Antwort: Die SOLID-Prinzipien sind fünf Designprinzipien: Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion. Sie sind wichtig weil sie zu wartbarerem, erweiterbarem und testbarem Code führen. Sie helfen technische Schulden zu vermeiden und die Code-Qualität zu verbessern.
2. Erklären Sie das Single Responsibility Principle mit einem Beispiel.
Antwort: Das SRP besagt, dass eine Klasse nur eine Verantwortlichkeit haben sollte. Beispiel: Eine UserService-Klasse sollte nur für Benutzer-Verwaltung zuständig sein, nicht auch für E-Mail-Versand oder Berichterstellung. Dies verbessert die Wartbarkeit da Änderungen an einer Verantwortlichkeit die andere nicht beeinflussen.
3. Was ist der Unterschied zwischen Kohäsion und Kopplung?
Antwort: Kohäsion beschreibt wie stark die Elemente innerhalb eines Moduls zusammengehören (hohe Kohäsion ist gut). Kopplung beschreibt wie stark Module voneinander abhängen (niedrige Kopplung ist gut). Ziel ist hohe Kohäsion und niedrige Kopplung für bessere Wartbarkeit.
4. Wann verwendet man das Singleton Pattern?
Antwort: Singleton wird verwendet wenn genau eine Instanz einer Klasse benötigt wird, z.B. für Datenbank-Connections, Logging-Services oder Konfigurationsmanager. Es stellt sicher dass global nur eine Instanz existiert und einen zentralen Zugriffspunkt bietet.
5. Erklären Sie das Open/Closed Principle.
Antwort: Das OCP besagt dass Software-Komponenten offen für Erweiterungen aber geschlossen für Modifikationen sein sollten. Das bedeutet man sollte neue Funktionalität durch Erweiterung (z.B. Vererbung) hinzufügen können, ohne bestehenden Code ändern zu müssen.
Buchempfehlungen zum Thema Software-Design
Dies sind externe Affiliate-Links. Wir haben durch Deinen Kauf Vorteile:
