Log4j-Orthogonalität anhand eines Beispiels

Orthogonalität ist ein Konzept, das häufig zur Beschreibung modularer und wartbarer Software verwendet wird, das jedoch anhand einer Fallstudie leichter zu verstehen ist. In diesem Artikel entmystifiziert Jens Dietrich die Orthogonalität und einige verwandte Entwurfsprinzipien, indem er deren Verwendung in der beliebten Log4j-Dienstprogrammbibliothek demonstriert. Er erörtert auch, wie Log4j in einigen Fällen die Orthogonalität verletzt, und erörtert mögliche Problemumgehungen für die aufgeworfenen Probleme.

Das Konzept der Orthogonalität basiert auf dem griechischen Wort orthogōnios und bedeutet "rechtwinklig". Es wird oft verwendet, um die Unabhängigkeit zwischen verschiedenen Dimensionen auszudrücken. Wenn sich ein Objekt in einem dreidimensionalen Raum entlang der x- Achse bewegt, ändern sich seine y- und z- Koordinaten nicht. Änderungen in einer Dimension bewirken keine Änderungen in einer anderen Dimension. Dies bedeutet, dass eine Dimension keine Nebenwirkungen für andere verursachen kann.

Dies erklärt, warum das Konzept der Orthogonalität häufig zur Beschreibung des modularen und wartbaren Softwaredesigns verwendet wird: Das Denken an Systeme als Punkte in einem mehrdimensionalen Raum (hervorgerufen durch unabhängige orthogonale Dimensionen) hilft Softwareentwicklern sicherzustellen, dass wir Änderungen an einem Aspekt des Systems vornehmen wird keine Nebenwirkungen für einen anderen haben.

Es kommt vor, dass Log4j, ein beliebtes Open Source-Protokollierungspaket für Java, ein gutes Beispiel für einen modularen Aufbau ist, der auf Orthogonalität basiert.

Die Abmessungen von Log4j

Die Protokollierung ist nur eine schickere Version der System.out.println()Anweisung, und Log4j ist ein Dienstprogrammpaket, das die Mechanismen der Protokollierung auf der Java-Plattform abstrahiert. Mit den Log4j-Funktionen können Entwickler unter anderem Folgendes tun:

  • Melden Sie sich bei verschiedenen Appendern an (nicht nur bei der Konsole, sondern auch bei Dateien, Netzwerkspeicherorten, relationalen Datenbanken, Dienstprogrammen für Betriebssystemprotokolle usw.).
  • Protokollieren Sie auf mehreren Ebenen (z. B. FEHLER, WARNUNG, INFO und DEBUG).
  • Steuern Sie zentral, wie viele Informationen auf einer bestimmten Protokollierungsstufe protokolliert werden
  • Verwenden Sie verschiedene Layouts, um zu definieren, wie ein Protokollierungsereignis in eine Zeichenfolge gerendert wird

Während Log4j andere Funktionen hat, werde ich mich auf diese drei Dimensionen seiner Funktionalität konzentrieren, um das Konzept und die Vorteile der Orthogonalität zu untersuchen. Beachten Sie, dass meine Diskussion auf Log4j Version 1.2.17 basiert.

Log4j auf JavaWorld

Verschaffen Sie sich einen Überblick über Log4j und erfahren Sie, wie Sie Ihre eigenen benutzerdefinierten Log4j-Appender schreiben . Möchten Sie weitere Java-Tutorials? Holen Sie sich den Enterprise Java-Newsletter in Ihren Posteingang.

Berücksichtigung von Log4j-Typen als Aspekte

Anhänge, Ebene und Layout sind drei Aspekte von Log4j, die als unabhängige Dimensionen betrachtet werden können. Ich verwende den Begriff Aspekt hier als Synonym für Besorgnis , was ein Stück Interesse oder Fokus in einem Programm bedeutet. In diesem Fall ist es einfach, diese drei Probleme anhand der jeweiligen Fragen zu definieren:

  • Appender : Wohin sollen die Protokollereignisdaten zur Anzeige oder Speicherung gesendet werden?
  • Layout : Wie soll ein Protokollereignis dargestellt werden?
  • Stufe : Welche Protokollereignisse sollen verarbeitet werden?

Versuchen Sie nun, diese Aspekte gemeinsam im dreidimensionalen Raum zu betrachten. Jeder Punkt in diesem Bereich stellt eine gültige Systemkonfiguration dar, wie in Abbildung 1 dargestellt. (Beachten Sie, dass ich eine leicht vereinfachte Ansicht von Log4j anbiete: Jeder Punkt in Abbildung 1 ist eigentlich keine globale systemweite Konfiguration, sondern eine Konfiguration für eine bestimmter Logger. Die Logger selbst können als vierte Dimension betrachtet werden.)

