Contract Driven Development

If you have a large piece of functionality to develop, with a complex Service layer and user interface client logic, it can be an advantage to decouple these two streams of development activity so that developers can continue in parallel, meeting up sometime in the future to combine efforts.

An approach to this is sometimes referred to as Contract Driven Development. This is where there is an agreement on a contract (or service definition) between the two development streams before they start their respective developments. Naturally, the contract can be adjusted over time, but having a solid starting point will lead to a smoother parallel development activity.

This type of development can be applied by implementing a small factory pattern within the Service layer class. The main methods on the service class are defined as normal, but their internal implementation can be routed to respective inner classes to provide an initial dummy implementation of the service, which is active for the client developers, for example, and another production implementation, which can be worked on independently by the service developers.

Note

Because the static Service methods actually exposed to callers don't expose this factory implementation, it can easily be removed later in the development cycle.

Take for example a requirement in the FormulaForce application to provide a Visualforce page to preview the final finishing positions of the drivers once the race is complete (known as the provisional positions). Development needs to start on the user interface for this feature as soon as possible, but the Service layer code has not been written yet. The client and Service layer developers sit down and agree what is needed, and define the skeleton methods in the Service layer together.

The first thing they do within the service class is to create an Apex interface that describes the service methods, which is the contract. The methods on this interface follow the same design guidelines as described earlier in this chapter.

The following code shows the definition of the IRaceService Apex interface:

public interface IRaceService {
  Map<Id, List<RaceService.ProvisionalResult>>calculateProvisionResults(Set<Id>raceIds);
  void applyRaceResults(Map<Id,List<RaceService.ProvisionalResult>>provisionalResultsByRaceId);
  void awardChampionshipPoints(Set<Id>raceIds);
}

Next, an implementation of this interface for the dummy (or sometimes known as a Stub) client implementation of this service is created RaceServiceImplSub, along with another class for the service developers to continue to develop independently as the client developer goes about their work RaceServiceImpl.

Consider the following code:

public class RaceServiceImplStub implements IRaceService {

  public Map<Id, List<RaceService.ProvisionalResult>>calculateProvisionResults(Set<Id>raceIds) {
    // Dummy behavior to allow callers of theservice be developed 
    // independent of the main service implementation 
    Id raceId = new List<Id>(raceIds)[0];
    RaceService.ProvisionalResulthamilton = new RaceService.ProvisionalResult();
    hamilton.racePosition = 1;
    hamilton.contestantName = 'Lewis Hamilton';
    hamilton.contestantId = 'a03b0000006WVph';
    RaceService.ProvisionalResultrubens = new RaceService.ProvisionalResult();
    rubens.racePosition = 2;
    rubens.contestantName = 'Rubens Barrichello';
    rubens.contestantId = 'a03b00000072xx9';
    return new Map<Id, List<RaceService.ProvisionalResult>> {
      new List<Id>(raceIds)[0] =>
      new List<RaceService.ProvisionalResult>
      { hamilton, rubens } };
    }

    public void applyRaceResults(Map<Id,List<RaceService.ProvisionalResult>> provisionalResultsByRaceId) {
      throw new RaceService.RaceServiceException('Not implemented'),
    }

    public void awardChampionshipPoints(Set<Id>raceIds) {
      throw new RaceService.RaceServiceException('Not implemented'),
    }
  }

  public class RaceServiceImpl implements IRaceService {
    public Map<Id, List<RaceService.ProvisionalResult>>calculateProvisionResults(Set<Id>raceIds) {
      throw new RaceService.RaceServiceException('Not implemented'),
    }

    public void applyRaceResults(Map<Id, List<RaceService.ProvisionalResult>>provisionalResultsByRaceId) {
      throw new RaceService.RaceServiceException('Not implemented'),
    }

    public void awardChampionshipPoints(Set<Id>raceIds) {

       fflib_ISObjectUnitOfWorkuow =Application.UnitOfWork.newInstance();

       // Query Races and contestants and bulkify list of all contestants
       List<Contestant__c> contestants = new List<Contestant__c>();
       for(Race__c race : new RacesSelector().selectByIdWithContestants(raceIds)) {
          contestants.addAll(race.Contestants__r);            
       }

       // Delegate to contestant service
       ContestantService.awardChampionshipPoints(uow, contestants);

       // Commit work
       uow.commitWork();
    }
}

