Difference between revisions of "Tutorial: Creating a Service"

m (Adding Service Functionality)
m (Adding Service Functionality)
Line 93: Line 93:
 
== Adding Service Functionality ==
 
== 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.
  
 +
{{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 have bad performance. Because the interface was already released without a flush routine a flush has to be performed every time a log message is added.
 +
 +
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.
  
 
<eiffel>
 
<eiffel>
Line 99: Line 105:
 
     LOGGER_SERVICE_I
 
     LOGGER_SERVICE_I
  
inherit
+
...
    SERVICE_I
+
 
+
    SHARED_ENVIRONMENT_CATEGORIES
+
        export
+
            {NONE} all
+
        end
+
 
+
    SHARED_PRIORITY_LEVELS
+
        export
+
            {NONE} all
+
        end
+
  
 
feature -- Extension
 
feature -- Extension
Line 128: Line 123:
  
 
     put_message_with_severity (a_msg: STRING_32; a_cat: NATURAL_8; a_level: INTEGER_8)
 
     put_message_with_severity (a_msg: STRING_32; a_cat: NATURAL_8; a_level: INTEGER_8)
             -- Logs a message specifiying a serverity level.
+
             -- Logs a message specifying a severity level.
 
             --
 
             --
 
             -- `a_msg': Message text to log.
 
             -- `a_msg': Message text to log.
 
             -- `a_cat': A message category, see {ENVIRONMENT_CATEGORIES}.
 
             -- `a_cat': A message category, see {ENVIRONMENT_CATEGORIES}.
             -- `a_level': A serverity level for the message, See {PRIORITY_LEVELS}.
+
             -- `a_level': A severity level for the message, See {PRIORITY_LEVELS}.
 
         require
 
         require
 
             a_msg_attached: a_msg /= Void
 
             a_msg_attached: a_msg /= Void
Line 141: Line 136:
 
         end
 
         end
  
feature -- Query
+
feature -- Removal
  
     frozen is_valid_category (a_cat: NATURAL_8): BOOLEAN
+
     clear_log
             -- Determines if `a_cat' is a valid logger category
+
             -- Clear any cached log data
            --
+
         deferred
            -- `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
 
         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
 
end
 
</eiffel>
 
</eiffel>
 +
 +
=== 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 <eiffel>EVENT_TYPE</eiffel>, which is an extremely powerful event abstraction that is simple to use.
 +
 +
To facilitate event hooks we'll add the events <eiffel>message_logged_events</eiffel> to notify subscribes when a message is added, and <eiffel>cleared_events</eiffel> to notify subscribes when a clear operation was performed.
 +
 +
<eiffel>
 +
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
 +
</eiffel>
 +
 +
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 <eiffel>result_consistent</eiffel> 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.
 +
 +
{{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.}}

Revision as of 09:37, 28 September 2007

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

IN this tutorial we'll be showing you how to create a service use to log messages. 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 either all deferred routines or deferred routines with only effective routines that reference the service interface directly or other interfaces in the EiffelStudio ecosystem.

Information.png Note: It is important to realize 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 _SERVICE_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 LOGGER_SERVICE_S as an actually service. In order to achieve this LOGGER_SERVICE_S must inherit SERVICE_I. As of EiffelStudio 6.1 SERVICE_I does not contain any effective or deferred routines, it's merley a place holder for future additions and a method of classification.

So now you should have something looking like this:

deferred class
    LOGGER_SERVICE_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 incase a logger service consumer deems that a particular entry deserves more or less attention. Fortunatly Griffin offeres 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 have bad performance. Because the interface was already released without a flush routine a flush has to be performed every time a log message is added. 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

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.