Introduction
In the fast-evolving landscape of software development, productivity is often the key metric that determines the success of projects. I came from a successful coding environment that was distinctly characterized by high productivity levels. That success could largely be attributed to several crucial decisions made early in the project’s development cycle, which set the stage for efficient practices and processes.
At the heart of this productive environment was the implementation of code generation from a domain model. This utilized an extended form of the UML (Unified Modeling Language) static class model. UML provides a standardized way to visualize and document the design of a system, making it easier to communicate complex ideas and constraints. By generating code directly from such a 'pure' design, many drawbacks of traditional development workflows were avoided.
One of the primary challenges in software development is the oft-repeated violation of the DRY (Don’t Repeat Yourself) principle, which advocates for the minimization of redundancy within code. In many projects, separate aspects such as the model itself, database design, database persistence logic, backend API, and frontend data binding are developed independently - albeit with the model design at the root of all development. This often leads to duplication of effort, inconsistencies, and increased maintenance burdens. Make a change to the underlying model and the effects ripple out through other development artefacts. Generating code from a cohesive and unified domain model, however, ensures all components of the application are aligned with the underlying design, thus preserving the integrity of the data and reducing discrepancies.
In conjunction with this model-driven approach, there was a strategic investment in an associated code framework that facilitated the seamless use of generated code on both the client and server sides. This framework acted as a bridge that connected the generated artifacts to various parts of the application, promoting a more cohesive and maintainable codebase.
For some time now, I have been contemplating the idea of creating comprehensive training materials and code artifacts to share this effective approach with the broader developer community. By providing resources that delineate the intricate process of code generation from domain models, I aim to empower others to enhance their own productivity and adopt similar best practices.
High-level explanation of the workflow
Requirement
The crux of this initiative lies in code generation, a practice that can vastly improve the software development lifecycle by automating repetitive tasks. The goal is to find an open-source tool that offers two essential features:
A Diagrammatic View of a Domain Model: This feature allows developers to create visual representations of their domain, making it easier to understand complex relationships and behavioral structures within the system. Diagrams can serve as both a design tool and a communication artifact, ensuring that all stakeholders have a clear vision of the application's architecture.
A Textual Expression of that Model: Accompanied by a diagrammatic view, a textual representation provides a more formalized and precise means of describing the model. This text serves as the foundation from which the normalization of the model will be generated, creating a structured version that can be methodically translated into code.
The Modelling Tool
There are a number of free, open-source tools available that support this requirement. I have settled on mermaid.js. To quote the mermaid introduction page:
Mermaid lets you create diagrams and visualizations using text and code.
It is a JavaScript based diagramming and charting tool that renders Markdown-inspired text definitions to create and modify diagrams dynamically.
The Model
It would be possible to run generation directly against the text/model produced by the modelling tool but that would bind your process to that tool. Far better to identify a model that suits generation and transform the modelling tool’s representation into this model.
So, the next step involves using the textual representation of the domain model to create a normalized version, which will act as the backbone for various code-generation activities. You will never have to change your generation logic for potentially multiple code outputs, should the tool change in some way or (worst case) you have to switch tools. It will only be your model generation code that needs to change. This provides flexibility and isolates you from change.
In order to generate all types of (crud api, db persistence, form and control binding in the UI) code it will be necessary to create an intermediate model of the domain that:
Is easy to access and use in code;
Is human readable (not essential but useful);
Contains the information necessary to support generation of code artefacts in many languages;
As we shall see this proves to be fairly straightforward
Code Generation
I aim to generate code across several areas of what is called Full Stack Development:
For my Language(s) of Choice: Whether it be Java, Python, or JavaScript, the generated code should be tailored to the syntax and idioms of the chosen programming languages, allowing developers to leverage their existing skills while adhering to best practices.
For Database Generation: This includes creating the necessary schemas and table structures directly from the domain model, ensuring that the data layer is accurately aligned with the business logic of the application.
For Database Persistence: The persistence layer is crucial for storing and retrieving data consistently. By automating the generation of persistence code, developers can focus on more strategic tasks, knowing that database interactions will be handled efficiently.
For Back-End Persistence and API: The backend API serves as the conduit through which clients interact with the server. Generating this code from the domain model not only streamlines the development process but also adds coherence to how data is exposed to external consumers.
For Front-End Services: Finally, the generated front-end services will be designed to consume the backend API effectively. This simplifies the task of presenting data to users, enabling convenient updates and data maintenance from a user-friendly interface.
Understanding UML Static Class Diagrams
Basics of UML class diagrams
UML class diagrams use some fairly simple diagrammatic artefacts to describe business objects, their attributes and the associations between objects such as:
- Is a type of
- Has one
- Has many
- Is composed of
Tools like Sparx Enterprise Architect store this information in an internal bespoke format and the user maintains a diagrammatic version that visualises the stored data. Under the covers there is a data based description of the model.
Attributes
These are the atomic values of the business object such as name, price, date of birth, etc. Attributes have a data type such as string, number, date, boolean. Particular data types can themselves be constrained - for example, a string might have a minimum or maximum length; a number might be an integer or a decimal with a set number of significant figures.
Classes
These are the ‘containers’ for a set of attributes that represent particular business entities. A Person class might have attributes of name, dateofbirth (with obvious types). Generally (in a diagram) classes are represented by a box or rectangle with the attributes laid out within the box.
Following is a Mermaid Static Class Diagram and after that the text representation used to generate the diagram
classDiagram
note for PersistableBase "inheritance=propagateattributes,namespace=persistance"
class PersistableBase {
+int id
+bool active
}
<<Abstract>> PersistableBase
note for TimeLimitedPersistableBase "inheritance=propagateattributes,namespace=persistance"
class TimeLimitedPersistableBase {
+datetime effFrom
+datetime effTo
}
<<Abstract>> TimeLimitedPersistableBase
TimeLimitedPersistableBase --|> PersistableBase
note for ReferenceBase "inheritance=propagateattributes,namespace=persistance"
class ReferenceBase {
+string:50 typeShortDescription
+string:150 typeLongDescription
+string:10 code
}
<<Abstract>> ReferenceBase
TimeLimitedPersistableBase <|-- ReferenceBase
note for Person "inheritance=rollup,namespace=person"
class Person {
+string:100 givenNames
+string:100 lastName
+date dob
}
PersistableBase <|-- Person
note for Employee "inheritance=none,namespace=person"
class Employee {
+string:150 department
+date: startDate
}
Person <|-- Employee
note for Address "inheritance=none,namespace=person"
class Address {
+string:100 addressLine1
+string:100 addressLine2
+string:100 addressLine3
+string:100 suburb
+string:10 postcode
+string:10 state
}
PersistableBase <|-- Address
note for Contact "inheritance=none,namespace=person"
class Contact {
+string:150 details
}
PersistableBase <|-- Contact
Person "0" --> "*" Contact
Person "0" --> "*" Address
note for GenderType "inheritance=none,namespace=referencedata"
class GenderType
ReferenceBase <|-- GenderType
Person "*" --> "1" GenderType
note for AddressType "inheritance=none,namespace=referencedata"
class AddressType
ReferenceBase <|-- AddressType
Address "*" --> "1" AddressType
note for ContactType "inheritance=none,namespace=referencedata"
class ContactType
ReferenceBase <|-- ContactType
Contact "*" --> "1" ContactType
Class definitions
From the above example:
note for PersistableBase "inheritance=propagateattributes,namespace=persistance"
class PersistableBase {
+int id
+bool active
}
<<Abstract>> PersistableBase
Here is a definition for the abstract class PersistableBase. Points to note:
The class name definition;
The { } body containing the attributes with the + indicating a public attribute, the type (int and bool in this case) and the name of the attribute (id and active).
The <> decoration on the class name.
Use of the note syntax to introduce a property bag of information needed to support my brand of code generation (the way that inheritance is achieved and the namespace for my class)
So the next step is to convert this mermaid/text model into a json meta model that will then be the basis of code generation. The structure (type definition) for this is as follows:
export type Class = {
name: string;
inheritance?: Inheritance;
namespace?: string;
parent?: Class;
attributes: Attribute[];
isAbstract: boolean;
};
export type Attribute = {
name: string;
type: string;
length?: number;
precision?: number;
visibility: Visibility;
};
export type Association = {
name?: string;
source: Endpoint;
target: Endpoint;
};
export type Endpoint = {
multiplicity: "0" | "1" | "*" | "n";
role?: string;
fkWinner?: boolean;
class: Class;
navagability: boolean;
};
export enum Visibility {
Public = "public",
Private = "private",
Protected = "protected",
}
export enum Inheritance {
rollup = "rollup",
propagateattributes = "propagateattributes",
none = "none",
}
export type Model = {
modeldate: Date;
classes: Class[];
associations: Association[];
};
This is a fairly simple model but at the time sufficiently expressive to allow for all of the generation I need to do. Interestingly, this model can also be expressed using Mermaid - so an exercise for the future might be to create a crud interface and ui client to maintain models!
Top comments (0)