One of the primary sources of software defects is duplication, sometimes referred to as Copy and Paste programming. When code or design is copied into multiple places, it's only a matter of time before one instance of the code will be modified and the others missed - resulting in defects. Even if a developer does remember to update all instances of the duplicated logic, and manages to do so without typos or other errors, multiple changes take time and effort. An important software development principle is, Don't Repeat Yourself.
A simple way to avoid duplication in your Apex code is to define "Base" classes. For example, if you find yourself adding the same or similar methods to your page controllers, extending those controllers from a base class that contains the method in a single place may be a good solution. Below are some example base classes and typical methods I have found useful on various projects.
Base Model
My model classes include logic and values specific to the business, not a specific application or interface. In my example below you'll notice code for current object setup & modification, record validation, and object accessibility./** * This is the parent class for models. Model classes should contain * the data and business rules for the application. String constants for record * types, pick list values, SOQL queries, etc. should be defined here for easy * maintenance. * <p> * An SF_BaseModel represents a single SObject record in the system. This record * must be provided by the derived class. Methods include validation, CRUD * access inquiries, save, and validation. * <p> * Validate must be provided by the derived class. * <p> * By default, save does an upsert on the current record. It can be overridden * for specialized save requirements. * * @author Steve Cox */ public abstract with sharing class SF_BaseModel { //-------------------------------------------------------------------------- // Constants //-------------------------------------------------------------------------- // Properties /** SF Id for the current object. When changed, sObj is reset as well */ public Id theId { get; set { if (value != theId) { theId = value; sObj = null; } } } /** The object with ID = theId */ public SObject sObj { get { if (null == sObj) { sObj = SF.isEmpty(theId) ? theType.newSObject() : Database.query('SELECT ' + String.join(new List<String>(fields), ',') + ' FROM ' + theType + ' WHERE Id = :theId LIMIT 1'); } return sObj; } private set; } //-------------------------------------------------------------------------- // Constructor /** * @param theType required * @param theId optional Salesforce Id of the object */ public SF_BaseModel(SObjectType theType, Id theId) { SF.preCondition(null != theType, 'SF_BaseModel() - theType is required'); this.theType = theType; this.theId = theId; } //-------------------------------------------------------------------------- // Methods public void addFields(SObjectField[] fields) { SF.preCondition(null != fields, 'SF_BaseModel.addFields() - fields are required'); for (SObjectField f : fields) { final DescribeFieldResult d = f.getDescribe(); this.fields.add(d.getName()); } this.sObj = null; // force requery } public void addFields(FieldSet fieldSet) { SF.preCondition(null != fieldSet, 'SF_BaseModel.addFields() - fieldSet is required'); for (FieldSetMember m : fieldSet.getFields()) { this.fields.add(m.fieldPath); } this.sObj = null; // force requery } /** validates, then saves the object */ public virtual void save() { if (this.validate(sObj)) { //throw new SF.ValidationException('just testing...'); upsert sObj; } } public abstract Boolean validate(SObject obj); public virtual Boolean validate() { return this.validate(sObj); } //-------------------------------------------------------------------------- // Collection Methods //-------------------------------------------------------------------------- // Accessibility Methods public virtual Boolean canView() { return objDescribe.isAccessible(); } public virtual Boolean canInsert() { return objDescribe.isCreateable(); } public virtual Boolean canUpdate() { return objDescribe.isUpdateable(); } public virtual Boolean canDelete() { return objDescribe.isDeletable(); } //-------------------------------------------------------------------------- // Helpers private SObjectType theType; private Set<String> fields = new Set<String> { 'Id' }; private DescribeSObjectResult objDescribe { get { if (null == objDescribe) { objDescribe = sObj.getSObjectType().getDescribe(); } return objDescribe; } private set; } }
Base Controller
VisualForce page controllers can benefit from shared code to handle common controller patterns, action functions, utilities, diagnostics, exception handling, authentication, and accessibility./** * This is the parent class for controllers. * This base class provides an SObject record (through it's model), * authentication, saving, CRUD accessibility, and several diagnostic methods * and properties. * <p> * Exceptions should be passed through the handleException method, in order to * track the last exception. That method can also be overridden, for more control. * * @author Steve Cox */ public virtual with sharing class SF_BaseController { //-------------------------------------------------------------------------- // Constants //-------------------------------------------------------------------------- // Properties public Boolean debugging { get { return SF.debugging; }} public String debugMessage { get; set; } public String successMessage { get; set; } public SF_BaseModel model; // Exception handling. Check a controller's lastException in unit tests public Exception lastException; public virtual void handleException(Exception ex) { lastException = ex; ApexPages.addMessages(ex); } public Boolean isAuthenticated { get { return SF_User.isAuthenticated; }} //-------------------------------------------------------------------------- // Methods public PageReference authenticate() { return SF_User.isAuthenticated ? null : SF.redirect(Page.SF_Login); } /** * save the record and return true if successful. On failure, * provide rollback and standard exception handling. */ public Boolean save() { SF.invariant(null != model, 'SF_BaseController.save() - missing model'); Savepoint save = Database.setSavepoint(); try { model.save(); return true; } catch (Exception e) { handleException(e); Database.rollback(save); return false; } } /** generic, NOOP method for use by VF; leave the body of this blank */ public void noop() { } //-------------------------------------------------------------------------- // Accessibility Methods public Boolean canView { get { return model.canView(); } } public Boolean canInsert { get { return model.canInsert(); } } public Boolean canUpdate { get { return model.canUpdate(); } } public Boolean canDelete { get { return model.canDelete(); } } //-------------------------------------------------------------------------- // Helpers }
Base Handler
I'm a fan of Tony Scott's ">ITrigger pattern. When implementing this interface, it's handy to extend a base handler so you don't have to implement every function./** * Base implementation of SF_ITrigger. Derive your handler from this class, then * override methods as needed. * * @author Steve Cox */ public virtual class SF_BaseHandler { //-------------------------------------------------------------------------- // Methods public virtual void bulkBefore() {} public virtual void bulkAfter() {} public virtual void beforeInsert(SObject so) {} public virtual void beforeUpdate(SObject oldSo, SObject so) {} public virtual void beforeDelete(SObject so) {} public virtual void afterInsert(SObject so) {} public virtual void afterUpdate(SObject oldSo, SObject so) {} public virtual void afterDelete(SObject so) {} public virtual void afterUndelete(SObject so) {} public virtual void andFinally() {} }
Base Test
Testing is probably the best application I've found for defining a base class. I try to make writing tests easier by utilizing lots of utilities and helpers./** * This class should be extended by all test classes. It contains utilities and * methods for use in unit tests * @author Steve Cox */ public virtual class SF_BaseTest { //-------------------------------------------------------------------------- // Properties public static SF_BaseTest t; /** a default user to use in System.runAs() */ public User adminUser { get { if (null == adminUser) { // all test code should execute under a user we can control so as to avoid // surprises when deploying to different environments. UserRole[] roles = [SELECT Id FROM UserRole WHERE DeveloperName = 'System_Administrator']; if (roles.isEmpty()) { roles.add(new UserRole(DeveloperName = 'System_Administrator', Name = 'r0')); insert roles; } adminUser = newUser('admin@sf.com'); adminUser.UserRoleId = roles[0].Id; insert adminUser; } return adminUser; } private set; } //-------------------------------------------------------------------------- // Methods /** * unit test initialization * <p> * Put your global initializtion within this method and provide overrides * in classes that require additional initialization. */ protected virtual void init() { debugging = true; } /** use this instead of Test.startTest() to provide extra functionality */ public void start() { init(); Test.startTest(); } /** use this instead of Test.stopTest() to provide extra functionality */ public void stop() { Test.stopTest(); } /** allows get/set of the global 'debugging' flag */ public Boolean debugging { get { return (null != SF__c.getInstance()) ? SF__c.getInstance().debugging__c : false; } set { SF__c prefs = SF__c.getInstance(); if (null == prefs) { prefs = new SF__c(); } prefs.debugging__c = value; upsert prefs; } } //-------------------------------------------------------------------------- // Methods for creating SObjects public User newUser(String username) { final Id profileId = [SELECT Id FROM Profile WHERE Name = 'System Administrator'].Id; return newUser(username, profileId); } //-------------------------------------------------------------------------- // Helpers private User newUser(String username, Id profileId) { return new User( ProfileId = [SELECT Id FROM Profile WHERE Id = :profileId LIMIT 1].Id, LastName = 'last', Email = 'user@sf.com', Username = username + System.currentTimeMillis(), CompanyName = 'sf', Title = 'title', Alias = 'alias', TimeZoneSidKey = 'America/Los_Angeles', EmailEncodingKey = 'UTF-8', LanguageLocaleKey = 'en_US', LocaleSidKey = 'en_US' ); } }
No comments:
Post a Comment