This pattern is used to translate something from one representation into another given the rules.
The benefits come with scalability of the solution. An item can be translated into many
representations using the same mechanism. That is quite convenient. The drawback of the pattern also comes with
the increase of translation mechanisms: maintaining grammar and rules may become tedious.
The pattern implementation requires two things: first a
Context
class that provides
input, the original representation, and a placeholder for output, the end result. The second required thing is
a set of rules, and it can be any class as long as it implements the Interpret
method. The code
example below shows a Context and an abstract class that specifies the Interpret
method.
class Context {
input: string;
output: string;
constructor(input: string) {
this.input = input;
this.output = '';
}
}
abstract class Expression {
Interpret(context: Context): void { }
}
Next, the set of rules, the meat of the pattern. The following implementation converts Oracle
Data Definition Language into C# class code. This program helps convert an existing Oracle database into
a code-first Entity Framework project. So an item like
"STRING_FIELD" VARCHAR2(6 BYTE)
is
converted to public string String_Field {get; set;} = string.Empty;
, and a field declared as, say,
"ID" NUMBER(15,0)
needs to be translated to public int Id {get; set;}
and so on. The
class listed below,
has one method, Interpret
, that takes an item of type
Context
, then consumes that Context's input
field and appends translation to
output
field. Input/output could be properties, but the type Context
in this
case acts as a container, and the OracleToEFExpression
class knows what to do with it. If another
translation is needed, a different class can do that as long as it implements the Expression
interface
and uses input
to provide a useful output
.
class OracleToEFExpression implements Expression {
Interpret(context: Context): void {
let properties = context.input.split('\n');
properties.forEach(element => {
if (element.length > 0) {
let name = this.extractName(element);
let type = this.extractType(element)
let specifiedDefaultValue = this.extractDefaultConstraint(element);
let determinedDefaultValue = this.convertDefaultValue(specifiedDefaultValue, type);
context.output += `public ${type} ${this.convertNameToCamelCase(name)} { get; set; }`;
if (determinedDefaultValue.length > 0)
context.output += ` = ${determinedDefaultValue};`
context.output += '\n'
}
});
console.log(context.output)
}
extractName(input: string): string {
let firstQuote = input.indexOf('"');
let nextQuote = input.indexOf('"', firstQuote + 1)
if (firstQuote >= 0 && nextQuote > firstQuote) {
return input.substring(firstQuote + 1, nextQuote)
}
return 'NO_NAME_FOUND'
}
extractType(input: string): string {
let firstQuote = input.indexOf('"');
let nextQuote = input.indexOf('"', firstQuote + 1)
if (input.indexOf('VARCHAR2') > nextQuote) {
return 'string'
} else if (input.indexOf('NUMBER') > nextQuote) {
return 'int'
} else if (input.indexOf('CHAR') > nextQuote) {
return 'bool'
} else if (input.indexOf('TIMESTAMP') > nextQuote) {
return 'DateTime'
}
return 'int'
}
extractDefaultConstraint(input: string): string {
let defaultConstraintStartsAt = input.indexOf(' DEFAULT ');
if (defaultConstraintStartsAt < 0)
return '';
return input.substring(defaultConstraintStartsAt, input.length);
}
convertDefaultValue(input: string, determinedDataType: string): string {
if (determinedDataType === 'string')
return 'string.Empty'
if (input.trim().length === 0)
return '';
if (determinedDataType === 'int') {
let ret = input.replace('DEFAULT', '');
return ret.substring(0, ret.indexOf(','))
}
if (determinedDataType === 'bool')
return input.indexOf('0') >= 0 ? 'false' : 'true';
return '';
}
convertNameToCamelCase(input: string): string {
let parts = input.split('_');
parts.forEach((p, i) => parts[i] = this.camelCaseString(p));
return parts.join('_');
}
camelCaseString(input: string): string {
return input[0] + input.substring(1, input.length).toLocaleLowerCase()
}
}
The following code is used to call the interpreter mechanism described above:
let c = new Context(`
"ID" NUMBER(15,0),
"EMPLOYEE_CODE" VARCHAR2(6 BYTE),
"COMPANY_ID" NUMBER(15,0),
"FIRST_NAME" VARCHAR2(50 BYTE),
"LAST_NAME" VARCHAR2(50 BYTE),
"IS_ACTIVE" CHAR(1 BYTE) = '1',
"HIRE_DATE" TIMESTAMP (6)`);
let oef = new OracleToEFExpression();
oef.Interpret(c)
And this is what we get back:
public int Id { get; set; }
public string Employee_Code { get; set; } = string.Empty;
public int Company_Id { get; set; }
public string First_Name { get; set; } = string.Empty;
public string Last_Name { get; set; } = string.Empty;
public bool Is_Active { get; set; }
public DateTime Hire_Date { get; set; }
Definitely useful for large conversions.