Programmering

Log4j ortogonalitet som eksempel

Orthogonality er et koncept, der ofte bruges til at beskrive modulær og vedligeholdelig software, men det forstås lettere ved hjælp af en casestudie. I denne artikel afmystificerer Jens Dietrich ortogonalitet og nogle relaterede designprincipper ved at demonstrere deres anvendelse i det populære Log4j-hjælpebibliotek. Han diskuterer også, hvordan Log4j krænker ortogonalitet i et par tilfælde og diskuterer mulige løsninger på de rejste problemer.

Begrebet ortogonalitet er baseret på det græske ord ortogonios, der betyder "retvinklet." Det bruges ofte til at udtrykke uafhængigheden mellem forskellige dimensioner. Når en genstand bevæger sig langs x-aks i et tredimensionelt rum, dets y og z koordinater ændres ikke. Ændring i en dimension medfører ikke ændringer i en anden dimension, hvilket betyder, at en dimension ikke kan forårsage bivirkninger for andre.

Dette forklarer, hvorfor begrebet ortogonalitet ofte bruges til at beskrive modulært og vedligeholdeligt softwaredesign: at tænke på systemer som punkter i et multidimensionelt rum (skabt af uafhængige, ortogonale dimensioner) hjælper softwareudviklere med at sikre, at vores ændringer til et aspekt af systemet vil ikke have bivirkninger for en anden.

Det sker, at Log4j, en populær open source-logningspakke til Java, er et godt eksempel på et modulært design baseret på ortogonalitet.

Dimensionerne på Log4j

Logning er bare en mere avanceret version af System.out.println () erklæring, og Log4j er en hjælpepakke, der abstrakterer logikken på Java-platformen. Log4j-funktioner giver blandt andet udviklere mulighed for at gøre følgende:

  • Log på forskellige appenders (ikke kun konsollen men også til filer, netværksplaceringer, relationsdatabaser, operativsystemets logværktøjer og mere)
  • Log på flere niveauer (såsom FEJL, ADVARSEL, INFO og AFBRUG)
  • Styr centralt, hvor meget information der logges på et givet logningsniveau
  • Brug forskellige layouts til at definere, hvordan en logningshændelse gengives til en streng

Mens Log4j har andre funktioner, vil jeg fokusere på disse tre dimensioner af dens funktionalitet for at udforske konceptet og fordelene ved ortogonalitet. Bemærk, at min diskussion er baseret på Log4j version 1.2.17.

Log4j på JavaWorld

Få en oversigt over Log4j og lære at skrive din egen brugerdefinerede Log4j appenders. Vil du have flere Java-tutorials? Hent Enterprise Java-nyhedsbrev leveret til din indbakke.

Overvejer Log4j-typer som aspekter

Appenders, niveau og layout er tre aspekter af Log4j, der kan ses som uafhængige dimensioner. Jeg bruger udtrykket aspekt her som et synonym for bekymring, hvilket betyder et stykke interesse eller fokus i et program. I dette tilfælde er det let at definere disse tre bekymringer ud fra de spørgsmål, som hver behandler:

  • Appender: Hvor skal loghændelsesdataene sendes til visning eller opbevaring?
  • Layout: Hvordan skal en loghændelse præsenteres?
  • Niveau: Hvilke loghændelser skal behandles?

Prøv nu at overveje disse aspekter sammen i et tredimensionelt rum. Hvert punkt i dette rum repræsenterer en gyldig systemkonfiguration, som vist i figur 1. (Bemærk at jeg tilbyder en lidt forenklet visning af Log4j: Hvert punkt i figur 1 er faktisk ikke en global systemdækkende konfiguration, men en konfiguration til en bestemt logger. Selve loggerne kan betragtes som en fjerde dimension.)

Liste 1 er et typisk kodestykke, der implementerer Log4j:

Fortegnelse 1. Et eksempel på implementering af Log4j

// opsætningslogning! Logger logger = Logger.getLogger ("Foo"); Appender appender = ny ConsoleAppender (); Layoutlayout = nyt org.apache.log4j.TTCCLayout () appender.setLayout (layout); logger.addAppender (appender); logger.setLevel (Level.INFO); // start logning! logger.warn ("Hello World");

Hvad jeg vil have dig til at bemærke om denne kode er, at den er ortogonal: du kan ændre appender, layout eller niveau aspekt uden at bryde koden, hvilket forbliver helt funktionelt. I et ortogonalt design er hvert punkt i det givne rum i programmet en gyldig systemkonfiguration. Ingen begrænsning er tilladt at begrænse, hvilke punkter i rummet af mulige konfigurationer er gyldige eller ej.

Orthogonality er et stærkt koncept, fordi det giver os mulighed for at etablere en relativt simpel mental model til komplekse applikationsanvendelsessager. Især kan vi fokusere på en dimension, mens vi ignorerer andre aspekter.

