Difference between revisions of "Tutorial: Creating a Service"

m (Creating a Service Interface)
m (typo)
 
Line 12: Line 12:
  
 
== Overview ==
 
== Overview ==
This tutorial will showing you how to create a service logger service, used to log messages. The logger service will actually be a simplied version of the longger service already available in [[EiffelStudio]], <e>LOGGER_S</e>.
+
This tutorial will showing you how to create a service logger service, used to log messages. The logger service will actually be a simplied version of the logger service already available in [[EiffelStudio]], <e>LOGGER_S</e>.
  
 
The tutorial will cover creating and implementing a service, registering a service, adding eventing and finally consuming the service and using an service observer.
 
The tutorial will cover creating and implementing a service, registering a service, adding eventing and finally consuming the service and using an service observer.

Latest revision as of 14:14, 16 June 2008

Construction.png Not Ready for Review: This Page is Under Development!

In this tutorial I'll demonstrate the process for integrating third-party services inside EiffelStudio and hooking up internal parts of EiffelStudio to use the new service.

Before we begin you should have a fundamental understanding of what a service is and a clear understanding of the guidelines for writing service

Information.png Note: This tutorial is followed up by another tutorial for creating an EiffelStudio tool for displaying information published by the service.

Getting Started

When extending EiffelStudio, it is a good idea to separate your code from the EiffelStudio code. The Customizing the EiffelStudio Project page describes the process of doing this.

Overview

This tutorial will showing you how to create a service logger service, used to log messages. The logger service will actually be a simplied version of the logger service already available in EiffelStudio, LOGGER_S.

The tutorial will cover creating and implementing a service, registering a service, adding eventing and finally consuming the service and using an service observer.

Although the service contains a simple interface, it's actually quite complete in that the service itself will make use of the Event List Service as a demonstration how reusing services in EiffelStudio can make development strategies quicker and easier.

Creating a Service Interface

The very first step in creating a service is to define a service interface. A service interface should contain only deferred routines or deferred routines with effective routines that only reference the service interface directly or other interfaces in the EiffelStudio ecosystem.

Information.png Note: It is important that the interface abstraction allows for complete freedom to be given to the implementation of the service. Implementation details are not public and should remain that way. No consumer of the service should ever attempt to reverse assign a retrieve service to the implementation class but to the interface class. Consumer of the service should not have to rely on the implementation details of a service and doing so will potentially break code in the future or if a different implementation is returned than expected when querying to a specific service.

This tutorial is creating a logger service so it makes senses we should create a service interface class call LOGGER_SERVICE_S. Create a deferred class LOGGER_SERVICE_S in your extension project cluster.

Information.png Note: All service interfaces by convention are suffixed _S. This makes it clear to a consumer that they are using a service interface. All other related interfaces for the service should be suffixed _I to indicate an interface.

The first step is to define SIMPLE_LOGGER_S as an actually service interface. In order to achieve this SIMPLE_LOGGER_S must inherit a service base interface SERVICE_I. As of EiffelStudio 6.1 SERVICE_I does not contain any effective or deferred routines, it is merely a place holder for future additions and a method of classification. It does however inherit another service class SITE, which will be talked about it later.

So now you should have something looking like this:

deferred class
  SIMPLE_LOGGER_S
 
inherit
  SERVICE_I
 
end

Of course this doesn't do all that much, in fact it does nothing! We need to add a way to log messages. For this we'll add put routines; put_message and put_message_with_severity.

A logger shouldn't just simply log a message, it's just not powerful enough. So the put routines for the service should permit categorization and even indicate a level of severity in case a logger service consumer deems that a particular entry deserves more or less attention. Fortunately ESS offers built in support for categorization and a basic priority level, which will serve quite nicely as a translation for a log item severity level.

Categories and Priorities

ENVIRONMENT_CATEGORIES is a class consisting of constants defining EiffelStudio environment region categories. There are constants for the compiler, debugger the editor and so forth. As extenders you are free to add your own categories and utilize them. Any class can access a single instance of ENVIRONMENT_CATEGORIES through SHARED_ENVIRONMENT_CATEGORIES.categories.

PRIORITY_LEVELS is another class containing constants for basic priority levels; high, normal and low. Any class can access a single instance of PRIORITY_LEVELS through SHARED_PRIORITY_LEVELS.priorities.