Listing 1 ist ein typisches Code-Snippet, das Log4j implementiert:

Listing 1. Ein Beispiel für die Implementierung von Log4j

// setup logging ! Logger logger = Logger.getLogger("Foo"); Appender appender = new ConsoleAppender(); Layout layout = new org.apache.log4j.TTCCLayout() appender.setLayout(layout); logger.addAppender(appender); logger.setLevel(Level.INFO); // start logging ! logger.warn("Hello World");

Ich möchte, dass Sie an diesem Code bemerken, dass er orthogonal ist: Sie können den Appender, das Layout oder den Ebenenaspekt ändern, ohne den Code zu beschädigen, der vollständig funktionsfähig bleibt. Bei einem orthogonalen Entwurf ist jeder Punkt im angegebenen Bereich des Programms eine gültige Systemkonfiguration. Es ist keine Einschränkung zulässig, um einzuschränken, welche Punkte im Bereich möglicher Konfigurationen gültig sind oder nicht.

Orthogonalität ist ein leistungsfähiges Konzept, da es uns ermöglicht, ein relativ einfaches mentales Modell für komplexe Anwendungsfälle zu erstellen. Insbesondere können wir uns auf eine Dimension konzentrieren und andere Aspekte ignorieren.

Testen ist ein weit verbreitetes und bekanntes Szenario, in dem Orthogonalität nützlich ist. Wir können die Funktionalität von Protokollebenen mit einem geeigneten festen Paar aus Appender und Layout testen. Die Orthogonalität stellt sicher, dass es keine Überraschungen gibt: Die Protokollebenen funktionieren bei jeder Kombination aus Appender und Layout auf die gleiche Weise. Dies ist nicht nur praktisch (es ist weniger Arbeit zu erledigen), sondern auch notwendig, da es unmöglich wäre, die Protokollebenen mit jeder bekannten Kombination aus Appender und Layout zu testen. Dies gilt insbesondere, da Log4j wie viele andere Softwaretools und Dienstprogramme so konzipiert ist, dass es von Dritten erweitert wird.

The reduction in complexity that orthogonality brings to software programs is similar to how dimensions are used in geometry, where the complicated movement of points in an n-dimensional space is broken down to the relatively simple manipulation of vectors. The entire field of linear algebra is based on this powerful idea.

Designing and coding for orthogonality

If you are now wondering how to design and code orthogonality into your programs, then you are in the right place. The key idea is to use abstraction. Each dimension of an orthogonal system addresses one particular aspect of the program. Such a dimension will usually be represented by a type (class, interface, or enumeration). The most common solution is to use an abstract type (interface or abstract class). Each of these types represents a dimension, while the type instance represents the points within the given dimension. Because abstract types can not be directly instantiated, concrete classes are also needed.

In some cases we can do without them. For instance, we don't need concrete classes when the type is just a markup, and doesn't encapsulate behavior. Then we can just instantiate the type representing the dimension itself, and often predefine a fixed set of instances, either by using static variables, or by using an explicit enumeration type. In Listing 1 this rule would apply to the "level" dimension.

Figure 3. Inside the Level dimension

The general rule of orthogonality is to avoid references to specific concrete types representing other aspects (dimensions) of the program. This enables you to write generic code that will work the same way for all possible instances. Such code can still reference properties of instances, as long as they are part of the interface of the type defining the dimension.

For instance, in Log4j the abstract type Layout defines the method ignoresThrowable(). This method returns a boolean indicating whether the layout can render exception stack traces or not. When an appender uses a layout, it would be perfectly fine to write conditional code on ignoresThrowable(). For instance, a file appender could print exception stack traces on System.err when using a layout that could not handle exceptions.

In a similar manner, a Layout implementation could refer to a particular Level when rendering logging events. For instance, if the log level was Level.ERROR, an HTML-based layout implementation could wrap the log message in tags rendering it in red. Again, the point is that Level.ERROR is defined by Level, the type representing the dimension.

You should, however, avoid references to specific implementation classes for other dimensions. If an appender uses a layout then there is no need to know what kind of layout it is. Figure 4 illustrates good and bad references.

Several patterns and frameworks make it easier to avoid dependencies to implementation types, including dependency injection and the service locator pattern.

Violating orthogonality

Overall, Log4j is a good example of the use of orthogonality. However, some code in Log4j violates this principle.

