Service Providers |
Services in DME Component Libraries are a flexible mechanism for accessing the capabilities of an object. Many parts of the library, most notably AccessConstraint, use services to avoid being coupled to a particular class.
Services are interfaces that are discovered through the IServiceProvider interface. IServiceProvider has just one method: GetService This method allows you to obtain an object that implements a particular service for the service provider. The required service is specified by passing its type to the method. So what exactly is a service?
As mentioned previously, a service is simply another interface. It can be any interface. In fact, it can also be a class or value type, though that is less common. IServiceProvider enables us to discover at run-time whether or not a particular instance implements a required service (interface) and to obtain that service so that we can call methods and access properties on it. In some ways it is similar to a cast. If the same class that implements IServiceProvider also implements ILocationPointService (a commonly-used DME Component Libraries service), we could simply cast the IServiceProvider to ILocationPointService and then use the methods and properties on that service interface. However, using GetService is much more flexible, however, by allowing for the case that the requested service interface is not directly implemented on the same class that implements IServiceProvider An example may help to clarify this.
First, let's define a simple service interface:
public interface IMyService { /// <summary> /// A service method. Services can have any number of methods and properties. /// </summary> void DoSomeOperation(); }
Now let's write a class that provides the service:
public class MyServiceProviderClass1 : IServiceProvider, IMyService { /// <summary> /// Implements IServiceProvider.GetService. /// </summary> public object GetService(Type serviceType) { if (serviceType == typeof(IMyService)) { return this; } else { return null; } } /// <summary> /// Implements IMyService.DoSomeOperation. /// </summary> public void DoSomeOperation() { Console.WriteLine("Doing some operation!"); } }
In this case, we've made MyServiceProviderClass implement both IServiceProvider and IMyService. We might determine that a particular service provider implemented IMyService by using run-time type checking using as, but instead it is better to use the GetService method:
private void DoSomeOperationIfAvailable2(IServiceProvider provider) { IMyService service = (IMyService)provider.GetService(typeof(IMyService)); if (service != null) { service.DoSomeOperation(); } }
The benefit of using the service provider interface is that now MyServiceProviderClass is no longer required to implement IMyService directly in order to provide the service. For example, MyServiceProviderClass could be changed to look like this:
public class MyServiceProviderClass2 : IServiceProvider { /// <summary> /// Implements IServiceProvider.GetService. /// </summary> public object GetService(Type serviceType) { if (serviceType == typeof(IMyService)) { return new ImplementMyService(); } else { return null; } } } /// <summary> /// This class actually implements IMyService for MyServiceProviderClass2. /// </summary> public class ImplementMyService : IMyService { /// <summary> /// Implements IMyService.DoSomeOperation. /// </summary> public void DoSomeOperation() { Console.WriteLine("Doing some operation!"); } }
With this change, code using the service provider interface will still be able to find the IMyService service, unlike the direct type-checking approach.
In DME Component Libraries, ExtensibleObjects use this flexibility to support extensions. An ExtensibleObject (such as the commonly used Platform) is very simple, but it can be extended with additional capabilities in the form of ObjectExtensions. Other parts of the library, such as Access, work with objects that provide certain services, and those services may be provided by the ExtensibleObject itself or by any of its extensions. If a required service is not available, a ServiceNotAvailableException will be thrown when the service is required.
In practice, Platform provides a conventional, convenient way to define real-world objects, however, because access constraints are defined using IServiceProvider objects, access can be computed using any object that provides the correct services. For example, this very simple class can be used to compute Access using the CentralBodyObstructionConstraint:
public class Simple : IServiceProvider, ILocationPointService { /// <summary> /// Construct this class with a Point describing the location. /// </summary> public Simple(Point locationPoint) { m_locationPoint = locationPoint; } /// <summary> /// Implements IServiceProvider.GetService. We use IsAssignableFrom to return any /// interface that is directly implemented by this class. /// </summary> public object GetService(Type serviceType) { if (serviceType.IsAssignableFrom(GetType())) { return this; } else { return null; } } /// <summary> /// Implements ILocationPointService.LocationPoint. /// </summary> public Point LocationPoint { get { return m_locationPoint; } } private Point m_locationPoint; }
Because the CentralBodyObstructionConstraint only requires the ILocationPointService service, we can implement a simple class that provides only this service and then use instances of the class to create a link to assign to the constraint's ConstrainedLink.