We want to make use of both categories and priorites in the logger service so LOGGER_SERVICE_S should inherit both SHARED_ENVIRONMENT_CATEGORIES and SHARED_PRIORITY_LEVELS.

Warning.png Warning: Inheriting the shared classes should not affect the service interface so be sure to set the export status when inheriting those shared classes!

ENVIRONMENT_CATEGORIES and PRIORITY_LEVELS in addition to being constant definition classes, also contain validation functions to ensure a category identifier is a known identifiers, as it true for a priorty identifier. In the practice of Design by Contract our service routines are going to be passed a category and severity (priority) level, which require validation. Given SHARED_ENVIRONMENT_CATEGORIES.categories and SHARED_PRIORITY_LEVELS.priorities are not exported members of the interface we'll need to create proxy query functions, which is actually good design. These proxy function can then be used an service routine preconditions and can also be used by a service consumer client when making the call to one of the service routines.

Below is the complete code for adding categories and severity levels to the logger service interface. The proxy function is_valid_category has been added for category validation and is_valid_severity_level added for severity level validation.

deferred class
    LOGGER_SERVICE_I
 
inherit
    SERVICE_I
 
    SHARED_ENVIRONMENT_CATEGORIES
        export
            {NONE} all
        end
 
    SHARED_PRIORITY_LEVELS
        export
            {NONE} all
        end
 
feature -- Query
 
    frozen is_valid_category (a_cat: NATURAL_8): BOOLEAN
            -- Determines if `a_cat' is a valid logger category
            --
            -- `a_cat': A category identifier to validate.
            -- `Result': True to indicate the category is valid; False otherwise.
        do
            Result := categories.is_valid_category (a_cat)
        end
 
    frozen is_valid_severity_level (a_level: INTEGER_8): BOOLEAN
            -- Determines if `a_level' is a valid severity level
            --
            -- `a_level': A severity level.
            -- `Result': True to indicate the level of severity is valid; False otherwise.
        do
            Result := priorities.is_valid_priority_level (a_level)
        end
 
end

Adding Service Functionality

Still, the logger service has not functionality. The service interface is now at a stage where the actual service routines can be added. We're going to add three routines; two to log messages and another to clear the log.

Information.png Note: The logger created here is very simple. It would be highly likely for you to add routines to flush log entries and even provide a mutable status attribute to set an auto-flush mode. It's actually important to realize your design before releasing a service in a version of EiffelStudio, because once released then service interface may be used by others. In our case there is no flush routine, which means a later implementation of the logger service who's message flushing capabilities are expensive, will suffer bad performance penalties. The interface was already released without a flush routine so now a flush has to be performed every time a log message is added because existing consumer clients are not using the new service version's flush routine. When designing a service it's necessary to think how the service might be used by EiffelStudio, the Eiffel compiler and what might happen in the future. In the case of the logger you may have one EiffelStudio SKU that presents logged information in an embedded EiffelStudio tool, in another it may be pushed to the OS event log, in another it may be written to a file. Or, you might have all three available and a preference to indicate how added log messages are handled.

Here is the basic interface with the previous interface members elided for clarity.

deferred class
    LOGGER_SERVICE_I
 
...
 
