R Language
Implementeer State Machine Pattern met behulp van S4 Class
Zoeken…
Invoering
Eindige toestanden Machineconcepten worden meestal geïmplementeerd onder OOP-talen (Object Oriented Programming), bijvoorbeeld met behulp van de Java-taal, gebaseerd op het statuspatroon dat is gedefinieerd in GOF (verwijst naar het boek: "Design Patterns").
R biedt verschillende mechanismen om het OO-paradigma te simuleren, laten we S4 Object System toepassen voor de implementatie van dit patroon.
Lijnen parseren met behulp van State Machine
Laten we het State Machine-patroon toepassen voor het parseren van lijnen met het specifieke patroon met behulp van de S4 Class-functie van R.
PROBLEEM-UITBREIDING
We moeten een bestand parseren waarin elke regel informatie over een persoon geeft, met behulp van een scheidingsteken ( ";"
), maar sommige verstrekte informatie is optioneel en ontbreekt in plaats van een leeg veld. Op elke regel kunnen we de volgende informatie hebben: Name;[Address;]Phone
. Waar de adresinformatie optioneel is, hebben we die soms en soms niet, bijvoorbeeld:
GREGORY BROWN; 25 NE 25TH; +1-786-987-6543
DAVID SMITH;786-123-4567
ALAN PEREZ; 25 SE 50TH; +1-786-987-5553
De tweede regel geeft geen adresinformatie. Daarom kan het aantal scheidingstekens afhankelijk zijn zoals in dit geval met één scheidingsteken en voor de andere lijnen twee scheidingstekens. Omdat het aantal scheidingstekens kan variëren, is een manier om dit probleem aan te pakken het herkennen van de aanwezigheid van een bepaald veld op basis van het patroon. In dat geval kunnen we een reguliere expressie gebruiken om dergelijke patronen te identificeren. Bijvoorbeeld:
- Naam :
"^([AZ]'?\\s+)* *[AZ]+(\\s+[AZ]{1,2}\\.?,? +)*[AZ]+((-|\\s+)[AZ]+)*$"
. Bijvoorbeeld:RAFAEL REAL, DAVID R. SMITH, ERNESTO PEREZ GONZALEZ, 0' CONNOR BROWN, LUIS PEREZ-MENA
, enz. - Adres :
"^\\s[0-9]{1,4}(\\s+[AZ]{1,2}[0-9]{1,2}[AZ]{1,2}|[AZ\\s0-9]+)$"
. Bijvoorbeeld:11020 LE JEUNE ROAD
,87 SW 27TH
. Omwille van de eenvoud nemen we hier niet de postcode, stad, staat op, maar ik kan in dit veld worden opgenomen of extra velden toevoegen. - Telefoon :
"^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$"
. Bijvoorbeeld:305-123-4567, 305 123 4567, +1-786-123-4567
.
Opmerkingen :
- Ik overweeg het meest voorkomende patroon van Amerikaanse adressen en telefoons, het kan gemakkelijk worden uitgebreid om meer algemene situaties te overwegen.
- In R heeft het teken
"\"
een speciale betekenis voor karaktervariabelen, daarom moeten we eraan ontsnappen. - Om het proces van het definiëren van reguliere expressies te vereenvoudigen, is het een goede aanbeveling om de volgende webpagina te gebruiken: regex101.com , zodat u ermee kunt spelen, met een bepaald voorbeeld, totdat u het verwachte resultaat voor alle mogelijke combinaties krijgt.
Het idee is om elk lijnveld te identificeren op basis van eerder gedefinieerde patronen. Het statuspatroon definieert de volgende entiteiten (klassen) die samenwerken om het specifieke gedrag te beheersen (het statuspatroon is een gedragspatroon):
Laten we elk element beschrijven, rekening houdend met de context van ons probleem:
-
Context
: Slaat de contextinformatie van het parseerproces op, dwz de huidige status en verzorgt het gehele statusmachine-proces. Voor elke status wordt een actie uitgevoerd (handle()
), maar de context delegeert deze, op basis van de status, op de actiemethode die is gedefinieerd voor een bepaalde status (handle()
uitState
klasseState
). Het definieert de interface die interessant is voor klanten. OnzeContext
kan als volgt worden gedefinieerd:- Attributen:
state
- Methoden:
handle()
, ...
- Attributen:
-
State
: de abstracte klasse die elke staat van de staatsmachine vertegenwoordigt. Het definieert een interface voor het inkapselen van het gedrag geassocieerd met een bepaalde staat van de context. Het kan als volgt worden gedefinieerd:- Attributen:
name, pattern
- Methoden:
doAction()
,isState
(controleer met behulp van hetpattern
of hetisState
al dan niet tot ditisState
behoort), ...
- Attributen:
-
Concrete States
(subklassen van deState
): elke subklasse van de klasseState
die een gedrag implementeert dat verband houdt met een toestand van deContext
. Onze subklassen zijn:InitState
,NameState
,AddressState
,PhoneState
. Dergelijke klassen implementeren gewoon de generieke methode met behulp van de specifieke logica voor dergelijke toestanden. Er zijn geen aanvullende attributen vereist.
Opmerking: het is een kwestie van voorkeur hoe de methode wordt genoemd die de actie uitvoert, handle()
, doAction()
of goNext()
. De methode naam doAction()
kan hetzelfde zijn voor beide klassen ( State
of Context
) die we liever als handle()
in de klasse Context
noemen handle()
verwarring te voorkomen bij het definiëren van twee generieke methoden met dezelfde invoerargumenten, maar met verschillende klassen.
PERSOONLIJKE KLASSE
Met behulp van de S4-syntaxis kunnen we een Person-klasse als volgt definiëren:
setClass(Class = "Person",
slots = c(name = "character", address = "character", phone = "character")
)
Het is een goede aanbeveling om de klassenkenmerken te initialiseren. De documentatie van setClass
suggereert het gebruik van een generieke methode gelabeld als "initialize"
, in plaats van het gebruik van verouderde attributen zoals: prototype, representation
.
setMethod("initialize", "Person",
definition = function(.Object, name = NA_character_,
address = NA_character_, phone = NA_character_) {
.Object@name <- name
.Object@address <- address
.Object@phone <- phone
.Object
}
)
Omdat de initialisatiemethode al een standaard generieke methode voor methods
, moeten we de oorspronkelijke argumentdefinitie respecteren. We kunnen controleren of het op de R-prompt typt:
> initialize
Het geeft de volledige functiedefinitie terug, u kunt bovenaan zien wie de functie is gedefinieerd als:
function (.Object, ...) {...}
Daarom moeten we bij het gebruik van setMethod
exact dezelfde syntaxis ( .Object
) volgen.
Een andere bestaande generieke methode is show
, deze is equivalent aan de toString()
van Java en het is een goed idee om een specifieke implementatie voor klassedomein te hebben:
setMethod("show", signature = "Person",
definition = function(object) {
info <- sprintf("%s@[name='%s', address='%s', phone='%s']",
class(object), object@name, object@address, object@phone)
cat(info)
invisible(NULL)
}
)
Opmerking : we gebruiken dezelfde conventie als in de standaard Java-implementatie toString()
.
Laten we zeggen dat we de ontlede informatie (een lijst met Person
objecten) in een dataset willen opslaan, dan moeten we eerst een lijst met objecten kunnen omzetten in iets dat de R kan transformeren (bijvoorbeeld het object als een lijst dwingen). We kunnen de volgende aanvullende methode definiëren (zie het bericht voor meer informatie hierover)
setGeneric(name = "as.list", signature = c('x'),
def = function(x) standardGeneric("as.list"))
# Suggestion taken from here:
# http://stackoverflow.com/questions/30386009/how-to-extend-as-list-in-a-canonical-way-to-s4-objects
setMethod("as.list", signature = "Person",
definition = function(x) {
mapply(function(y) {
#apply as.list if the slot is again an user-defined object
#therefore, as.list gets applied recursively
if (inherits(slot(x,y),"Person")) {
as.list(slot(x,y))
} else {
#otherwise just return the slot
slot(x,y)
}
},
slotNames(class(x)),
SIMPLIFY=FALSE)
}
)
R biedt geen suikersyntaxis voor OO omdat de taal aanvankelijk was bedoeld om waardevolle functies voor statistici te bieden. Daarom vereist elke gebruikersmethode twee delen: 1) het Definitiedeel (via setGeneric
) en 2) het implementatiedeel (via setMethod
). Zoals in het bovenstaande voorbeeld.
STAATSKLASSE
Laten we na de S4-syntaxis de abstracte klasse State
definiëren.
setClass(Class = "State", slots = c(name = "character", pattern = "character"))
setMethod("initialize", "State",
definition = function(.Object, name = NA_character_, pattern = NA_character_) {
.Object@name <- name
.Object@pattern <- pattern
.Object
}
)
setMethod("show", signature = "State",
definition = function(object) {
info <- sprintf("%s@[name='%s', pattern='%s']", class(object),
object@name, object@pattern)
cat(info)
invisible(NULL)
}
)
setGeneric(name = "isState", signature = c('obj', 'input'),
def = function(obj, input) standardGeneric("isState"))
setGeneric(name = "doAction", signature = c('obj', 'input', 'context'),
def = function(obj, input, context) standardGeneric("doAction"))
Elke subklasse van State
heeft een name
en pattern
gekoppeld, maar ook een manier om te bepalen of een bepaalde invoer tot deze status behoort of niet ( isState()
-methode), en ook de bijbehorende acties voor deze status te doAction()
methode).
Om het proces te begrijpen, laten we de overgangsmatrix voor elke status definiëren op basis van de ontvangen invoer:
Ingang / Huidige status | In het | Naam | Adres | Telefoon |
---|---|---|---|---|
Naam | Naam | |||
Adres | Adres | |||
Telefoon | Telefoon | Telefoon | ||
Einde | Einde |
Opmerking: De cel [row, col]=[i,j]
vertegenwoordigt de bestemmingstoestand voor de huidige status j
, wanneer deze de invoer i
ontvangt.
Het betekent dat het onder de status Naam twee ingangen kan ontvangen: een adres of een telefoonnummer. Een andere manier om de transactietabel weer te geven, is met behulp van het volgende UML State Machine- diagram:
Laten we elke specifieke staat implementeren als een substaat van de klasse State
SUBKLASSEN VAN DE STAAT
Oorspronkelijke staat :
De initiële status wordt geïmplementeerd via de volgende klasse:
setClass("InitState", contains = "State")
setMethod("initialize", "InitState",
definition = function(.Object, name = "init", pattern = NA_character_) {
.Object@name <- name
.Object@pattern <- pattern
.Object
}
)
setMethod("show", signature = "InitState",
definition = function(object) {
callNextMethod()
}
)
In R om aan te geven dat een klasse een subklasse is van een andere klasse, gebruikt het attribuut contains
en geeft de klassenaam van de bovenliggende klasse aan.
Omdat de subklassen gewoon de generieke methoden implementeren, zonder extra attributen toe te voegen, dan de show
methode, roept u gewoon de equivalente methode uit de hogere klasse aan (via methode: callNextMethod()
)
Aan de begintoestand is geen patroon gekoppeld, het vertegenwoordigt slechts het begin van het proces, daarna initialiseren we de klasse met een NA
waarde.
Laten we nu de generieke methoden uit de klasse State
implementeren:
setMethod(f = "isState", signature = "InitState",
definition = function(obj, input) {
nameState <- new("NameState")
result <- isState(nameState, input)
return(result)
}
)
Voor deze specifieke status (zonder pattern
), zal het idee dat het parseerproces gewoon wordt geïnitieerd en het eerste veld verwacht, een name
, anders zal het een fout zijn.
setMethod(f = "doAction", signature = "InitState",
definition = function(obj, input, context) {
nameState <- new("NameState")
if (isState(nameState, input)) {
person <- context@person
person@name <- trimws(input)
context@person <- person
context@state <- nameState
} else {
msg <- sprintf("The input argument: '%s' cannot be identified", input)
stop(msg)
}
return(context)
}
)
De doAction
methode zorgt voor de overgang en werkt de context bij met de geëxtraheerde informatie. Hier hebben we toegang tot contextinformatie via de @-operator
. In plaats daarvan kunnen we get/set
methoden definiëren get/set
dit proces in te kapselen (zoals voorgeschreven in OO best practices: inkapseling), maar dat zou nog vier methoden per get-set
toevoegen zonder waarde toe te voegen voor het doel van dit voorbeeld.
Het is een goede aanbeveling bij alle doAction
implementatie om een beveiliging toe te voegen wanneer het doAction
niet correct is geïdentificeerd.
Naam staat
Hier is de definitie van deze klassedefinitie:
setClass ("NameState", contains = "State")
setMethod("initialize","NameState",
definition=function(.Object, name="name",
pattern = "^([A-Z]'?\\s+)* *[A-Z]+(\\s+[A-Z]{1,2}\\.?,? +)*[A-Z]+((-|\\s+)[A-Z]+)*$") {
.Object@pattern <- pattern
.Object@name <- name
.Object
}
)
setMethod("show", signature = "NameState",
definition = function(object) {
callNextMethod()
}
)
We gebruiken de functie grepl
om te controleren of de invoer bij een bepaald patroon hoort.
setMethod(f="isState", signature="NameState",
definition=function(obj, input) {
result <- grepl(obj@pattern, input, perl=TRUE)
return(result)
}
)
Nu definiëren we de actie die moet worden uitgevoerd voor een bepaalde staat:
setMethod(f = "doAction", signature = "NameState",
definition=function(obj, input, context) {
addressState <- new("AddressState")
phoneState <- new("PhoneState")
person <- context@person
if (isState(addressState, input)) {
person@address <- trimws(input)
context@person <- person
context@state <- addressState
} else if (isState(phoneState, input)) {
person@phone <- trimws(input)
context@person <- person
context@state <- phoneState
} else {
msg <- sprintf("The input argument: '%s' cannot be identified", input)
stop(msg)
}
return(context)
}
)
Hier houden we rekening met mogelijke overgangen: één voor adresstatus en de andere voor telefoonstatus. In alle gevallen werken we de contextinformatie bij:
- De
person
:address
ofphone
met het invoerargument. - De
state
van het proces
De manier om de status te identificeren is door de methode: isState()
voor een bepaalde status aan te roepen. We maken een standaardspecifieke addressState, phoneState
( addressState, phoneState
) en vragen vervolgens om een bepaalde validatie.
De logica voor de implementatie van de andere subklassen (één per staat) lijkt erg op elkaar.
Adres staat
setClass("AddressState", contains = "State")
setMethod("initialize", "AddressState",
definition = function(.Object, name="address",
pattern = "^\\s[0-9]{1,4}(\\s+[A-Z]{1,2}[0-9]{1,2}[A-Z]{1,2}|[A-Z\\s0-9]+)$") {
.Object@pattern <- pattern
.Object@name <- name
.Object
}
)
setMethod("show", signature = "AddressState",
definition = function(object) {
callNextMethod()
}
)
setMethod(f="isState", signature="AddressState",
definition=function(obj, input) {
result <- grepl(obj@pattern, input, perl=TRUE)
return(result)
}
)
setMethod(f = "doAction", "AddressState",
definition=function(obj, input, context) {
phoneState <- new("PhoneState")
if (isState(phoneState, input)) {
person <- context@person
person@phone <- trimws(input)
context@person <- person
context@state <- phoneState
} else {
msg <- sprintf("The input argument: '%s' cannot be identified", input)
stop(msg)
}
return(context)
}
)
Telefoon staat
setClass("PhoneState", contains = "State")
setMethod("initialize", "PhoneState",
definition = function(.Object, name = "phone",
pattern = "^\\s*(\\+1(-|\\s+))*[0-9]{3}(-|\\s+)[0-9]{3}(-|\\s+)[0-9]{4}$") {
.Object@pattern <- pattern
.Object@name <- name
.Object
}
)
setMethod("show", signature = "PhoneState",
definition = function(object) {
callNextMethod()
}
)
setMethod(f = "isState", signature = "PhoneState",
definition = function(obj, input) {
result <- grepl(obj@pattern, input, perl = TRUE)
return(result)
}
)
Hier voegen we de persoonsinformatie toe aan de lijst met persons
uit de context
.
setMethod(f = "doAction", "PhoneState",
definition = function(obj, input, context) {
context <- addPerson(context, context@person)
context@state <- new("InitState")
return(context)
}
)
CONTEXT KLASSE
Nu is het verhuurt aan de uit te leggen Context
class-implementatie. We kunnen het definiëren rekening houdend met de volgende kenmerken:
setClass(Class = "Context",
slots = c(state = "State", persons = "list", person = "Person")
)
Waar
-
state
: de huidige status van het proces -
person
: de huidige persoon, het vertegenwoordigt de informatie die we al hebben ontleed uit de huidige regel. -
persons
: de lijst met verwerkte geparseerde personen.
Opmerking : Optioneel kunnen we een name
toevoegen om de context op name
te identificeren als we met meer dan één parsertype werken.
setMethod(f="initialize", signature="Context",
definition = function(.Object) {
.Object@state <- new("InitState")
.Object@persons <- list()
.Object@person <- new("Person")
return(.Object)
}
)
setMethod("show", signature = "Context",
definition = function(object) {
cat("An object of class ", class(object), "\n", sep = "")
info <- sprintf("[state='%s', persons='%s', person='%s']", object@state,
toString(object@persons), object@person)
cat(info)
invisible(NULL)
}
)
setGeneric(name = "handle", signature = c('obj', 'input', 'context'),
def = function(obj, input, context) standardGeneric("handle"))
setGeneric(name = "addPerson", signature = c('obj', 'person'),
def = function(obj, person) standardGeneric("addPerson"))
setGeneric(name = "parseLine", signature = c('obj', 's'),
def = function(obj, s) standardGeneric("parseLine"))
setGeneric(name = "parseLines", signature = c('obj', 's'),
def = function(obj, s) standardGeneric("parseLines"))
setGeneric(name = "as.df", signature = c('obj'),
def = function(obj) standardGeneric("as.df"))
Met dergelijke generieke methoden beheersen we het volledige gedrag van het parsproces:
-
handle()
: roept de specifiekedoAction()
-methode van de huidigestate
. -
addPerson
: Zodra we de eindstatus hebben bereikt, moeten we eenperson
aan de lijst metpersons
we hebben ontleed. -
parseLine()
: Parseer een enkele regel -
parseLines()
: meerdere regelsparseLines()
een reeks regels) -
as.df()
: extraheer de informatie uit depersons
in een dataframe-object.
Laten we nu verder gaan met de bijbehorende implementaties:
handle()
methode, delegeert op doAction()
methode uit de huidige state
van de context
:
setMethod(f = "handle", signature = "Context",
definition = function(obj, input) {
obj <- doAction(obj@state, input, obj)
return(obj)
}
)
setMethod(f = "addPerson", signature = "Context",
definition = function(obj, person) {
obj@persons <- c(obj@persons, person)
return(obj)
}
)
Eerst splitsen we de oorspronkelijke regel in een array met behulp van het scheidingsteken om elk element te identificeren via de R-functie strsplit()
, en itereren vervolgens voor elk element als een invoerwaarde voor een bepaalde status. De methode handle()
retourneert opnieuw de context
met de bijgewerkte informatie ( state
, person
, kenmerk persons
).
setMethod(f = "parseLine", signature = "Context",
definition = function(obj, s) {
elements <- strsplit(s, ";")[[1]]
# Adding an empty field for considering the end state.
elements <- c(elements, "")
n <- length(elements)
input <- NULL
for (i in (1:n)) {
input <- elements[i]
obj <- handle(obj, input)
}
return(obj@person)
}
)
Omdat R een kopie van het invoerargument maakt, moeten we de context teruggeven ( obj
):
setMethod(f = "parseLines", signature = "Context",
definition = function(obj, s) {
n <- length(s)
listOfPersons <- list()
for (i in (1:n)) {
ipersons <- parseLine(obj, s[i])
listOfPersons[[i]] <- ipersons
}
obj@persons <- listOfPersons
return(obj)
}
)
Het kenmerk persons
is een lijst met exemplaren van de klasse S4 Person
. Dit iets kan niet worden gedwongen tot een standaardtype omdat R niet weet of een instantie van een door de gebruiker gedefinieerde klasse moet worden behandeld. De oplossing is om een Person
om te zetten in een lijst, met behulp van de eerder gedefinieerde as.list
methode. Dan kunnen we deze functie toepassen op elk element van de lijst van persons
, via de lapply()
functie. Vervolgens past in de data.frame
functie voor het aanroepen van lappy()
functie data.frame
voor het converteren van elk element van de persons.list
in een gegevensframe. Ten slotte wordt de functie rbind()
aangeroepen voor het toevoegen van elk geconverteerd element als een nieuwe rij van het gegenereerde dataframe (zie voor meer informatie hierover dit bericht )
# Sugestion taken from this post:
# http://stackoverflow.com/questions/4227223/r-list-to-data-frame
setMethod(f = "as.df", signature = "Context",
definition = function(obj) {
persons <- obj@persons
persons.list <- lapply(persons, as.list)
persons.ds <- do.call(rbind, lapply(persons.list, data.frame, stringsAsFactors = FALSE))
return(persons.ds)
}
)
SAMEN ZETTEN
Laten we ten slotte de hele oplossing testen. Definieer de te parseren lijnen waar voor de tweede regel de adresinformatie ontbreekt.
s <- c(
"GREGORY BROWN; 25 NE 25TH; +1-786-987-6543",
"DAVID SMITH;786-123-4567",
"ALAN PEREZ; 25 SE 50TH; +1-786-987-5553"
)
Nu initialiseren we de context
en ontleden we de lijnen:
context <- new("Context")
context <- parseLines(context, s)
Eindelijk de bijbehorende dataset verkrijgen en afdrukken:
df <- as.df(context)
> df
name address phone
1 GREGORY BROWN 25 NE 25TH +1-786-987-6543
2 DAVID SMITH <NA> 786-123-4567
3 ALAN PEREZ 25 SE 50TH +1-786-987-5553
Laten we nu de show
testen:
> show(context@persons[[1]])
Person@[name='GREGORY BROWN', address='25 NE 25TH', phone='+1-786-987-6543']
En voor een deelstaat:
>show(new("PhoneState"))
PhoneState@[name='phone', pattern='^\s*(\+1(-|\s+))*[0-9]{3}(-|\s+)[0-9]{3}(-|\s+)[0-9]{4}$']
Test ten slotte de methode as.list()
:
> as.list(context@persons[[1]])
$name
[1] "GREGORY BROWN"
$address
[1] "25 NE 25TH"
$phone
[1] "+1-786-987-6543"
>
CONCLUSIE
Dit voorbeeld laat zien hoe het statuspatroon moet worden geïmplementeerd, met behulp van een van de beschikbare mechanismen van R voor het gebruik van het OO-paradigma. Desondanks is de R OO-oplossing niet gebruiksvriendelijk en verschilt hij zoveel van andere OOP-talen. U moet uw mindset veranderen, omdat de syntaxis totaal anders is, het herinnert meer aan het functionele programmeerparadigma. Bijvoorbeeld in plaats van: object.setID("A1")
zoals in Java / C #, voor R moet u de methode op deze manier setID(object, "A1")
: setID(object, "A1")
. Daarom moet u het object altijd opnemen als invoerargument om de context van de functie te bieden. Op dezelfde manier is er geen speciaal kenmerk van this
klasse en ook geen "."
notatie voor toegang tot methoden of attributen van de gegeven klasse. Het is meer foutprompt omdat het verwijzen naar een klasse of methoden gebeurt via kenmerkwaarde ( "Person"
, "isState"
, etc.).
De bovengenoemde oplossing uit de S4-klasse vereist veel meer coderegels dan traditionele Java / C # -talen voor het uitvoeren van eenvoudige taken. Hoe dan ook, het staatspatroon is een goede en generieke oplossing voor dergelijke problemen. Het vereenvoudigt het proces waarbij de logica in een bepaalde staat wordt gedelegeerd. In plaats van het hebben van een grote if-else
blok voor het regelen van alle situaties, we hebben kleinere if-else
blokken binnen op iedere State
sub-klasse implementatie voor de uitvoering van de actie om in elke staat uit te voeren.
Bijlage : hier kunt u het volledige script downloaden.
Elke suggestie is welkom.