Programmering

Log4j ortogonalitet ved eksempel

Orthogonality er et konsept som ofte brukes til å beskrive modulær og vedlikeholdbar programvare, men det blir lettere forstått gjennom en casestudie. I denne artikkelen avmystifiserer Jens Dietrich ortogonalitet og noen relaterte designprinsipper ved å demonstrere deres bruk i det populære Log4j-verktøybiblioteket. Han diskuterer også hvordan Log4j bryter ortogonalitet i et par tilfeller og diskuterer mulige løsninger på problemene som ble reist.

Begrepet ortogonalitet er basert på det greske ordet ortogonios, som betyr "rettvinklet." Det brukes ofte til å uttrykke uavhengigheten mellom forskjellige dimensjoner. Når et objekt beveger seg langs x-akse i et tredimensjonalt rom, dens y og z koordinatene endres ikke. Endring i en dimensjon forårsaker ikke endring i en annen dimensjon, noe som betyr at en dimensjon ikke kan forårsake bivirkninger for andre.

Dette forklarer hvorfor begrepet ortogonalitet ofte brukes til å beskrive modulær og vedlikeholdbar programvaredesign: å tenke på systemer som punkter i et flerdimensjonalt rom (skapt av uavhengige, ortogonale dimensjoner) hjelper programvareutviklere til å sikre at våre endringer i ett aspekt av systemet vil ikke ha bivirkninger for en annen.

Det hender at Log4j, en populær loggpakke med åpen kildekode for Java, er et godt eksempel på en modulær design basert på ortogonalitet.

Dimensjonene til Log4j

Logging er bare en mer avansert versjon av System.out.println () uttalelse, og Log4j er en verktøypakke som trekker ut mekanikken for logging på Java-plattformen. Log4j-funksjonene lar blant annet utviklere gjøre følgende:

  • Logg på forskjellige appenders (ikke bare konsollen, men også til filer, nettverkssteder, relasjonsdatabaser, operativsystemloggverktøy og mer)
  • Logg på flere nivåer (for eksempel FEIL, WARN, INFO og DEBUG)
  • Kontroller sentralt hvor mye informasjon som logges på et gitt loggnivå
  • Bruk forskjellige oppsett for å definere hvordan en logghendelse blir gjengitt til en streng

Mens Log4j har andre funksjoner, vil jeg fokusere på disse tre dimensjonene av funksjonaliteten for å utforske konseptet og fordelene med ortogonalitet. Merk at diskusjonen min er basert på Log4j versjon 1.2.17.

Log4j på JavaWorld

Få en oversikt over Log4j og lære å skrive dine egne tilpassede Log4j appenders. Vil du ha flere Java-opplæringsprogrammer? Hent Enterprise Java-nyhetsbrev levert til innboksen din.

Vurderer Log4j-typer som aspekter

Appenders, nivå og layout er tre aspekter av Log4j som kan sees på som uavhengige dimensjoner. Jeg bruker begrepet aspekt her som et synonym for bekymring, som betyr et stykke interesse eller fokus i et program. I dette tilfellet er det enkelt å definere disse tre bekymringene basert på spørsmålene som hver adresserer:

  • Appender: Hvor skal logghendelsesdataene sendes for visning eller lagring?
  • Oppsett: Hvordan skal en logghendelse presenteres?
  • Nivå: Hvilke logghendelser skal behandles?

Prøv nå å vurdere disse aspektene sammen i et tredimensjonalt rom. Hvert punkt i dette rommet representerer en gyldig systemkonfigurasjon, som vist i figur 1. (Merk at jeg tilbyr en litt forenklet visning av Log4j: Hvert punkt i figur 1 er faktisk ikke en global systemomfattende konfigurasjon, men en konfigurasjon for en bestemt logger. Loggerne selv kan betraktes som en fjerde dimensjon.)

Oppføring 1 er en typisk kodebit som implementerer Log4j:

Oppføring 1. Et eksempel på implementering av Log4j

// oppsettlogging! Logger logger = Logger.getLogger ("Foo"); Appender appender = ny ConsoleAppender (); Layoutlayout = ny org.apache.log4j.TTCCLayout () appender.setLayout (layout); logger.addAppender (appender); logger.setLevel (Level.INFO); // begynn å logge! logger.warn ("Hello World");

Det jeg vil at du skal legge merke til om denne koden er at den er ortogonal: du kan endre appender, layout eller nivåaspekt uten å bryte koden, noe som vil forbli helt funksjonell. I en ortogonal design er hvert punkt i det gitte rommet i programmet en gyldig systemkonfigurasjon. Ingen begrensning er tillatt å begrense hvilke punkter i mulige konfigurasjoner som er gyldige eller ikke.

Orthogonality er et kraftig konsept fordi det gjør det mulig for oss å etablere en relativt enkel mental modell for komplekse applikasjonsbrukstilfeller. Spesielt kan vi fokusere på en dimensjon mens vi ignorerer andre aspekter.