Test er et almindeligt og velkendt scenario, hvor ortogonalitet er nyttigt. Vi kan teste funktionaliteten af ​​logniveauer ved hjælp af et passende fast par af en appender og et layout. Orthogonality sikrer os, at der ikke er nogen overraskelser: logniveauer fungerer på samme måde med en given kombination af appender og layout. Ikke kun er dette praktisk (der er mindre arbejde at gøre), men det er også nødvendigt, fordi det ville være umuligt at teste logniveauer med enhver kendt kombination af appender og layout. Dette gælder især i betragtning af, at Log4j, ligesom mange softwareværktøjer og hjælpeprogrammer, er designet til at blive udvidet af tredjeparter.

Den reduktion i kompleksitet, som ortogonalitet medfører softwareprogrammer, svarer til hvordan dimensioner bruges i geometri, hvor den komplicerede bevægelse af punkter i et n-dimensionelt rum er opdelt til den relativt enkle manipulation af vektorer. Hele feltet med lineær algebra er baseret på denne stærke idé.

Design og kodning for ortogonalitet

Hvis du nu undrer dig over, hvordan du designer og kode ortogonalitet i dine programmer, så er du på det rigtige sted. Hovedideen er at bruge abstraktion. Hver dimension af et ortogonalt system vedrører et bestemt aspekt af programmet. En sådan dimension vil normalt være repræsenteret af en type (klasse, interface eller optælling). Den mest almindelige løsning er at bruge en abstrakt type (interface eller abstrakt klasse). Hver af disse typer repræsenterer en dimension, mens typeforekomsten repræsenterer punkterne inden for den givne dimension. Da abstrakte typer ikke direkte kan instantieres, er der også behov for konkrete klasser.

I nogle tilfælde kan vi undvære dem. For eksempel har vi ikke brug for konkrete klasser, når typen kun er en markering og ikke indkapsler adfærd. Derefter kan vi bare instantiere den type, der repræsenterer selve dimensionen, og foruddefinerer ofte et fast sæt forekomster, enten ved hjælp af statiske variabler eller ved hjælp af en eksplicit optællingstype. I liste 1 gælder denne regel for dimensionen "niveau".

Figur 3. Inde i niveau-dimensionen

Den generelle regel om ortogonalitet er at undgå henvisninger til specifikke konkrete typer, der repræsenterer andre aspekter (dimensioner) af programmet. Dette giver dig mulighed for at skrive generisk kode, der fungerer på samme måde i alle mulige tilfælde. En sådan kode kan stadig henvise til egenskaber for forekomster, så længe de er en del af grænsefladen af ​​den type, der definerer dimensionen.

For eksempel er den abstrakte type i Log4j Layout definerer metoden ignorererTrowable (). Denne metode returnerer en boolsk, der angiver, om layoutet kan gengive undtagelsesstakspor eller ej. Når en appender bruger et layout, ville det være helt fint at skrive betinget kode på ignorererTrowable (). For eksempel kunne en filappender udskrive undtagelsesstakkspor på System.err når du bruger et layout, der ikke kan håndtere undtagelser.

På en lignende måde a Layout implementering kan henvise til et bestemt Niveau ved gengivelse af logningshændelser. For eksempel hvis logniveauet var Niveau. FEJL, kunne en HTML-baseret layoutimplementering indpakke logbeskeden i tags, der gengiver den i rødt. Igen er pointen det Niveau. FEJL er defineret af Niveau, typen, der repræsenterer dimensionen.

Du bør dog undgå henvisninger til specifikke implementeringsklasser for andre dimensioner. Hvis en appender bruger et layout, er der ingen grund til at vide det hvilken slags af layout er det. Figur 4 illustrerer gode og dårlige referencer.

Flere mønstre og rammer gør det lettere at undgå afhængigheder af implementeringstyper, herunder afhængighedsinjektion og servicelokatormønster.

Krænkende ortogonalitet

Samlet set er Log4j et godt eksempel på brugen af ​​ortogonalitet. Imidlertid overtræder nogle koder i Log4j dette princip.

Log4j indeholder en appender kaldet JDBCAppender, som bruges til at logge på en relationsdatabase. I betragtning af skalationsbarhed og popularitet af relationsdatabase og det faktum, at dette gør loghændelser let søgbare (med SQL-forespørgsler), JDBCAppender er en vigtig brugssag.

JDBCAppender er beregnet til at løse problemet med logning til en relationsdatabase ved at omdanne loghændelser til SQL INDSÆT udsagn. Det løser dette problem ved hjælp af en Mønsterlayout.

Mønsterlayout bruger skabeloner for at give brugeren maksimal fleksibilitet til at konfigurere strengene genereret fra loghændelser. Skabelonen er defineret som en streng, og de variabler, der bruges i skabelonen, instantieres fra loghændelser ved kørsel, som vist i liste 2.

Annonce 2. PatternLayout

Strengmønster = "% p [@% d {dd MMM åååå HH: mm: ss} i% t]% m% n"; Layoutlayout = nyt org.apache.log4j.PatternLayout (mønster); appender.setLayout (layout);

