In diesem Artikel lernst du die SOLID-Prinzipien kennen. Du erhältst eine Verständnis jedes Prinzips zusammen mit Java-Code-Beispielen.

SOLID-Prinzipien sind eine Reihe von fünf Designprinzipien, die in objektorientiertem Programmieren verwendet werden. Der Anschluss an diese Prinzipien wird dir helfen, robuster Software zu entwickeln. Diese Prinzipien machen deinen Code effizienter, lesbarer und maintainable.

SOLID ist ein Akronym, das für folgendes steht:

  • Single Responsibility Principle (Einzelfunktionalitätsprinzip)
  • Open/Closed Principle (Offen/Geschlossen-Prinzip)
  • Liskov Substitution Principle (Liskov-Substitution-Prinzip)
  • Interface Segregation Principle (Schnittstellensegregationprinzip)
  • Dependency Inversion Principle (Abhängigkeitsinversionprinzip)

Einzelfunktionalitätsprinzip

Das Einzelfunktionalitätsprinzip besagt, dass jede Klasse nur eine einzige, fokussierte Verantwortung haben muss, eine einzige Grund, sich zu ändern.

public class Employee{
  public String getDesignation(int employeeID){ // }
  public void updateSalary(int employeeID){ // }
  public void sendMail(){ // }
}

In dem oben genannten Beispiel hat die Employee-Klasse einige spezifische Verhaltensweisen der Employee-Klasse, wie getDesignation & updateSalary.

Zusätzlich hat sie auch eine andere Methode namens sendMail, die von der Verantwortung der Employee-Klasse abweicht.

Dieses Verhalten ist nicht spezifisch für diese Klasse und verletzt somit das Einzelfunktionalitätsprinzip. Um dies zu überwinden, kannst du die Methode sendMail in eine separate Klasse verschieben.

So kann es aussehen:

public class Employee{
  public String getDesignation(int employeeID){ // }
  public void updateSalary(int employeeID){ // }
}

public class NotificationService {
    public void sendMail() { // }
}

Offen/Geschlossen Prinzip

Nach dem Offen/Geschlossen Prinzip müssen Komponenten für Erweiterungen offen sein, aber geschlossen für Änderungen. Um dieses Prinzip zu verstehen, nehmen wir ein Beispiel einer Klasse, die den Flächeninhalt eines Geometrischen Objekts berechnet.

public class AreaCalculator(){
  public double area(Shape shape){
    double areaOfShape;
    if(shape instanceof Square){
        // Berechne den Flächeninhalt eines Quadrats
    } else if(shape instanceof Circle){
        // Berechne den Flächeninhalt eines Kreises
    }
    return areaOfShape;
  }

Das Problem mit dem oben genannten Beispiel besteht darin, dass falls in Zukunft ein neuer Fall von Typ Shape auftritt, für den Sie den Flächeninhalt berechnen müssen, Sie die oben genannte Klasse ändern müssen, indem Sie einen weiteren else-if-Block hinzufügen. Sie werden dies für jeden neuen Shape-Objekt tun müssen.

Um dies zu vermeiden, können Sie ein Interface erstellen und jeder Shape-Klasse dieses Interface implementieren. Danach kann jeder Klasse ihre eigene Implementierung bereitstellen, um den Flächeninhalt zu berechnen. Dies wird Ihrem Programm zukünftig eine leichte Erweiterbarkeit ermöglichen.

interface IAreaCalculator(){
  double area();
}

class Square implements IAreaCalculator{
  @Override
  public double area(){
    System.out.println("Calculating area for Square");
    return 0.0;
   }
}

class Circle implements IAreaCalculator{
  @Override
  public double area(){
    System.out.println("Calculating area for Circle");
    return 0.0;
   }
}

Liskov-SubstitutionPrinzip

Das Liskov-SubstitutionPrinzip besagt, dass Sie ein übergeordnetes Objekt mit einem UntergeordnetenObjekt ersetzen können, ohne die Korrektheit des Programms zu beeinträchtigen.

abstract class Bird{
   abstract void fly();
}

class Eagle extends Bird {
   @Override
   public void fly() { // einige Implementierung }
}

class Ostrich extends Bird {
   @Override
   public void fly() { // leeres Implementation }
}

In dem obigen Beispiel verlassen die Klassen Eagle und Ostrich die Basisklasse Bird und überschreiben die Methode fly(). Allerdings ist die Klasse Ostrich gezwungen, eine leere Implementierung bereitzustellen, da sie nicht fliegen kann, und verhält sich daher nicht gleich, wenn wir ein Objekt der Klasse Bird durch ein Objekt der Klasse Ostrich ersetzen.

Dies verletzt die Liskov-Substitutionseigenschaft. Um dies zu beheben, können wir eine separate Klasse für fliegende Vögel erstellen und die Klasse Eagle daraus ableiten, während andere Vögel eine andere Klasse erben, die keine fly-Verhalten enthält.

abstract class FlyingBird{
   abstract void fly();
}

abstract class NonFlyingBird{
   abstract void doSomething();
}

class Eagle extends FlyingBird {
   @Override
   public void fly() { // some implementation }
}

class Ostrich extends NonFlyingBird {
   @Override
   public void doSomething() { // some implementation }
}

Interface Segregation Principle

gemäß dem Interface Segregation Principle sollten Sie kleine, konzentrierte Schnittstellen aufbauen, die das Client nicht zwingen, Implementierungen für nicht benötigtes Verhalten durchzuführen.

Ein direkter Beispiel wäre, eine Schnittstelle zu haben, die sowohl den Flächen- als auch den Volumenberechnung für ein Gegenstandsmodell übernimmt.

interface IShapeAreaCalculator(){
  double calculateArea();
  double calculateVolume();
}

class Square implements IShapeAreaCalculator{
  double calculateArea(){ // calculate the area }
  double calculateVolume(){ // dummy implementation }
}

Das Problem bei diesem ist, dass ein Square-Gegenstand, wenn es diese Schnittstelle implementiert, dazu gezwungen ist, die Methode calculateVolume() zu implementieren, die es nicht braucht.

Andererseits kann ein Cube sowohl das eine als auch das andere implementieren. Um dies zu überwinden, können wir die Schnittstelle aufteilen und zwei getrennte Schnittstellen verwenden: eine für die Berechnung des Flächeninhalts und eine weitere für die Berechnung des Volumens. Dies ermöglicht es jedem einzelnen geometrischen Objekt, selbst zu entscheiden, was es implementieren soll.

interface IAreaCalculator {
    double calculateArea();
}

interface IVolumeCalculator {
    double calculateVolume();
}

class Square implements IAreaCalculator {
    @Override
    public double calculateArea() { // Berechne die Fläche }
}

class Cube implements IAreaCalculator, IVolumeCalculator {
    @Override
    public double calculateArea() { // Berechne die Fläche }

    @Override
    public double calculateVolume() {// Berechne das Volumen }
}

Abhängigkeitsinversionprinzip

According to the dependency inversion principle, high-level modules should not depend on low-level modules. In other words, you must follow abstraction and ensure loose coupling

public interface Notification {
    void notify();
}

public class EmailNotification implements Notification {
    public void notify() {
        System.out.println("Sending notification via email");
    }
}

public class Employee {
    private EmailNotification emailNotification; 
    public Employee(EmailNotification emailNotification) {
        this.emailNotification = emailNotification;
    }
    public void notifyUser() {
        emailNotification.notify();
    }
}

In the given example, the Employee class depends directly on the EmailNotification class, which is a low-level module. This violates the dependency inversion principle.

public interface Notification{
  public void notify();
}

public class Employee{
  private Notification notification;
  public Employee(Notification notification){
      this.notification = notification;
  }
  public void notifyUser(){
    notification.notify();
  }
 }

 public class EmailNotification implements Notification{
    public void notify(){
        //implement notification via email 
    }
 }

 public static void main(String [] args){
    Notification notification = new EmailNotification();
    Employee employee = new Employee(notification);
    employee.notifyUser();
 }

In the above example, we have ensured loose coupling. Employee is not dependent on any concrete implementation, rather, it depends only on the abstraction (notification interface).

If we need to change the notification mode, we can create a new implementation and pass it to the Employee.

Conclusion

In conclusion, we’ve covered the essence of SOLID principles through straightforward examples in this article.

These principles form the building blocks for developing applications that are highly extensible and reusable.

Bitte verbinden Sie sich mit mir auf LinkedIn.