Testing er et vanlig og kjent scenario der ortogonalitet er nyttig. Vi kan teste funksjonaliteten til loggnivåer ved hjelp av et passende fast par av en appender og en layout. Orthogonality sikrer oss at det ikke blir noen overraskelser: loggnivåer fungerer på samme måte med en gitt kombinasjon av appender og layout. Ikke bare er dette praktisk (det er mindre arbeid å gjøre), men det er også nødvendig, fordi det ville være umulig å teste loggnivåer med alle kjente kombinasjoner av appender og layout. Dette gjelder spesielt med tanke på at Log4j, som mange programvareverktøy og verktøy, er designet for å utvides av tredjeparter.

Reduksjonen i kompleksitet som ortogonalitet fører til programvarene, ligner på hvordan dimensjoner brukes i geometri, der den kompliserte bevegelsen av punkter i et n-dimensjonalt rom blir brutt ned til den relativt enkle manipuleringen av vektorer. Hele feltet med lineær algebra er basert på denne kraftige ideen.

Utforming og koding for ortogonalitet

Hvis du nå lurer på hvordan du kan designe og kode ortogonalitet i programmene dine, så er du på rett sted. Hovedideen er å bruke abstraksjon. Hver dimensjon i et ortogonalt system adresserer ett bestemt aspekt av programmet. En slik dimensjon vil vanligvis være representert med en type (klasse, grensesnitt eller oppregning). Den vanligste løsningen er å bruke en abstrakt type (grensesnitt eller abstrakt klasse). Hver av disse typene representerer en dimensjon, mens typeforekomsten representerer punktene innenfor den gitte dimensjonen. Fordi abstrakte typer ikke kan instantiseres direkte, er det også nødvendig med konkrete klasser.

I noen tilfeller kan vi klare oss uten dem. For eksempel trenger vi ikke konkrete klasser når typen bare er en markering, og ikke innkapsler oppførsel. Da kan vi bare instantiere typen som representerer selve dimensjonen, og ofte definere et fast sett med forekomster, enten ved å bruke statiske variabler, eller ved å bruke en eksplisitt oppregningstype. I oppføring 1 vil denne regelen gjelde for "nivå" -dimensjonen.

Figur 3. Inne i nivådimensjonen

Den generelle regelen om ortogonalitet er å unngå referanser til spesifikke konkrete typer som representerer andre aspekter (dimensjoner) av programmet. Dette lar deg skrive generisk kode som fungerer på samme måte for alle mulige tilfeller. Slik kode kan fremdeles referere til forekomster, så lenge de er en del av grensesnittet av typen som definerer dimensjonen.

For eksempel i Log4j den abstrakte typen Oppsett definerer metoden ignorererTrowable (). Denne metoden returnerer en boolsk som indikerer om oppsettet kan gjengi unntaksspor eller ikke. Når en appender bruker et oppsett, ville det være helt greit å skrive betinget kode på ignorererTrowable (). For eksempel kan en filappender skrive ut unntaksspor på System.err når du bruker en layout som ikke kan håndtere unntak.

På en lignende måte, a Oppsett implementering kan referere til et bestemt Nivå når du gjengir logghendelser. For eksempel hvis loggnivået var Nivå. FEIL, kan en HTML-basert layoutimplementering pakke loggmeldingen i koder som gjengir den i rødt. Igjen, poenget er at Nivå. FEIL er definert av Nivå, typen som representerer dimensjonen.

Du bør imidlertid unngå referanser til spesifikke implementeringsklasser for andre dimensjoner. Hvis en appender bruker en layout, er det ikke nødvendig å vite hvilken type av layout er det. Figur 4 illustrerer gode og dårlige referanser.

Flere mønstre og rammer gjør det lettere å unngå avhengigheter til implementeringstyper, inkludert avhengighetsinjeksjon og tjenestelokatormønster.

Krenkende ortogonalitet

Samlet sett er Log4j et godt eksempel på bruk av ortogonalitet. Imidlertid bryter noen koder i Log4j dette prinsippet.

Log4j inneholder en appender som heter JDBCAppender, som brukes til å logge på en relasjonsdatabase. Gitt skalerbarheten og populariteten til relasjonsdatabase, og det faktum at dette gjør logghendelser lett søkbare (med SQL-spørsmål), JDBCAppender er en viktig brukssak.

JDBCAppender er ment å løse problemet med logging til en relasjonsdatabase ved å gjøre logghendelser til SQL SETT INN uttalelser. Det løser dette problemet ved å bruke en Mønsteroppsett.

Mønsteroppsett bruker mal for å gi brukeren maksimal fleksibilitet til å konfigurere strengene generert fra logghendelser. Malen er definert som en streng, og variablene som brukes i malen, instantieres fra logghendelser ved kjøretid, som vist i liste 2.

Oppføring 2. PatternLayout

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

