Log4j ortogonalitas dengan contoh

Ortogonalitas adalah konsep yang sering digunakan untuk mendeskripsikan perangkat lunak modular dan dapat dipelihara, tetapi lebih mudah dipahami melalui studi kasus. Dalam artikel ini, Jens Dietrich mendemistifikasi ortogonalitas dan beberapa prinsip desain terkait dengan mendemonstrasikan penggunaannya di pustaka utilitas Log4j yang populer. Dia juga membahas bagaimana Log4j melanggar ortogonalitas dalam beberapa contoh dan membahas solusi yang mungkin untuk masalah yang diangkat.

Konsep ortogonalitas didasarkan pada kata Yunani orthogōnios , yang berarti "siku-siku". Ini sering digunakan untuk mengekspresikan kemandirian antara dimensi yang berbeda. Ketika sebuah benda bergerak sepanjang sumbu x dalam ruang tiga dimensi, koordinat y dan z -nya tidak berubah. Perubahan dalam satu dimensi tidak menyebabkan perubahan pada dimensi lain, yang berarti bahwa satu dimensi tidak dapat menimbulkan efek samping bagi dimensi lainnya.

Ini menjelaskan mengapa konsep ortogonalitas sering digunakan untuk mendeskripsikan desain perangkat lunak modular dan dapat dipelihara: memikirkan sistem sebagai titik dalam ruang multi-dimensi (yang dihasilkan oleh dimensi ortogonal independen) membantu pengembang perangkat lunak untuk memastikan bahwa perubahan kami pada satu aspek sistem tidak akan memiliki efek samping untuk orang lain.

Kebetulan Log4j, paket logging open source yang populer untuk Java, adalah contoh yang baik dari desain modular berdasarkan ortogonalitas.

Dimensi Log4j

Logging hanyalah versi System.out.println()pernyataan yang lebih bagus, dan Log4j adalah paket utilitas yang mengabstraksi mekanisme logging di platform Java. Antara lain, fitur Log4j memungkinkan pengembang melakukan hal berikut:

  • Masuk ke appenders yang berbeda (tidak hanya konsol tetapi juga ke file, lokasi jaringan, database relasional, utilitas log sistem operasi, dan banyak lagi)
  • Log di beberapa level (seperti ERROR, WARN, INFO, dan DEBUG)
  • Kontrol secara terpusat berapa banyak informasi yang dicatat pada tingkat pencatatan tertentu
  • Gunakan tata letak yang berbeda untuk menentukan bagaimana peristiwa logging dirender menjadi string

Meskipun Log4j memiliki fitur lain, saya akan fokus pada tiga dimensi fungsinya untuk mengeksplorasi konsep dan manfaat ortogonalitas. Perhatikan bahwa diskusi saya didasarkan pada Log4j versi 1.2.17.

Log4j di JavaWorld

Dapatkan gambaran umum tentang Log4j dan pelajari cara menulis appender Log4j kustom Anda sendiri . Ingin lebih banyak tutorial Java? Dapatkan buletin Enterprise Java dikirim ke kotak masuk Anda.

Mempertimbangkan tipe Log4j sebagai aspek

Penambah, level, dan tata letak adalah tiga aspek Log4j yang dapat dilihat sebagai dimensi independen. Saya menggunakan istilah aspek di sini sebagai sinonim untuk perhatian , yang berarti bagian yang menarik atau fokus dalam sebuah program. Dalam kasus ini, mudah untuk mendefinisikan ketiga masalah ini berdasarkan pertanyaan yang masing-masing membahas:

  • Penambah : Ke mana data peristiwa log harus dikirim untuk ditampilkan atau disimpan?
  • Tata letak : Bagaimana seharusnya acara log disajikan?
  • Level : Peristiwa log mana yang harus diproses?

Sekarang coba pertimbangkan aspek-aspek ini bersama-sama dalam ruang tiga dimensi. Setiap titik di dalam ruang ini mewakili konfigurasi sistem yang valid, seperti yang ditunjukkan pada Gambar 1. (Perhatikan bahwa saya menawarkan tampilan Log4j yang sedikit disederhanakan: Setiap titik pada Gambar 1 sebenarnya bukan konfigurasi seluruh sistem global, tetapi konfigurasi untuk satu penebang tertentu. Para penebang itu sendiri dapat dianggap sebagai dimensi keempat.)

Cantuman 1 adalah cuplikan kode khas yang menerapkan Log4j:

Kode 1. Contoh implementasi 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");

Yang saya ingin Anda perhatikan tentang kode ini adalah bahwa kode ini ortogonal: Anda dapat mengubah aspek appender, layout, atau level tanpa merusak kode, yang akan tetap berfungsi sepenuhnya. Dalam desain ortogonal, setiap titik dalam ruang tertentu dari program adalah konfigurasi sistem yang valid. Tidak ada batasan yang diperbolehkan untuk membatasi titik mana dalam ruang konfigurasi yang mungkin valid atau tidak.

Orthogonality adalah konsep yang ampuh karena memungkinkan kita untuk membangun model mental yang relatif sederhana untuk kasus penggunaan aplikasi yang kompleks. Secara khusus, kita dapat fokus pada satu dimensi sambil mengabaikan aspek lainnya.

Pengujian adalah skenario umum dan familiar di mana ortogonalitas berguna. Kita bisa menguji fungsionalitas level log menggunakan pasangan tetap yang sesuai dari appender dan layout. Orthogonality memastikan kita bahwa tidak akan ada kejutan: level log akan bekerja dengan cara yang sama dengan kombinasi appender dan layout yang diberikan. Ini tidak hanya nyaman (ada lebih sedikit pekerjaan yang harus dilakukan) tetapi juga diperlukan, karena tidak mungkin menguji level log dengan setiap kombinasi appender dan layout yang diketahui. Ini terutama benar mengingat Log4j, seperti banyak alat dan utilitas perangkat lunak, dirancang untuk dikembangkan oleh pihak ketiga.

Pengurangan kompleksitas yang dibawa oleh ortogonalitas ke program perangkat lunak mirip dengan bagaimana dimensi digunakan dalam geometri, di mana pergerakan titik yang rumit dalam ruang dimensi-n dipecah menjadi manipulasi vektor yang relatif sederhana. Seluruh bidang aljabar linier didasarkan pada gagasan hebat ini.

Merancang dan membuat kode untuk ortogonalitas

Jika Anda sekarang bertanya-tanya bagaimana merancang dan mengkodekan ortogonalitas ke dalam program Anda, maka Anda berada di tempat yang tepat. Ide utamanya adalah menggunakan abstraksi . Setiap dimensi dari sistem ortogonal membahas satu aspek tertentu dari program. Dimensi seperti itu biasanya diwakili oleh sebuah tipe (kelas, antarmuka, atau enumerasi). Solusi paling umum adalah menggunakan tipe abstrak (antarmuka atau kelas abstrak). Masing-masing tipe ini merepresentasikan sebuah dimensi, sedangkan tipe instance merepresentasikan titik-titik dalam dimensi yang diberikan. Karena tipe abstrak tidak dapat langsung dipakai, kelas beton juga diperlukan.

Dalam beberapa kasus kita dapat melakukannya tanpa mereka. Misalnya, kita tidak memerlukan kelas konkret jika jenisnya hanya markup, dan tidak merangkum perilaku. Kemudian kita dapat memberi contoh tipe yang mewakili dimensi itu sendiri, dan sering kali menetapkan sebelumnya satu set contoh tetap, baik dengan menggunakan variabel statis, atau dengan menggunakan tipe pencacahan eksplisit. Dalam Cantuman 1, aturan ini akan berlaku untuk dimensi "level".

Gambar 3. Di dalam dimensi Level

Aturan umum ortogonalitas adalah menghindari referensi ke jenis konkret tertentu yang mewakili aspek (dimensi) lain dari program. Ini memungkinkan Anda untuk menulis kode generik yang akan bekerja dengan cara yang sama untuk semua kemungkinan contoh. Kode tersebut masih dapat mereferensikan properti instance, selama mereka adalah bagian dari antarmuka dari jenis yang menentukan dimensi.

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

Dalam artikel ini saya telah menggunakan Log4j sebagai contoh untuk mendemonstrasikan prinsip desain ortogonalitas dan sesekali trade-off antara mengikuti prinsip desain dan mencapai atribut kualitas sistem seperti skalabilitas. Bahkan dalam kasus di mana tidak mungkin untuk mencapai ortogonalitas penuh, saya percaya bahwa trade-off haruslah keputusan yang disengaja, dan harus didokumentasikan dengan baik (misalnya, sebagai hutang teknis). Lihat bagian Sumber Daya untuk mempelajari lebih lanjut tentang konsep dan teknologi yang dibahas dalam artikel ini.