feature -- Extension
 
    put_message (a_msg: STRING_32; a_cat: NATURAL_8)
            -- Logs a message.
            --
            -- `a_msg': Message text to log.
            -- `a_cat': A message category, see {ENVIRONMENT_CATEGORIES}.
        require
            a_msg_attached: a_msg /= Void
            not_a_msg_is_empty: not a_msg.is_empty
            a_cat_is_empty_is_valid_category: is_valid_category (a_cat)
        do
            put_message_with_severity (a_msg, a_cat, {PRIORITY_LEVELS}.normal)
        end
 
    put_message_with_severity (a_msg: STRING_32; a_cat: NATURAL_8; a_level: INTEGER_8)
            -- Logs a message specifying a severity level.
            --
            -- `a_msg': Message text to log.
            -- `a_cat': A message category, see {ENVIRONMENT_CATEGORIES}.
            -- `a_level': A severity level for the message, See {PRIORITY_LEVELS}.
        require
            a_msg_attached: a_msg /= Void
            not_a_msg_is_empty: not a_msg.is_empty
            a_cat_is_empty_is_valid_category: is_valid_category (a_cat)
            a_level_is_valid_severity_level: is_valid_severity_level (a_level)
        deferred
        end
 
feature -- Removal
 
    clear_log
            -- Clear any cached log data
        deferred
        end
 
...
 
end

The simpler put_message service routine has already been implemented by calling the more specific put_message_with_severity, using a default severity level. Now our service has a simple and more specific versions of logging routines with zero-cost to a service implementer, and value-add to the logger service clients as they will not have to use the specific version each time a log message is to be added.

Adding Events

To be a good service citizen of EiffelStudio it is highly desirable to provide events service consumers can hook up to. Not all services have events but it so happens that the logger service is interacted with in a way that tools or other services may be interested in; a message is added and messaged are cleared.

Griffin provides its own even mechanism using EVENT_TYPE, which is an extremely powerful event abstraction that is simple to use.

To facilitate event hooks we'll add the events message_logged_events to notify subscribes when a message is added, and cleared_events to notify subscribes when a clear operation was performed.

deferred class
    LOGGER_SERVICE_I
 
...
 
feature -- Events
 
    message_logged_events: EVENT_TYPE [TUPLE [service: LOGGER_SERVICE_I; message: STRING_32;
        category: NATURAL_8; level: INTEGER_8]]
            -- Events called when a message has been logged
        deferred
            result_attached: Result /= Void
            result_consistent: Result = Result
        end
 
    cleared_events: EVENT_TYPE [TUPLE [service: LOGGER_SERVICE_I]]
            -- Events called when the messages have been cleared from the log
        deferred
            result_attached: Result /= Void
            result_consistent: Result = Result
        end
 
...
 
end

Note, the events are deferred also. This given a logger service implementation the option to implement the events as attributes or deferred-evaluation functions, for performance and memory footprint optimizations. The postcondition result_consistent ensures that any deferred-evaluation/once-per-object implementation actually performs the correct per-object caching.

Respecting the events could be implemented using lazy-evaluation no class invariants have been added to ensure the event attributes are always attached, because (A) in deferred-evaluation they may not be attached until called and (B) evaluating the class invariants would remove any optimization benefits of deferred-evaluation as they would be evaluated after the service has been created.

Information.png Note: For events implemented as attributes it's desirable for the implementation to add the appropriate invariants to ensure the events at in an attached state after the logger service has been created.

Creating a Consumer

Consumer helper classes are a nice addition to adding a new service. It makes working with an added service so much easier and it take only a minute to create a consumer.

A consumer is a helper class that provides cached access to a service. Service consumers can then simply inherit one or more consumer helper classes to gain access to desired services.

Information.png Note: As a convention all service consumer helper classes are suffix by _SERVICE_CONSUMER.

Below is literally all the code you need to create a consumer helper class for your service. It simply renames the features from a generic class base class as to provide non-conflict feature names when using multiple service consumer helper classes from a single class.

class
    LOGGER_SERVICE_CONSUMER
 
inherit
    SERVICE_CONSUMER [LOGGER_SERVICE_S]
        rename
            service as logger_service,
            is_service_available as is_logger_service_available,
            internal_service as internal_logger_service
        end
 
end

Creating the Service Implementation

The ground work has been laid for basing the implementation of the logger service on. In this tutorial we are simply going to make use of the Event List Service, which does an fantastic job of proving facilities for adding and removing added object (call event items). It means our implementation is basically a proxy to another service. Using another service saves time and effort to go from design to integration. The additional benefit of using the Event List Service is that an EiffelStudio tool can be created very quick to display the logged messages because EiffelStudio foundations provides base implementation for tools built using consuming the Event List Service (for those that are interested see ES_EVENT_LIST_TOOL_BASE and ES_CLICKABLE_EVENT_LIST_TOOL_BASE.)


Below is the stub implementation for the effective logger service.

Information.png Note: Just like service interfaces, interfaces and service consumers, effective service classes should always yield the name of the service with the _S suffix removed.

class
    LOGGER_SERVICE
 
inherit
    LOGGER_SERVICE_S
 
    SAFE_AUTO_DISPOSABLE
 
    EVENT_LIST_SERVICE_CONSUMER
        export
            {NONE} all
        end
 
create
    make
 
feature {NONE} -- Initialization
 
    make
            -- Initialize logger service.
        do
                -- Initialize events
            create message_logged_events
            create cleared_events
 
                -- Set up automatic cleaning of event object
            auto_dispose (message_logged_events)
            auto_dispose (cleared_events)
        end
 
feature -- Extension
 
    put_message_with_severity (a_msg: STRING_32; a_cat: NATURAL_8; a_level: INTEGER_8)
            -- Logs a message specifying a severity level.
            --
            -- `a_msg': Message text to log.
            -- `a_cat': A optional message category.
            -- `a_level': A serverity level for the message.
        do
        end
 