JDBCAppender bruker en Mønsteroppsett med et mønster som definerer SQL SETT INN uttalelse. Spesielt kan følgende kode brukes til å angi SQL-setningen som brukes:

Oppføring 3. SQL sett inn uttalelse

offentlig ugyldig setSql (String s) {sqlStatement = s; if (getLayout () == null) {this.setLayout (new PatternLayout (s)); } annet {((PatternLayout) getLayout ()). setConversionPattern (s); }}

Innebygd i denne koden er den implisitte antagelsen om at oppsettet, hvis det er angitt før du bruker setLayout (Layout) metoden definert i Appender, er faktisk en forekomst av Mønsteroppsett. Når det gjelder ortogonalitet, betyr dette at plutselig mange punkter i 3D-kuben som bruker JDBCAppender med andre oppsett enn Mønsteroppsett representerer ikke gyldige systemkonfigurasjoner lenger! Det vil si at ethvert forsøk på å sette SQL-strengen med et annet oppsett vil resultere i et unntak for kjøretid (klassekast).

Figur 5. JDBCAppender som bryter ortogonalitet

Det er en annen grunn til at JDBCAppenderdesign er tvilsom. JDBC har sine egne malmotorer utarbeidet uttalelser. Ved bruk av Mønsteroppsettmalmotoren er imidlertid forbigått. Dette er uheldig fordi JDBC forhåndskompilerer utarbeidede uttalelser, noe som fører til betydelige ytelsesforbedringer. Dessverre er det ingen enkel løsning for dette. Den åpenbare tilnærmingen vil være å kontrollere hva slags layout som kan brukes i JDBCAppender ved å overstyre setter som følger.

Oppføring 4. Overriding setLayout ()

public void setLayout (Layout layout) {if (layout instanceOf PatternLayout) {super.setLayout (layout); } annet {kast ny IllegalArgumentException ("Layout er ikke gyldig"); }}

Dessverre har denne tilnærmingen også problemer. Metoden i oppføring 4 gir et unntak for kjøretid, og applikasjoner som kaller denne metoden er kanskje ikke forberedt på å fange den. Med andre ord, den setLayout (Layout layout) metoden kan ikke garantere at ingen runtime unntak blir kastet; det svekker derfor garantiene (postconditions) som gis ved metoden den overstyrer. Hvis vi ser på det i form av forutsetninger, setLayout krever at oppsettet er en forekomst av Mønsteroppsett, og har derfor sterkere forutsetninger enn metoden den overstyrer. Uansett har vi brutt et kjerne objektorientert designprinsipp, som er Liskov-substitusjonsprinsippet som brukes for å sikre arv.

Løsninger

Det faktum at det ikke er noen enkel løsning å fikse utformingen av JDBCAppender indikerer at det er et dypere problem på jobben. I dette tilfellet er abstraksjonsnivået valgt når du designer kjerne-abstrakte typer (spesielt Oppsett) trenger finjustering. Kjernemetoden definert av Oppsett er format (LoggingEvent hendelse). Denne metoden returnerer en streng. Når du logger på en relasjonsdatabase, trenger det imidlertid å generere en mengde verdier (en rad), og ikke en streng.

En mulig løsning ville være å bruke en mer sofistikert datastruktur som en returtype for format. Dette vil imidlertid innebære ytterligere overhead i situasjoner der du kanskje vil generere en streng. Ytterligere mellomliggende gjenstander må opprettes og deretter samles søppel, noe som kompromitterer ytelsen til loggerammene. Å bruke en mer sofistikert returtype vil også gjøre Log4j vanskeligere å forstå. Enkelhet er et veldig ønskelig designmål.

En annen mulig løsning ville være å bruke "lagdelt abstraksjon" ved å bruke to abstrakte typer, Appender og TilpassesAppender som strekker seg Appender. Kun TilpassesAppender vil da definere metoden setLayout (Layout layout). JDBCAppender ville bare implementere Appender, mens andre appender-implementeringer som ConsoleAppender ville gjennomføre TilpassesAppender. Ulempen med denne tilnærmingen er den økte kompleksiteten (f.eks. Hvordan Log4j-konfigurasjonsfiler behandles), og det faktum at utviklere må ta en informert beslutning om hvilket abstraksjonsnivå de skal bruke tidlig.

For å konkludere

I denne artikkelen har jeg brukt Log4j som et eksempel for å demonstrere både designprinsippet om ortogonalitet og en og annen avveining mellom å følge et designprinsipp og å oppnå en systemkvalitetsattributt som skalerbarhet. Selv i tilfeller der det er umulig å oppnå full ortogonalitet, mener jeg at kompromisset skal være en bevisst beslutning, og at det skal være godt dokumentert (for eksempel som teknisk gjeld). Se seksjonen Ressurser for å lære mer om konseptene og teknologiene som er diskutert i denne artikkelen.

$config[zx-auto] not found$config[zx-overlay] not found