In a previous article, I discussed Dependency Injection (aka Inversion of Control and the Hollywood principle) and more specifically an implementation of Dependency Injection in PL/SQL. In that we have looked at injecting values. Our objective was to remove the dependencies of our components on hard-coded values or external objects. The responsibility for getting hold of the required values has been taken out of the component.
We can take dependency injection to an even higher level. To allow for loosely coupled, component based development or assembly, we can start injecting entire components. Injecting a component means providing one component with access to another component that will perform tasks for it. Both components are independent of each other. We can just as easily switch the injected component for another one that can perform the same task.
In this article, we will take a look at how we could implement this Interface Injection in PL/SQL. As before, it is primarily meant to trigger further brainstorm and discussion, about applying best practices from other technology arenas to PL/SQL development. As before, your feedback is very welcome.
Say our package has built in hooks for Logging, Sending Emails and Security. It allows us to plug in our own implementations for each of these functions. If we do not provide our own implementation, it either falls back on its own default implementation or it does not perform Logging, Email Sending and Security Enforcement at all.
Above I have used the terms provide and plugin. If we substitute them with inject we stumble across another type of Dependency Injection, called Interface Injection. With interface injection we make implementations of a specific interface available to the injectee. Let’s take a closer look at an example.
Our package HRM_SALARY_RULES has indicated that it allows a logger component to be injected through its set_logger(p_logger in varchar2) procedure. It states that the value that is injected should refer to a procedure – stand-alone or packaged – that takes the following input parameters: p_log_level, p_log_origin and p_log_message. If a logger has been injected, the package will write logging to it. If no logger has been set, it will perform no logging at all. Note that the HRM_SALARY_RULES package knows nothing whatsoever about the implementation of the logger. It just calls it.
create or replace package HRM_SALARY_RULES is /* Logger interface: p_log_level in varchar2 , p_log_origin in varchar2 , p_log_message in varchar2 */ procedure set_logger (p_logger in varchar2); procedure set_salessalarycap( p_salessalarycap in number); function verify_salary_raise( p_job in varchar2, p_old_salary in number, p_new_salary in number) return boolean; end HRM_SALARY_RULES;
And the package body:
CREATE OR REPLACE PACKAGE BODY "HRM_SALARY_RULES" is g_salessalarycap number:= 4; g_logger varchar2(120); procedure set_logger (p_logger in varchar2) is begin g_logger := p_logger; end set_logger; procedure log ( p_log_message in varchar2 , p_log_level in varchar2 default 'debug' , p_log_origin in varchar2 default 'HRM_SALARY_RULES' ) is begin if g_logger is not null then execute immediate 'begin '||g_logger ||' ( p_log_level => :1 , p_log_origin => :2 , p_log_message => :3 ); end;' using in p_log_level , in p_log_origin , in p_log_message ; end if; end log; procedure set_salessalarycap( p_salessalarycap in number) is begin g_salessalarycap:= p_salessalarycap; end set_salessalarycap; function verify_salary_raise( p_job in varchar2, p_old_salary in number, p_new_salary in number) return boolean is l_start_time number:= dbms_utility.get_time; l_end_time number; l_result boolean:= true; begin log('verify_salary_raise for job='||p_job||' and old salary '||p_old_salary||' changing into new salary '||p_new_salary, p_log_level=> 'info'); -- among a lot of other tests... if instr(lower(p_job),'sales') > 0 then log( '- salarycap = '||g_salessalarycap); if (p_new_salary - p_old_salary)/p_old_salary > g_salessalarycap/100 then log('- FALSE: this change in salary for this job is not in accordance with the rules', p_log_level=> 'info'); l_result:= false; end if; log('- TRUE: this change in salary for this job is in accordance with the rules'); end if; return l_result; end verify_salary_raise; end HRM_SALARY_RULES;
My very simple Logger implementation – note we could create a far more advanced one that hooks into log4plsql or writes to an external file, table or database pipe. For the purpose of this article, a simple Logger will do:
create or replace package LOGGER is -- Created : 3/8/2006 2:02:12 PM -- Purpose : provides basic logging functionality; might hook into Log4PLSQL in the future procedure log_message ( p_log_level in varchar2 , p_log_origin in varchar2 , p_log_message in varchar2 ); procedure set_loglevel_threshold ( p_log_level in varchar2 ); procedure set_file_writer ( p_file_writer in varchar2 ); end LOGGER; create or replace package body LOGGER is g_loglevel_threshold varchar2(20):= 'info'; -- any message with a lower level than this threshold is not written g_file_writer varchar2(120); type hashmap is table of number(1) index by varchar2(15); g_log_levels hashmap; procedure log_message ( p_log_level in varchar2 , p_log_origin in varchar2 , p_log_message in varchar2 ) is begin if g_log_levels( p_log_level) >= g_log_levels(g_loglevel_threshold) then dbms_output.put_line( substr(p_log_origin||': '||p_log_message, 1,255)); end if; end log_message; procedure set_loglevel_threshold ( p_log_level in varchar2 ) is begin g_loglevel_threshold:= p_log_level; end set_loglevel_threshold; procedure set_file_writer ( p_file_writer in varchar2 ) is begin g_file_writer:= p_file_writer; end set_file_writer; begin g_log_levels('fine'):= 0; g_log_levels('debug'):= 1; g_log_levels('info'):= 2; g_log_levels('warning'):= 3; g_log_levels('error'):= 4; g_log_levels('fatal'):= 5; end LOGGER;
Now before we invoke the HRM_SALARY_RULES package for verifying salary raises, we should inject a logger component. If we do not, the validations will run okay, but we will not see any logging.
A simple test:
declare procedure test_job_sal_raise ( p_job in varchar2 , p_old_sal in number , p_new_sal in number ) is begin if hrm_salary_rules.verify_salary_raise(p_job => p_job,p_old_salary => p_old_sal ,p_new_salary => p_new_sal) then dbms_output.put_line( 'Salaryraise for '||p_job||' from '||p_old_sal||' to '||p_new_sal||' is approved.'); else dbms_output.put_line( 'Salaryraise for '||p_job||' from '||p_old_sal||' to '||p_new_sal||' is NOT approved.'); end if; end; begin hrm_salary_rules.set_logger('LOGGER.LOG_MESSAGE'); logger.set_loglevel_threshold('info'); test_job_sal_raise('SALESMAN', 1000, 1020); test_job_sal_raise('SALESMAN', 1000, 1200); dbms_output.put_line('========= changed loglevel to debug ============================'); logger.set_loglevel_threshold('debug'); test_job_sal_raise('SALESMAN', 1000, 1020); test_job_sal_raise('SALESMAN', 1000, 1200); end;
yields the following output:
The name Interface Injection is derived from the Interface construct in the Java programming language. In Java, an interface is a specification in its own right, with its own source file and runtime existence. An interface is like a contract that classes should follow in order to qualify for certain manipulations, such as interface injection. The Java Compiler will verify whether a class that claims to implement an interface really sticks to the contract. As a result, the strongly typed interface injection – strongly typed because the setter used for injecting the interface will only accept an object based on a class that implements the interface – is guaranteed at runtime. In PL/SQL – even though the Object Relational Extension supports Methods, Inheritance – we do not have the concept of an interface. We can only at runtime find out whether the injected component is actually one that the injected package can properly invoke.
Note that we can interchange implementations of the logger interface without any impact on the HRM_SALARY_RULES package. Just inject the new logger, even when previously another implementation has been used.
Using the Adapter Pattern to enable Interface Injection
The package has also indicated that it is ready to accept an emailer. Through the set_emailer(p_emailer in varchar2) procedure, we can specify a procedure that the package can call for sending an email. The procedure should have the signature:
p_sender in varchar2
, p_recipient in varchar2
, p_header in varchar2
, p_message in varchar2
, p_importance in varchar2
If we inject an emailer, the HRM_SALARY_RULES package will use it to send emails at appropriate times. The package already contains the logic for deciding when to send an email, to whom and with what contents. It does not have or want to know how to send an email, other than by call an injected emailer with the specified interface.
We happen to have an emailer available: EMAILER.send_email. Unfortunately, it has the wrong signature!
procedure send_email ( p_sender in varchar2 , p_addressee in varchar2 , p_subject in varchar2 , p_message in varchar2 );
Now we have the opportunity to apply another design pattern: the adapter pattern. See for example: http://www.exciton.cs.rice.edu/JavaResources/DesignPatterns/adapter.htm or http://en.wikipedia.org/wiki/Adapter_pattern . The emailer we injected should abide by the interface specified by the HRM_SALARY_RULES package. So we create an adapter that makes the emailer we have look like that interface.
create or replace package EMAILER_ADAPTER is -- Purpose : implements the emailer interface using the available mail sender package procedure send_email ( p_sender in varchar2 , p_recipient in varchar2 , p_header in varchar2 , p_message in varchar2 , p_importance in varchar2 ); end EMAILER_ADAPTER; create or replace package body EMAILER_ADAPTER is procedure send_email ( p_sender in varchar2 , p_recipient in varchar2 , p_header in varchar2 , p_message in varchar2 , p_importance in varchar2 ) is begin emailer.send_email ( p_sender => p_sender , p_addressee => p_recipient , p_subject => p_header , p_message => p_message ); end send_email; end EMAILER_ADAPTER;
And the following test demonstrates how to inject both interfaces into the HRM_SALARY_RULES package:
declare procedure test_job_sal_raise ( p_job in varchar2 , p_old_sal in number , p_new_sal in number ) is begin if hrm_salary_rules.verify_salary_raise(p_job => p_job,p_old_salary => p_old_sal ,p_new_salary => p_new_sal) then dbms_output.put_line( 'Salaryraise for '||p_job||' from '||p_old_sal||' to '||p_new_sal||' is approved.'); else dbms_output.put_line( 'Salaryraise for '||p_job||' from '||p_old_sal||' to '||p_new_sal||' is NOT approved.'); end if; end; begin hrm_salary_rules.set_logger('LOGGER.LOG_MESSAGE'); hrm_salary_rules.set_emailer('EMAILER_ADAPTER.SEND_EMAIL'); dependency_injector.inject_dependencies; test_job_sal_raise('SALESMAN', 1000, 1020); test_job_sal_raise('SALESMAN', 1000, 1200); end;
In addition to logging as implemented in our own LOGGER the reusable component HRM_SALARY_RULES now also sends emails through our own EMAILER component. Interface Injection has allowed for all of this.
Download all sources for this article: InterfaceInjection.zip
The initial article on Dependency Injection in PL/SQL: Dependency Injection in PL/SQL – Remove Hard Coded Dependencies from your code
4 thoughts on “Design Patterns in PL/SQL – Interface Injection for even looser coupling”
A further thought – rather than usiing the package initialise method, it could all be in the dependency_injector – i.e. you pass in the name of the called package, and it instantiates and sets it up.
By the way, your Spam protection doesn’t seem to like Safari.
It wouldn’t be too hard to create something that validates the signature of the passed in procedure against the package metadata – I do something similar in my code generation framework – checking if an object type has certain optional methods (like debug / log) before generating a call.
What I am wondering is if you could do something better, and potentially faster, with objects – i.e rather than passing in the name of the called package, pass in an abstract Logger object – other implementations would then be implemented as subclasses to the Logger (which could be simple wrappers to code in standard packages).
hrm_salary_rules.set_logger(pLogger => LoggerFactory(‘log4pl’) );
Then the interface could be enforced by the class / object – any new implementations would need an adapter object writing.
Alternatively, rather than having a Logger class, you could have a more general Object class, and set_logger could inspect the object methods – there’s always a higher cost to runtime introspection, but there are obviously places where it has it’s benefits – although I can’t quite see them here (mainly as objects don’t play well with dynamic pl/sql in 9).
You could then combine this with other patterns, to create some kind of bean generator.
i.e. package initialise calls dependency_injector.inject_dependencies
dependency_injector reads configuration for the called package for things like observers, logger, emailer, and other services, to be injected into the package, via standard setters
dependency_injector uses dynamic pl/sql to set the relevant values from configuration where the setters exist
(I will admit that I’m struggling to think of an occasion when I’d need that much loose coupling, but I guess the point is less around giving ME the flexibility to change my logger, than giving someone else the chance to use their logger with my code)
This is innovative and deserves more popularity than it is receiving. If done properly, the loose coupling between interfaces would allow a developer to easily use mock objects and provide a nice level of interaction based testing.
The url for the InterfaceInjection.zip hurls a 404 error.
Comments are closed.