feature -- Removal
 
    clear_log
            -- Clear any cached log data
        do
        end
 
feature -- Events
 
    message_logged_events: EVENT_TYPE [TUPLE [service: LOGGER_SERVICE_S; message: STRING_32; 
        category: NATURAL_8; level: INTEGER_8]]
            -- Events called when a message has been logged
 
    cleared_events: EVENT_TYPE [TUPLE [service: LOGGER_SERVICE_S]]
            -- Events called when the messages have been cleared from the log
 
invariant
    message_logged_events_attached: not is_zombie implies message_logged_events /= Void
    cleared_events_attached: not is_zombie implies cleard_events /= Void
 
end

The service is an implementation of the previously defined logger service interface (LOGGER_SERVICE_S) so we have to inherit the interface so later the SOA core can validated the service when it's registered, but more on this later.

Also inherited is SAFE_AUTO_DISPOSABLE, a memory resource management base class for handling automatically disposing of class objects when then class object itself is disposed. As the service hosts two events, after creation of those event they are added to the auto-dispose pool to automatic disposal. This saves the logger service from having to implement safe_disposable and performing the resource management manually. For more information on resource management see EiffelStudio Memory Management.

As stated this implementation is actually using the Event List Service so access to the service is provided using the service consumer EVENT_LIST_SERVICE_CONSUMER.

A creation routine make has been added for the class to create the implemented event attribute objects and register them with the auto disposable pool from SAFE_AUTO_DISPOSABLE. As recommended the events, implemented as attributes, have class invariants to ensure their validity for the life time of the object.

All the other routines are empty stubs waiting to be implemented.

Supporting the Event List Service: Event Items

The Event List Service make used of an entity call an event item. So, for the logger service to effectively use the Event List Service it must implement an event item for a log message.

Fortunately Griffin already provides much of the implementation to implement basic event items through EVENT_LIST_ITEM. EVENT_LIST_ITEM however does not provide all the implementation required for an logger based-event item, there are still a few deferred routines that require implementation. One of these function type is important for agnostic event item identification as the implementation for an event item should not be relied upon by any other part of EiffelStudio other than the implementation aspect responsible for creating it, in this case the implementation of the logger service - LOGGER_SERVICE.

Event List Item Types

An event item type corresponds to a type identifier found in EVENT_LIST_ITEM_TYPES. As the logger service is a new service introducing a new type of event item the logger service needs to add a new type identifier. Open EVENT_LIST_ITEM_TYPES and add the following code:

log: NATURAL_8 = 2
        -- Logger event list item type

Even though the type constant identifier is given the value of 2, the value should be unique. If you are using a version of EiffelStudio where 2 is already taken by another type constant identifier, pick the next available index.

A Log Event Item Abstraction

To support clean abstraction and separation from the underlying implementation, a derived event item interface for a log message should be created, implementing type from EVENT_LIST_ITEM_I. type should return the new type constant identifier - log - added to EVENT_LIST_ITEM_TYPES.

Create a new deferred class LOGGER_EVENT_LIST_ITEM_I and copy the following code into it:

deferred class
    LOGGER_EVENT_LIST_ITEM_I
 
inherit
    EVENT_LIST_ITEM_I
 
feature -- Access
 
    frozen type: NATURAL_8
            -- Event list item type identifier, see {EVENT_LIST_ITEM_TYPES}
        once
            Result := {EVENT_LIST_ITEM_TYPES}.log
        end
 
