Model Driven Architecture and Code Generation
I have been using Model Driven Architecture and code generation for some time. In this short blog post I have tried to summarise some of the lessons I have found useful. I have included an example using Sparx EA. Happy to share the code but I haven't yet been bothered to package it up!
1. Code Generation - Use Appropriately
Code generation is old hat of course ... for a long time designers have wanted (and often succeeded) to transform their detailed architectural models into physical implementations. The aim of model driven code generation is increasing productivity and to better document or communicate the details of the system in question.
The danger is you end up really just creating ... well ... 1970's flow diagrams, large pictures of program logic. Time consuming to produce and giving no additional insight; much easier to code in a good productive modern language.
What I am saying is that generating Java EJB (for example) code from a hand crafted UML model of all the required J2EE classes is just pointless. What you need to do is transform a simpler logical "technology independent" model into implementation "physical" models.
2. Conceptual is not the same as Technology Neutral
Another mistake is to try to automatically transform conceptual models into models that can be the target for code generation. Conceptual is not the same as technology neutral, a conceptual model is designed to help develop and communicate how a system works. To aid understanding unnecessary and confusing details will have been missed out, and relationships between components will be "idealised" ... lies designed to elucidate if you like. On the other hand a technology neutral model needs to be exact if it is going to be the root source for code generation.
Conceptual models have a purpose, don't ruin them ...
3. Keep Generated Code Simple and Readable
Generated code is hard to debug and fix. Changing code involved changing your code generator; the code generator will inevitably become complex and ugly. Tracking causes between the generated code, input model and the generator logic will be frustrating ... especially if it has been sometime since you have looked in detail at the generator logic.
Some users of your generated code may not have access to the generator (or more to the point may NOT want access to it). Of course changing the code manually is a mistake, but nethertheless they should be able to understand the code. In a way it should look like manually generated code.
Generated code should be made to be simple and clean ... use object oriented encapsulation (not quite the right word but you know what I mean) or libraries to separate the handcrafted, involved and complicated plumbing from the automatically generated simple if repetitive and large code base.
Example - Google App Engine Datastore
My UML tool of choice is Sparx EA, and this has good MDA capabilities:
- A two stage generating methodology. First to transform from a technology neutral model to technology specific model, secondly to generate code (Java, C#, SQL DDL etc) from the corresponding technology specific model.
- A common templating engine for transformations and code generation.
- A simple Domain Specific Language (DSL) to describe the model to be generated (this is a really important but almost hidden Sparx EA capability).
- Out of the box templates for transformation and generation - both working and as accelerators to create your own.
I suggest you start with this white paper for more details.
Now Google App Engine (GAE) has a database system - Datastore - quite simple to use but inevitably you will end up using or rolling a framework. I rolled my own and for each entity I need to create a number of simple classes each extending complicated base classes:
- A Controller to CRUDL entities
- A Factory to get the Controller
- An Entity interface
- A Server implementation designed to be used on the server side (wraps the Datastore Entity)
- A Client implementation design to be used on the client side. I use it with Google Web Toolkit (GWT). This is a Plain Old Java Object.
- I also need to generate GWT RMI Classes.
This is a typical requirement for most frameworks (and I much prefer build time creation rather than SLOW runtime introspection etc.) This would be very boring to handcraft ... so I created a MDA transformation to a Java physical model. Then the actual Java code generation was done by the out-of-the-box Sparx EA templates.
Screenshots ... feel free to ask for more details.
This is an example of a logical model
This is a bit of the generated Java model just showing the client/shared classes
And this is some generated code examples - boring code to write manually ...
package xxx.generated.shared; import org.architectureportal.gaeaddons.shared.BaseEntityClient; /** * Patient Core Details * @author Adrian Sutherland * @version 1.0 * @created 25-Mar-2014 18:30:06 */ public class BasePatientEntityClient extends BaseEntityClient implements BasePatientEntity { /** * Postal Address */ private String address; /** * Allergies */ private String allergies; /** * Alternative Contact Details */ private String alternativeContact; /** * Conditions */ private String conditions; /** * Date of Birth */ private java.util.Date dob; /** * Email Address */ private String email; /** * Gender */ private GenderType gender = GenderType.Unknown; /** * Mobile Number */ private String mobile; /** * Patient Name */ private String name; /** * NHS Number */ private String nhsNmber; /** * Phone Number */ private String phone; /** * Special Residence Instructions */ private String residenceInstructions; private static final long serialVersionUID = 1; /** * Special Instructions and Advice */ private String specialNotes; /** * Brief Summary History */ private String summaryHistory; /** * Constructor */ public BasePatientEntityClient(){ super(); addSubtype(BasePatientEntity.SUBTYPE); } /** * Copy Constructor * * @param source */ public BasePatientEntityClient(BasePatientEntity source){ super(source); address = source.getAddress(); allergies = source.getAllergies(); alternativeContact = source.getAlternativeContact(); conditions = source.getConditions(); dob = source.getDob(); email = source.getEmail(); gender = source.getGender(); mobile = source.getMobile(); name = source.getName(); nhsNmber = source.getNhsNmber(); phone = source.getPhone(); residenceInstructions = source.getResidenceInstructions(); specialNotes = source.getSpecialNotes(); summaryHistory = source.getSummaryHistory(); } /** * Getter for Postal Address */ @Override public String getAddress(){ return address; } /** * Getter for Allergies */ @Override public String getAllergies(){ return allergies; } /** * Getter for Alternative Contact Details */ @Override public String getAlternativeContact(){ return alternativeContact; } /** * Getter for Conditions */ @Override public String getConditions(){ return conditions; } /** * Getter for Date of Birth */ @Override public java.util.Date getDob(){ return dob; } /** * Getter for Email Address */ @Override public String getEmail(){ return email; } /** * Get the description of this BasePatient instance */ @Override public String getEntityDescription(){ return getName() + " (" + formatDate(getDob()) + ")"; } /** * Get the description of the BasePatient type */ @Override public String getEntityTypeDescription(){ return BasePatientEntity.ENTITYDESC; } /** * Getter for Gender */ @Override public GenderType getGender(){ return gender; } /** * Getter for Mobile Number */ @Override public String getMobile(){ return mobile; } /** * Getter for Patient Name */ @Override public String getName(){ return name; } /** * Getter for NHS Number */ @Override public String getNhsNmber(){ return nhsNmber; } /** * Getter for Phone Number */ @Override public String getPhone(){ return phone; } /** * Getter for Special Residence Instructions */ @Override public String getResidenceInstructions(){ return residenceInstructions; } /** * Getter for Special Instructions and Advice */ @Override public String getSpecialNotes(){ return specialNotes; } /** * Getter for Brief Summary History */ @Override public String getSummaryHistory(){ return summaryHistory; } /** * Setter for Postal Address * * @param newVal */ @Override public void setAddress(String newVal){ if (isDifferent(this.address, newVal)) { this.address = newVal; setDirty(true); } } /** * Setter for Allergies * * @param newVal */ @Override public void setAllergies(String newVal){ if (isDifferent(this.allergies, newVal)) { this.allergies = newVal; setDirty(true); } } /** * Setter for Alternative Contact Details * * @param newVal */ @Override public void setAlternativeContact(String newVal){ if (isDifferent(this.alternativeContact, newVal)) { this.alternativeContact = newVal; setDirty(true); } } /** * Setter for Conditions * * @param newVal */ @Override public void setConditions(String newVal){ if (isDifferent(this.conditions, newVal)) { this.conditions = newVal; setDirty(true); } } /** * Setter for Date of Birth * * @param newVal */ @Override public void setDob(java.util.Date newVal){ if (isDifferent(this.dob, newVal)) { this.dob = newVal; setDirty(true); } } /** * Setter for Email Address * * @param newVal */ @Override public void setEmail(String newVal){ if (isDifferent(this.email, newVal)) { this.email = newVal; setDirty(true); } } /** * Setter for Gender * * @param newVal */ @Override public void setGender(GenderType newVal){ if (isDifferent(this.gender, newVal)) { this.gender = newVal; setDirty(true); } } /** * Setter for Mobile Number * * @param newVal */ @Override public void setMobile(String newVal){ if (isDifferent(this.mobile, newVal)) { this.mobile = newVal; setDirty(true); } } /** * Setter for Patient Name * * @param newVal */ @Override public void setName(String newVal){ if (isDifferent(this.name, newVal)) { this.name = newVal; setDirty(true); } } /** * Setter for NHS Number * * @param newVal */ @Override public void setNhsNmber(String newVal){ if (isDifferent(this.nhsNmber, newVal)) { this.nhsNmber = newVal; setDirty(true); } } /** * Setter for Phone Number * * @param newVal */ @Override public void setPhone(String newVal){ if (isDifferent(this.phone, newVal)) { this.phone = newVal; setDirty(true); } } /** * Setter for Special Residence Instructions * * @param newVal */ @Override public void setResidenceInstructions(String newVal){ if (isDifferent(this.residenceInstructions, newVal)) { this.residenceInstructions = newVal; setDirty(true); } } /** * Setter for Special Instructions and Advice * * @param newVal */ @Override public void setSpecialNotes(String newVal){ if (isDifferent(this.specialNotes, newVal)) { this.specialNotes = newVal; setDirty(true); } } /** * Setter for Brief Summary History * * @param newVal */ @Override public void setSummaryHistory(String newVal){ if (isDifferent(this.summaryHistory, newVal)) { this.summaryHistory = newVal; setDirty(true); } } }