OO with TS for RV
This small NotFancyAppChallenge was the concept I presented for the new APP. And yes, I took the opportunity to play a little with TS and Jest.
The concepts I wanted to present were:
- Since we have an atomic “report”: To use one object that is passed all around. No new is allowed to modify it. Later on, we will seal it.
- An object-oriented solution to display different visualization to a report.
- All the visualization calculations will be handled only by the relevant visualization object.
- The view layer, which later will be extracted to React, needs to be as thin as possible.
- Visualization is a solid object. If you change one value can/you need to render all the visualization.
Report object
To keep the demo simple, the report object has only two properties: type and data:
class Report { type: ReportType; data: any; }
The report class has one method that returns the current visualization.
get visualization(): Visualization { return new Visualization(); }
I’ll fill the method with actual logic later on.
Report type
The report type is a closed list that presents the different types of visualizations that the report can use. Each type must have his specific type of data.
enum ReportType{ String, Numeric }
Visualization object
The visualization object supposed to have two public methods: one to check the report data according to the current visualization. And the second returns the HTML.
abstract class Visualization{ get isValid(): boolean{ return false; } abstract get html() : string; }
The validation method returns false for the TDD and will be changed later. the HTML method is abstract since each visualization has its own implementation.
Validation object
The validation object’s purpose is to keep track of all the validations we want to run on the visualization. It has three properties:
- description: describes the validation. This is an internal value.
- isValid: stores the validation result.
- errorMessage: this is what we present the user in the case of invalid data.
export class Validations{ description: string; isValid: boolean; errorMessage: string; constructor(description:string, isValid:boolean, errorMEssage:string){ this.isValid = isValid; this.description = description; this.errorMessage = errorMEssage; } }
With the validation object we can get back to the visualization class:
- Validation list that will keep all the validation we ran on the report.
- Constructor that initializes the basic “has data” validation.
- Write the “has data” validation.
- isValid: check all the validation pass.
abstract class Visualization{ report: Report; validations: Array<Validations> = []; constructor(report: Report){ this.report = report; this.validations.push(this.hasDataValidation) } get isValid(): boolean{ return this.validations.every(v => v.isValid===true); } abstract get html() : string; private get hasDataValidation(): Validations { return new Validations( "check if report has data", !!this.report.data, "The report has no data") } }
String visualization
Creating new visualizations are straight forward:
- Define the required validations.
- Create the HTML method.
- “Get” all the HTML calculated value from the report.
class StringVisualization extends Visualization{ constructor(report: Report){ super(report); this.validations.push(this.hasStringData); } get html(): string{ return this.stringValue; } private get stringValue(){ return this.report.data; } private get hasStringData(): Validations{ return new Validations( "Check that data type is string", typeof this.report.data == "string", "The data is not string type") } }
The last piece in the puzzle is to fill the get visualization method on the report class. This time before returning the correct visualization we will check that the report passes all the validation. In case it failed, we will return an error visualization:
class ErrorVisualization extends Visualization{ constructor(report: Report, validations: Array<Validations>){ super(report); this.validations = validations; } get html(){ return `${this.firstError}!`; } private get firstError(): string{ return this.validations.find(v => v.isValid === false).errorMessage; } }
The method passes the validation list into the error visualization so we will be able to display the first validation error on the error visualization.
class Report { ... ... get visualization(): Visualization { const lib = {}; lib[ReportType.String] = () => new StringVisualization(this); lib[ReportType.Numeric] = () => new NumericVisualization(this); let vis: Visualization = lib[this.type](); if(!vis.isValid){ vis = new ErrorVisualization(this, vis.validations); } return vis; } }
I’m not pleased with the solution found for creating the visualization dynamically, but could not find a better one. So don’t hesitate to send some better suggestions.
Goes without saying that this “NotFancyAppChallenge” was wasn’t the fun it had been without something running, and what’s better than Jest unit testing.
describe('Visualisation unit tests', function () { test('Fail the data has validation', () => { const report = new Report(ReportType.String,null); const visualization = report.visualization; expect(visualization.isValid).toBe(false); expect(visualization.html).toBe("The report has no data!"); }); test('Check the string vizualization', () => { const report = new Report(ReportType.String,"string value"); const visualization = report.visualization; expect(visualization.isValid).toBe(true); expect(visualization.html).toBe("string value"); }) test('', () => { const report = new Report(ReportType.Numeric,2); const visualization = report.visualization; expect(visualization.isValid).toBe(true); expect(visualization.html).toBe("2.00"); }) });
Conclusion:
With all the chaos with the Corona virus, there is nothing better than celebrate the small stuff, like eating all 5 meals with the family or having time to write some code.