Finally, the standard service methods are added, but are implemented via the service method, which is used to resolve the correct implementation of the service methods to use. The following service methods leverage the appropriate implementation of the preceding interface:

public with sharing class RaceService {

	public static Map<Id, List<ProvisionalResult>>calculateProvisionResults(Set<Id>raceIds) {
	   return service().calculateProvisionResults(raceIds);
	}

	public static void applyRaceResults(Map<Id, List<ProvisionalResult>>provisionalResultsByRaceId) {
	   service().applyRaceResults(provisionalResultsByRaceId);
	}

	public static void awardChampionshipPoints(Set<Id>raceIds) {
	   service().awardChampionshipPoints(raceIds);
	}

	private static IRaceService service() {
	  return (IRaceService)Application.Service.newInstance(IRaceService.class);
	}

	public class RaceServiceException extends Exception {} 	

	public class ProvisionalResult {
		public Integer racePosition {get; set;}
		public Id contestantId {get; set;}
		public String contestantName {get; set;}		
	}
}

The service method utilizes the Application class once more; this time a Service factory has been used to allow the actual implementation of the IRaceService interface to vary at runtime. This allows developers to control which of the two preceding implementations shown they want the application to use based on their needs. The default is to use the RaceServiceImpl class, however by inserting a record in the Services Custom Metadata Type object this can be overridden. More on this to follow.

The factory exposed via the Service field in the Application class extends the fflib_Application.ServiceFactory class, which instantiates the configured class provided in the Map passed to the factory. This factory also supports mocking the service implementation; something that will be discussed in more detail in a later chapter.

The factory has been extended to look forconfigured alternative service implementation classes via a protected (and thus hidden to end users) custom metadata object called Services, which is included in the source code for this chapter.

You can see this being queried as following:

public class Application {
  //Configure and create the ServiceFactory for this Application
  public static final fflib_Application.ServiceFactory Service =new Application.ServiceFactory (new Map<Type, Type> {IRaceService.class => RaceServiceImpl.class });

  // Customised Service factory overrides via Custom Metadata 
  private class ServiceFactory extendsfflib_Application.ServiceFactory {
    private Map<String, String> servicesByClassName = new Map<String, String>();
    public ServiceFactory(Map<Type,Type>serviceInterfaceTypeByServiceImplType) {

      super(serviceInterfaceTypeByServiceImplType);

      // Map of overridden services defined by the developer
      for(Service__mdtserviceOverride : [select DeveloperName, NamespacePrefix, ApexClass__c from Service__mdt]) {
        servicesByClassName.put(
        serviceOverride.NamespacePrefix + '.'
        serviceOverride.DeveloperName,
        serviceOverride.ApexClass__c);
      }
    }

    public override Object newInstance(Type serviceInterfaceType) {

      // Has the developer overridden the Service impl?
      if(!Test.isRunningTest() && servicesByClassName.containsKey( serviceInterfaceType.getName())) {
        String overridenServiceImpl = servicesByClassName.get( serviceInterfaceType.getName());
        return Type.forName(overridenServiceImpl).newInstance();
      }

      //Base factory returns mocked or registered impl
      return super.newInstance(serviceInterfaceType);
    }
  }
} 

Note

Querying Custom Metadata records does not count against the SOQL governor; however, the records returned do count against the maximum 50k record limit.

By creating the following Custom Metadata record the client developers can configure the application to use the stub implementation without making any code changes.

Contract Driven Development

Tip

This approach could be used to provide a custom extensibility feature in your application by making the Custom Metadata Type object Public and allowing customers or partners to override packaged implementations of your services. Use this approach with care, however, and only when its adds value to your customers or partners.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.139.107.210