end

A Log Event Item Implementation

Using the newly create log interface LOGGER_EVENT_LIST_ITEM_I, create a new class LOGGER_EVENT_LIST_ITEM, which will be the event list item LOGGER_SERVICE will use to push log messages to the Event List Service.

Copy and paste the following code into the new class:

class
    LOGGER_EVENT_LIST_ITEM
 
inherit
    LOGGER_EVENT_LIST_ITEM_I
 
    EVENT_LIST_ITEM
        rename
            make as make_event_list_item
        end
 
create
    make
 
feature {NONE} -- Initialization
 
    make (a_category: like category; a_description: like description; a_level: like priority)
            -- Initialize a new event list error item.
            --
            -- `a_category': Log category, see {ENVIRONMENT_CATEGORIES}.
            -- `a_description': Log message.
            -- `a_level': Serverity level of the logged message.
        require
            a_category_is_valid_category: is_valid_category (a_category)
            a_description_attached: a_description /= Void
            not_a_description_is_empty: not a_description.is_empty
            a_level_is_valid_priority: is_valid_priority (a_level)
        do
            make_event_list_item (a_category, a_level, Void)
            description := a_description
        ensure
            category_set: category = a_category
            description_set: description = a_description
            priority_set: priority = a_level
        end
 
feature -- Access
 
    description: STRING_32
            -- Log message description
 
feature -- Query
 
    is_valid_data (a_data: like data): BOOLEAN
            -- Determines is the user data `a_data' is valid for the current event item.
            --
            -- `a_data': The user data to validate.
            -- `Result': True if the user data is valid; False otherwise.
        do
            Result := True
        end
 
invariant
    description_attached: description /= Void
    not_description_is_empty: not description.is_empty
 
end

The new logger service event list item implements the deferred functions EVENT_LIST_ITEM leaves deferred. Namely description and is_valid_data.

description is implemented as an attribute and the class invariants taken from EVENT_LIST_ITEM_I.description's postconditions. The description will serve a the holder of a log message.

is_valid_data determines if an custom data is valid for the event list item but seeing as not custom data is used by the logger event list item it's implemented returning always True.

Finally, LOGGER_EVENT_LIST_ITEM's creation routine make is used to set the information passed through LOGGER_SERVICE_I.put_message_with_severity_level on the log event list item. The category and priority are members of EVENT_LIST_ITEM and are effective implementations of the deferred parent declarations of EVENT_LIST_ITEM_I

With the log event list item created it's time to finish up the implementation of the logger service.

Implementing the Service Routines

The final stage to complete the implementation of the logger service is to implement the deferred features of LOGGER_SERVICE_S. The logger service is quite compact and only two features require implementing; put_message_with_severity and clear_log.

put_message_with_severity will basically take the information passed in and create an instance of LOGGER_EVENT_LIST_ITEM and push it to the Event List Service, if it's available.

Warning.png Warning: When working with services it is important to remember that they might not be available for one reason or another, even if they are available when you are debugging. There are multiple reasons for their absence, one being a service did not make the final release because of bugs and time constraints.

In order to push and remove ###event list items### to and from the Event List Service, the Event List Service interface requires a "Context Cookie". A context cookie allows the Event List Service to track where ###event list items### were added from. Using a context cookie enables tools and services to remove all ###event list items### added by that tool or service in a single step. The benefit of this, apart from simplicity, is the tool or service does not have to manually track what it pushed to the Event List Service for later removal.

Adding the following code to LOGGER_SERVICE will support put_message_with_severity and clear_log in their endeavors:

feature {NONE} -- Access
 
    context_cookie: UUID
            -- Context cookie for event list service
        once
            create Result.make_from_string ("E1FFE100-0106-4145-A53F-ED44CE92714D")
        end

put_message_with_severity is not done with. The logger service exposes events, one of which should be published to any subscribers when a message is logged. put_message_with_severity will need to publish the event also.

The full implementation of put_message_with_severity looks like this:

feature -- Extension
 
    put_message_with_severity (a_msg: STRING_32; a_cat: NATURAL_8; a_level: INTEGER_8)
            -- Logs a message specifiying a severity level.
            --
            -- `a_msg': Message text to log.
            -- `a_cat': A optional message category.
            -- `a_level': A severity level for the message.
        local
            l_item: like create_event_list_log_item
        do
            if is_event_list_service_available then
                l_item := create_event_list_log_item (a_msg, a_cat, a_level)
                event_list_service.put_event_item (context_cookie, l_item)
            end
 
                -- Publish events
            message_logged_events.publish ([Current, a_msg, a_cat, a_level])
        end

To support better design, put_message_with_severity should not actually create an instance of LOGGER_EVENT_LIST_ITEM directly. It's possible for another party to take and extend the logger service creating even more specialized log ##event list items###, in which case the descendant logger service should not have to reimplement put_message_with_severity in order to change the type of log ##event list items### pushed to the Event List Service. To facilitate, instead a factory function will be used to create the log ##event list items###, allowing service extenders to create specialize log ##event list items###.

The factory function, used to create the log ###event list item###:

feature {NONE} -- Factory
 
    create_event_list_log_item (a_msg: STRING_32; a_cat: NATURAL_8;
        a_level: INTEGER_8): EVENT_LIST_LOG_ITEM_I
            -- Creates a new event list item for a log message.
            --
            -- `a_msg': Message text to log.
            -- `a_cat': A message category, see {ENVIRONMENT_CATEGORIES}.
            -- `a_level': A severity level for the message, See {PRIORITY_LEVELS}.
            -- `Result': An event list service item.
        require
            a_msg_attached: a_msg /= Void
            not_a_msg_is_empty: not a_msg.is_empty
            a_cat_is_empty_is_valid_category: is_valid_category (a_cat)
            a_level_is_valid_severity_level: is_valid_severity_level (a_level)
        do
            create {EVENT_LIST_LOG_ITEM} Result.make (a_cat, a_msg, a_level)
        ensure
            result_attached: Result /= Void
            result_is_log_item: Result.type = {EVENT_LIST_ITEM_TYPES}.log
        end

The last part to complete the logger event service is implementing clear_log.

clear_log is contains fairly rudementry logic thanks to the Event List Service. In a single line of code the logger service can remove all pushed log ##event list items### from the Event List Service using:

event_list_service.prune_event_items (context_cookie)

In addition to removing all the pushed log ##event list items### clear_log must also publish the event cleared_events to notify all subscribes of the purge.

The full implementation of clear_log is as follows:

feature -- Removal
 
    clear_log
            -- Clear any cached log data
        do
            if is_event_list_service_available then
                event_list_service.prune_event_items (context_cookie)
            end
 
                -- Publish events
            cleared_events.publish ([Current])
        end

That's it! Coming this far you have created all of the interfaces and implementation needed to use the service. All that's left to do now is to proffer the service so other tools and services can make use of the logger service.

Proffering a Service

To permit access to the logger service you need to register the service with a [[Service#Service Containers|service container]. Typically most services in EiffelStudio will be registered in ES_ABSTRACT_GRAPHIC.add_core_services.

In ES_ABSTRACT_GRAPHIC.add_core_services add the following line of code to register the logger service.

a_container.add_service_with_activator ({LOGGER_SERVICE_S},
    agent create_logger_service, False)

The above line of code registers the logger service with a "activator", which ensures the service is only created when it's requested. This saves on start up performance degradation as well as not using up memory required to instantiate and run the logger service. It may seem small in the scope of a single, tiny service but every little counts and that count grows with ever services registered.

Services are registered using their service interface type. One reason why the _S suffix is used is for identity. When querying for a service you know instinctively to look for a class name ending _S, which is assignable to an Eiffel variable of the same type.

To actually create the logger service, a factory function is used which has the added advantage of allowing descendants to redefine the default service returned (or return no service at all.)

feature {NONE} -- Service factories
 
    create_logger_service: LOGGER_SERVICE_S
            -- Creates the logger service
        do
            create {LOGGER_SERVICE}Result.make
        end

Next Steps

With the all the code in place, the logger service is read to be interacted with. In the next tutorial on services we will put the logger service to use in existing functionality in EiffelStudio.

In the final tutorial we'll explorer using EiffelStudio Foundations to create a dockable Eiffel tool to display the logged messages.

SVN Patch

For the full tutorial code, please grab an SVN patch from here.