JDBCAppender bruger en Mønsterlayout med et mønster, der definerer SQL INDSÆT udmelding. Især kan følgende kode bruges til at indstille den anvendte SQL-sætning:

Liste 3. SQL indsæt sætning

public void setSql (String s) {sqlStatement = s; hvis (getLayout () == null) {this.setLayout (nye mønsterLayout (er)); } andet {((PatternLayout) getLayout ()). setConversionPattern (s); }}

Indbygget i denne kode er den implicitte antagelse om, at layoutet, hvis det er indstillet inden brug af setLayout (layout) metode defineret i Appender, er faktisk en forekomst af Mønsterlayout. Med hensyn til ortogonalitet betyder det, at pludselig mange punkter i 3D-terningen, der bruger JDBCAppender med andre layouter end Mønsterlayout repræsenterer ikke gyldige systemkonfigurationer længere! Det vil sige, at ethvert forsøg på at indstille SQL-strengen med et andet layout vil resultere i en runtime (class cast) undtagelse.

Figur 5. JDBCAppender krænker ortogonalitet

Der er en anden grund JDBCAppenderdesign er tvivlsom. JDBC har sine egne erklæringer, der er udarbejdet med skabelonmotorer. Ved hjælp af Mønsterlayoutdog forbigås skabelonmotoren. Dette er uheldigt, fordi JDBC prækompilerer udarbejdede udsagn, hvilket fører til betydelige præstationsforbedringer. Desværre er der ingen nem løsning på dette. Den åbenlyse tilgang ville være at kontrollere, hvilken type layout der kan bruges i JDBCAppender ved at tilsidesætte setter som følger.

Fortegnelse 4. Overriding setLayout ()

public void setLayout (Layout layout) {if (layout instanceOf PatternLayout) {super.setLayout (layout); } ellers {kast nyt IllegalArgumentException ("Layout er ikke gyldigt"); }}

Desværre har denne tilgang også problemer. Metoden i liste 4 kaster en undtagelse for kørsel, og applikationer, der kalder denne metode, er muligvis ikke klar til at fange den. Med andre ord, setLayout (layoutlayout) metoden kan ikke garantere, at der ikke kastes nogen undtagelse for runtime; det svækker derfor de garantier (postconditions), der gives ved den metode, den tilsidesætter. Hvis vi ser på det i form af forudsætninger, setLayout kræver, at layoutet er en forekomst af Mønsterlayoutog har derfor stærkere forudsætninger end den metode, den tilsidesætter. Uanset hvad har vi overtrådt et kerneobjektorienteret designprincip, som er Liskov-substitutionsprincippet, der bruges til at beskytte arv.

Løsninger

Det faktum, at der ikke er nogen nem løsning at rette designet på JDBCAppender indikerer, at der er et dybere problem på arbejdspladsen. I dette tilfælde er det abstraktionsniveau, der vælges ved design af kerneabstrakt-typerne (især Layout) skal finjusteres. Kernemetoden defineret af Layout er format (LoggingEvent begivenhed). Denne metode returnerer en streng. Når du logger på en relationsdatabase, skal der imidlertid genereres en tuple af værdier (en række) og ikke en streng.

En mulig løsning ville være at bruge en mere sofistikeret datastruktur som en returtype for format. Dette vil imidlertid medføre yderligere omkostninger i situationer, hvor du måske faktisk vil generere en streng. Yderligere mellemliggende objekter skulle oprettes og derefter indsamles skrald, hvilket kompromitterer præstationen af ​​logningsrammen. Brug af en mere sofistikeret returtype ville også gøre Log4j sværere at forstå. Enkelhed er et meget ønskeligt designmål.

En anden mulig løsning ville være at bruge "lagdelt abstraktion" ved at bruge to abstrakte typer, Appender og Kan tilpassesAppender som strækker sig Appender. Kun Kan tilpassesAppender ville derefter definere metoden setLayout (layoutlayout). JDBCAppender kun ville gennemføre Appender, mens andre appender-implementeringer som f.eks ConsoleAppender ville gennemføre Kan tilpassesAppender. Ulempen ved denne tilgang er den øgede kompleksitet (f.eks. Hvordan Log4j-konfigurationsfiler behandles) og det faktum, at udviklere skal træffe en informeret beslutning om, hvilket niveau af abstraktion der skal bruges tidligt.

Afslutningsvis

I denne artikel har jeg brugt Log4j som et eksempel til at demonstrere både designprincippet om ortogonalitet og lejlighedsvis kompromis mellem at følge et designprincip og opnå en systemkvalitetsattribut som skalerbarhed. Selv i tilfælde, hvor det er umuligt at opnå fuld ortogonalitet, mener jeg, at kompromiset skal være en bevidst beslutning, og at det skal være veldokumenteret (for eksempel som teknisk gæld). Se afsnittet Ressourcer for at lære mere om de begreber og teknologier, der diskuteres i denne artikel.