Log4j contains an appender called JDBCAppender, which is used to log to a relational database. Given the scalability and popularity of relational database, and the fact that this makes log events easily searchable (with SQL queries), JDBCAppender is an important use case.

JDBCAppender is intended to address the problem of logging to a relational database by turning log events into SQL INSERT statements. It solves this problem by using a PatternLayout.

PatternLayout uses templating to give the user maximum flexibility to configure the strings generated from log events. The template is defined as a string, and the variables used in the template are instantiated from log events at runtime, as shown in Listing 2.

Listing 2. PatternLayout

String pattern = "%p [@ %d{dd MMM yyyy HH:mm:ss} in %t] %m%n"; Layout layout = new org.apache.log4j.PatternLayout(pattern); appender.setLayout(layout);

JDBCAppender uses a PatternLayout with a pattern that defines the SQL INSERT statement. In particular, the following code can be used to set the SQL statement used:

Listing 3. SQL insert statement

public void setSql(String s) { sqlStatement = s; if (getLayout() == null) { this.setLayout(new PatternLayout(s)); } else { ((PatternLayout)getLayout()).setConversionPattern(s); } }

Built into this code is the implicit assumption that the layout, if set before using the setLayout(Layout) method defined in Appender, is in fact an instance of PatternLayout. In terms of orthogonality, this means that suddenly a lot of points in the 3D cube that use JDBCAppender with layouts other than PatternLayout do not represent valid system configurations anymore! That is, any attempts to set the SQL string with a different layout would result in a runtime (class cast) exception.

Figure 5. JDBCAppender violating orthogonality

There is another reason that JDBCAppender's design is questionable. JDBC has its own template engine prepared statements. By using PatternLayout, however, the template engine is bypassed. This is unfortunate because JDBC precompiles prepared statements, leading to significant performance improvements. Unfortunately, there is no easy fix for this. The obvious approach would be to control what kind of layout can be used in JDBCAppender by overriding the setter as follows.

Listing 4. Overriding setLayout()

public void setLayout(Layout layout) { if (layout instanceOf PatternLayout) { super.setLayout(layout); } else { throw new IllegalArgumentException("Layout is not valid"); } }

Unfortunately, this approach also has problems. The method in Listing 4 throws a runtime exception, and applications calling this method may not be prepared to catch it. In other words, the setLayout(Layout layout) method cannot guarantee that no runtime exception will be thrown; it therefore weakens the guarantees (postconditions) made by the method it overrides. If we look at it in terms of preconditions, setLayout requires that the layout is an instance of PatternLayout, and has therefore stronger preconditions than the method it overrides. Either way, we've violated a core object-oriented design principle, which is the Liskov substitution principle used to safeguard inheritance.

Workarounds

The fact that there is no easy solution to fix the design of JDBCAppender indicates that there is a deeper problem at work. In this case, the level of abstraction chosen when designing the core abstract types (in particular Layout) needs fine-tuning. The core method defined by Layout is format(LoggingEvent event). This method returns a string. However, when logging to a relational database a tuple of values (a row), and not a string needs to be generated.

One possible solution would be to use a more sophisticated data structure as a return type for format. However, this would imply additional overhead in situations where you might actually want to generate a string. Additional intermediate objects would have to be created and then garbage-collected, compromising the performance of the logging framework. Using a more sophisticated return type would also make Log4j more difficult to understand. Simplicity is a very desirable design goal.

Another possible solution would be to use "layered abstraction" by using two abstract types, Appender and CustomizableAppender which extends Appender. Only CustomizableAppender would then define the method setLayout(Layout layout). JDBCAppender would only implement Appender, while other appender implementations such as ConsoleAppender would implement CustomizableAppender. The drawback of this approach is the increased complexity (e.g., how Log4j configuration files are processed), and the fact that developers must make an informed decision about which level of abstraction to use early.

In conclusion

In diesem Artikel habe ich Log4j als Beispiel verwendet, um sowohl das Entwurfsprinzip der Orthogonalität als auch den gelegentlichen Kompromiss zwischen dem Befolgen eines Entwurfsprinzips und dem Erreichen eines Systemqualitätsattributs wie Skalierbarkeit zu demonstrieren. Selbst in Fällen, in denen es unmöglich ist, eine vollständige Orthogonalität zu erreichen, glaube ich, dass der Kompromiss eine bewusste Entscheidung sein sollte und dass er gut dokumentiert sein sollte (zum Beispiel als technische Verschuldung). Weitere Informationen zu den in diesem Artikel beschriebenen Konzepten und Technologien finden Sie im Abschnitt Ressourcen.