8. kodutöö (alternatiiv): Ärireeglid
See on teine alternatiivne kodutöö! Kodutöö tähtaeg on 1. juuni ja kuigi see sobib siia interpretaatorite juurde, siis pigem tuleks lahendada ka interpretaatori kodutöö ja jätta viimane kodutöö lahendamata.
Tarkvaraarenduses on ärireeglid otsustusjuhiste kogum, mida kasutatakse süsteemi käitumise reguleerimiseks, võttes arvesse erinevaid sisendeid ja sündmusi. Ärireeglid aitavad otsusteprotsesse automatiseerida. Ärireeglite eraldamine lähtekoodist tagab selle, et neid reegleid on pärast lihtsam hallata. Olenevalt kasutatud tehnoloogiast võib reeglite eraldamine koodist isegi lubada reegleid koostada isikutel, kes ei oska programmeerida. Siin kodutöös käsitleme kahte sellist ärireeglite keelt – JsonLogic ja Easy Rules.
JsonLogic
JsonLogic on JSON formaadis ja piiratud funktsioonidega keel. JsonLogic põhineb reeglite defineerimisel, mida saab siis andmete sisestamisel kasutada otsuste tegemiseks. Keele väga ühtlane formaat teeb reeglite käsitsi kirjutamise kohmakamaks, aga selles kirjutatud reegleid on lihtsam töödelda. Näiteks Euroopa Liidul on keskne pandeemiasertifikaatide kontrollimise raamistik, kus riikidesse sisenemise tingimused on formaliseeritud JsonLogicul põhinevas formaadis (CertLogic).
Formaadi lihtsus seisneb selles, et iga tema json objekt on kujul: { "operaator": [ ... ] }
. Lubatud on samuti primitiivtüübid (vt. JSONi kodutöö), aga kõik objektid on ühesugused. Seega, JsonLogicus on kõik operaatorid ja võtmesõnad defineeritud välja võtme osas ja välja väärtuse osas on operandid. Järgmises tabelis on mõned näited (kõiki operatsioone saab näha siit).
Java | JsonLogic | |
---|---|---|
Avaldis |
4 + 2 |
{"+": [4, 2]} |
Muutujad |
kala |
{"var": ["kala"]} |
Tingimused |
if (temp < 0) return "freezing"; else if (temp < 100) return "liquid"; else return "gas"; |
{"if" : [ {"<": [{"var":["temp"]}, 0] }, "freezing", {"<": [{"var":["temp"]}, 100] }, "liquid", "gas" ]} |
NB! Tingimuslause korral on argumendid lihtsalt järjest komaga eraldatud. Üksikul if-lausel on kolm argumenti: tingimus, tõese tingimuse tulemus ja väära tingimuse tulemus. Tingimuslauseid saab ka üksteise järele panna. Sel juhul on massiivi esimene element esimese if lause tingimus ja massiivi teine element tagastatav väärtus kui tingimus on tõene. Samamoodi kolmanda ja neljanda elemendiga massiivis, jne. Viimane element on samaväärne else-lausega.
NB! Muutuja juures võib kirjutada lihtsalt {"var": "kala"}
. Formaat pidi olema ühtlane, aga see väike erand on siin muutuja juures, et oleks natuke mugavam kirjutada.
JsonLogic programmide väärtustamine
Selleks, et meie JsonLogic kood midagi teeks, peame JsonLogic interpretaatori käivitama. Kuna meil on ilmselt ka muutujad meie JSON koodis, siis peame need enne jooksutamist JsonLogicule andma. Javascriptis on andmed samuti JSON formaadis ja selliselt saab nende veebilehel koodi katsetada.
Paremal on pildil reegliks ülalolev tingimuslause. Kui andmeks talle ette anda JSON objekt { "temp": 37 }
, siis on tulemuseks "liquid".
Javas käivitame JsonLogicut apply meetodiga, kus anname edasi Mapiga, kus key on muutuja nimi ja value on muutuja väärtus:
// Kõigepealt peab looma JsonLogicu isendi! JsonLogic jsonLogic = new JsonLogic(); // Andmed esitame Mapis: nimi -> väärtus Map<String, Integer> data = Map.of("temp", 37); // Käivitame JsonLogicu interpretaatori apply meetodiga, // esimene argument on ülalolev tingimuslause JSON sõnena System.out.println(jsonLogic.apply(jsonString, data));
JsonLogic üleanded
Repositooriumis on paketis week8.altbusinessrules klass nimega JsonLogicExercise, kus on kõigepealt üks väike näide palga arvutamise reeglist ja selle rakendamisest. Seda reeglit on meil pärast vaja analüüsiks, aga ta võib ka olla abiks esimese ülesande lahendamisel.
1. ülesanne (allahindlus)
Defineeri JsonLogic avaldis, mis olenevalt kliendi eelnevatest kulutustest, määrab allahindluse ostule. Täpsemalt, kui klient on eelnevalt kulutanud kas 1000€ või rohkem poes, siis määratakse 10% allahindlus. Kui klient on kulutanud alla 1000€ aga 200€ või rohkem, siis 5% allahindlus. Kui klient on kulutanud vähem kui 200€, siis allahindlust ei ole. Tagastage ostu korrektne lõpphind. Sisendiks on seega kaks muutujat:
- spent: kui palju on klient juba kulutanud.
- price: toote hind, millele tuleb soodustus rakendada.
Näiteks sisendi { "spent": 465, "price": 100 }
korral on tulemuseks 95.
2. ülesanne (jada operatsioonid)
Siin on eesmärk kirjutada JSON-loogika reegel, mis paneb sisendsõnade jadast kokku ühe terviksõna, kasutades aga ainult need sõnad sisendjadast, mis sisaldavad tähte 'a'. Sisendiks on siis sõnade jada, words, aga tulemuseks peaks olema üks sõna. Näiteks, JSON formaadis sisendi { "words": ["kartuli", "püree", "salat", "kivi"] }
korral peaks olema tulemuseks "kartulisalat". Jällegi on mugav seda katsetada jsonlogic/play lehel.
See on lihtsalt tehislik ülesanne, et harjutada jada operatsioone filter ja reduce. Vajalike meetodite kohta võib lugeda dokumentatsioonist: Jada operatsioonid ja sõne operatsioonid. NB! Dokumentatsioonis on üsna kohmakalt tehtud, et mitmerealised näited on seal üherealistes kastides. Seetõttu needsamad näited on siin tabelis nähtavamalt ja nende all on näha Java vasted:
filter | reduce |
---|---|
{"filter":[ {"var":"integers"}, {"%":[{"var":""},2]} ]} |
{"reduce":[ {"var":"integers"}, {"+":[{"var":"current"}, {"var":"accumulator"}]}, 0 ]} |
IntStream integers = ... integers.filter(i -> i % 2 == 0) |
IntStream integers = ... integers.reduce(0, (e, acc) -> e + acc) |
Sisendiks on siis muutuja integers, mis on siis täisarvude jada. Filter funktsioon eemaldab esimese argumendina antud massiivist kõik elemendid, mille puhul vastab teise argumendina antud predikaat tõele. Siin tuleb märkida, et {"var":""}
vastab massiivi hetkesele elemendile. Reduce selles näites liidab kõik sisendjada arve kokku ja siin on samuti kasutusel kindlate nimedega muutujad, current ja accumulator, mis tähistavad siis vastavalt hetkel vaadeldavat elementi ja siiamaani arvutatud summa.
PS. Ülesanne lahendamiseks ei pea isegi liiga sügavalt aru saame reduce funktsiooni täitmisest, sest siin on lihtsalt vaja %
ja +
asendada õigete sõne meetoditega (neid on ainult in, cat või substr). Muidugi peab midagi seal ka muuta, et tulemus oleks "kartulisalat" ja mitte "salatkartuli"!
3. ülesanne (palgareeglite analüüs)
Me oleme rõhutanud, et JsonLogic on lihtne keel, mistõttu on palju lihtsam kindlaks teha, mida iga reegel täpselt teeb. Oletame, et meil on järgmised suurepärased palga arvutamise reeglid. Need on võetud ideaalsest maailmast, kus testimine on kõrgelt väärtustatud ja küberkuritegevus elimineeritud. Kraadide korrutised on võetud siit, aga me ei tegele siin karjäärinõustamisega...
Aus reegel | Diskrimineeriv reegel |
---|---|
{"*": [ {"if" : [ {"==": [{"var":"job"}, "programmer"]}, 1100, {"==": [{"var":"job"}, "designer"]}, 1300, {"==": [{"var":"job"}, "tester"]}, 1400, 1000 ]}, {"if" : [ {"==": [{"var":"degree"}, "bsc"]}, 2.1, {"==": [{"var":"degree"}, "msc"]}, 2.5, {"==": [{"var":"degree"}, "phd"]}, 3.4, 1.2 ]} ]} |
{"*": [ {"if" : [ {"==": [{"var":"job"}, "programmer"]}, 1100, {"==": [{"var":"job"}, "designer"]}, 1300, {"==": [{"var":"job"}, "tester"]}, 1400, 1000 ]}, {"if" : [ {"==": [{"var":"gender"}, "male"]}, 1.3, {"==": [{"var":"gender"}, "female"]}, 1.0, 0.8 ]} ]} |
Üks reegel on ilmselt parem kui teine, aga igaüks võib ise otsustada, mis maailmas tahab elada: ülesanne on kirjutada lihtne meetod analyzeJsonRule
, mis tagastaks kõik muutujad, millest palga arvutamine sõltub. Selle lahendamiseks soovitaks kasutada Gson teeki!
Easy Rules
JsonLogic abil saab ühte avaldist väärtustada, aga nüüd tutvume reeglimasinaga, millega saab äriloogika reeglite komplektina spetsifitseerida. Easy Rules on Javas kirjutatud reeglimasin, mis lubab meil defineerida reegleid ja nendele vastavaid käske, mis võivad kasutada meie ülejäänud Java koodi meetodeid.
Easy Rules loojad olid suuresti inspireeritud Martin Fowleri artiklist nimega “Should I use a Rules Engine?”. Kui tahad propagandat, siis võib vaadata järgmist videot, aga peamine eesmärk on siin tutvuda sellise lähenemisega, kus täidetavad reeglid on süsteemist eraldatud ja andmetena esitatud, näiteks skeemide või tabelite kujul. Martin Fowler soovitab pigem ise reeglimasina kirjutada ja tal on pikem näide, kuidas reeglid ja nende täitmine eraldada.
Reeglid
Reegleid saab defineerida väga paljudel erinevatel viisidel, aga iga reegel koosneb järgmistest osadest:
- nimi: iga reegel peab olema unikaalse nimega, muidu ei tööta!
- kirjeldus: selgitab inimesele, mida see reegel teeb.
- prioriteet: määrab reeglite rakendamise järjekorra: reegel prioriteediga 1 toimub enne kui prioriteediga 2.
- tingimus: reeglit täidetakse, kui tingimus on tõene.
- tegevused: jada käsudest, mida täidetakse (kui reegel rakendub).
Kui reeglid on defineeritud, siis saab neid rakendada "faktidele". See on sisuliselt meie terminoloogias väärtuskeskkond. Reeglid ei saa aga fakte üle kirjutada, kuid faktideks võib olla objekt, mille meetodeid saab kutsuda. Me kasutame siin reeglite defineerimiseks MVEL formaati, kus saab kirjutada data.value = 10
ja seda tõlgendatakse automaatselt kui data.setValue(10)
, ja defineerime reeglid otse Java meetodite abil. Reegleid saab muidugi ka failidest lugeda, aga viimases ülesandes tahame tabeli põhjal reegleid luua, mistõttu on kasulik siin kõik teha Java meetodikutsete abil.
public static void main(String[] args) { Rules discountRules = new Rules(); Rule rule1 = new MVELRule() .name("5% sale") .description("If customer has spent between 200€-1000€, apply 5% discount") .priority(1) .when("200 <= spent && spent < 1000") .then("cashier.discount = 0.05"); Rule rule2 = new MVELRule() .name("10% sale") .description("If customer has spent 1000€ or more, apply 10% discount") .priority(1) .when("1000 <= spent") .then("cashier.discount = 0.1"); Rule rule3 = new MVELRule() .name("compute price") .description("Apply discount to cost") .priority(2) .when("true") .then("cashier.payment = price * (1 - cashier.discount)"); discountRules.register(rule1, rule2, rule3); Facts facts = new Facts(); Cashier cashier = new Cashier(); facts.put("spent", (double) 20); facts.put("price", (double) 100); facts.put("cashier", cashier); RulesEngine rulesEngine = new DefaultRulesEngine(); rulesEngine.fire(discountRules, facts); System.out.println(cashier.getPayment()); }
Easy Rules ülesanded
Repositooriumis on paketis week8.altbusinessrules klass nimega EasyRulesExercise, kus on ka ülalolev näide. Järgmistes ülesannetes tuleb tagastada reegleid ehk Rules
klassi isend, mida me testides siis käivitame.
4. ülesanne (FizzBuzz)
Implementeeri lihtsustatud FizzBuzz meetod. Argumendina antakse arv ja siis tuleks realiseerida järgmisi reegleid.
- Kui arv jagub 3-ga, tagastage "fizz".
- Kui arv jagub 5-ga, tagastage "buzz".
- Kui arv jagub nii 3 kui 5-ga, tagastage "fizzbuzz".
Siin võib oletada, et alati jagub antud arv vähemalt 3 või 5-ga. Väljund tuleb lisada etteantud StringBuilder isendile. Faktideks on seega sisendarv nimega number
ja väljundiks mõeldud StringBuilder isend sb
. Ilus oleks seda lahendada kahe reegliga, kasutades prioriteete järjekorra määramiseks.
5. ülesanne (reeglite loomine tabelist)
Kui vaatasid ülalolevat propagandavideot, siis oli äkki näha, et reegleid võiks esitada tabelina. Näiteks on meie allahindluse skeem üleval esitatav järgmise tabelina:
Nimi | Spent | Discount | |
---|---|---|---|
5% sale | 200€ | 1000€ | 0.05 |
10% sale | 1000€ | - | 0.10 |
Implementeeri meetod, mis teisendab etteantud tabeli EasyRules reeglite komplektiks! Sisendiks on siis tabel vahemikega ja tagastada tuleks reeglid, mis kasutavad neidsamu "spent", "price" ja "cashier" fakte nagu üleval, aga reeglite sisu sõltub tabelist.
Esitada saab siin. Väga teretulnud oleks ka tagasiside kodutöö kohta!