Writing Maintainable Code

advertisement
Writing Maintainable Code
by Donald J. Bales
(630) 776-0071
don@donaldbales.com
http://www.pl-sql.org
http://www.donaldbales.com
http://www.facebook.com/donald.j.bales
http://www.linkedin.com/in/donaldbales
Please buy these, I’d need a vacation!
Audience Assessment



Who has had some experience with object-orientation?
Who has done some object-oriented programming?
PL/SQL?


Who has used CREATE TYPE… AS OBJECT?
Who has used CREATE TABLE … OF…

Who knows some Sixties-speak?
“Make it obvious!”
Organization
Stewardship
“Inspect what you expect” (Ziglar)


Everyone must participate, but you’ll only have a maintainable code base if
someone makes sure you have a maintainable code base.
Use a source control system – Hint: “The data-dictionary in an Oracle database is
not a source control system!”
The Organizing Principle

All decisions must evolve around a core principal, in this case, writing
maintainable code. Not two or three principals, just one!
So what is maintain-able code?
 No or low defects – that way there’s less to maintain
 A flexible architecture; expansion capabilities – so when you’re asked to make it do
something additional, you don’t have to start over. “Plan for the future, where
every one always wants more.”
 Modular coding – well defined interfaces mean less internal interaction between
components
 Consistency – so when changes do need to be made, it’s easy. “It's better to be
consistently wrong than it is to be inconsistent.”
 Built-in troubleshooting – when something does go wrong, it’s easy to detect
 Built-in performance profiling – when it’s not expanding well, it’s easy to detect
 Use object-orientation – better taste and less filling
Organizing Your Source Code
 Break scripts into separate files
 Make scripts atomic
 Make scripts re-runnable
 Agree upon and follow naming conventions
(stewardship)
 Name files after the object they will create
“make it obvious”
Example: genders.tab for a table of gender codes
Script Type
Suffix
Stand-alone functions
.fun
Stand-alone procedures
.prc
Type specifications
.tps
Type bodies
.tpb
Tables (includes column and tables
constraints, sequence, indexes)
.tab
Package specifications
.pks
Package bodies
.pkb
Views
.vw
Inserts
.ins
Updates
.upd
Deletes
.del
Selects
.sql
SQL
Naming Conventions
In a Relational Setting…


Use plural table names, after all, they are collections
Create a sequence with the same name as its associate table, with a suffix of _ID
–


Example: a table named GENDERS would have a corresponding sequence named GENDER_ID.
Why? “Then it’s obvious.”
Use singular package names
Create a package for each table’s associated behavior
–
Example: a table named GENDERS would have a corresponding package named GENDER. Why?
Atomicity and modularity. If you need to fix something in the GENDER package, only it will be
affected.
Object Type
Object Name
Table
GENDERS
Sequence
GENDER_ID
(table) Package
GENDER
Naming Conventions
In an Object-relational Setting…


Use singular type names
Create tables for each type’s persistence
–


Example: a type named GENDER would have a corresponding table named GENDERS. Why?
“Then it’s obvious.”
Use plural table names, after all, they are collections
Create a sequence with the same name as its associate table, with a suffix of _ID
–
Example: a table named GENDERS would have a corresponding sequence named GENDER_ID.
Why? “Then it’s obvious.”
Object Type
Object Name
Table
GENDERS
Sequence
GENDER_ID
Type
GENDER
Naming Conventions
Consistency

Use the same name for something at every layer, after all it’s the same thing!
– column -> parameter -> variable, table -> cursor -> record
– attribute -> parameter -> variable, type -> table -> cursor -> record
In other words, none of this %^&*:
birth, birth_date, date_of_birth, born, born_on, bday (ahhh, your making me crazy!)

Few or no abbreviations – they create ambiguity
Use Constraints
“garbage in, garbage out”

Use check constraints
–
–
–
not null
date = trunc(date) if time is not used needed
number = trunc(number) for integers, why?
create
insert
insert
select
table
into
into
* from
TEST_INTEGER (an_integer integer);
TEST_INTEGER values (1.5);
TEST_INTEGER values (1.6);
TEST_INTEGER;
AN_INTEGER
---------1
2




Use primary keys against an id
Use unique keys against real-world primary key values
Use foreign keys – Really. I’m not kidding.
Use triggers to implement conditional foreign keys
Plan For Change





Normalize your types and tables, don't “de-normalize for performance” –
denormalization hinders expansion without modification
Don't constrain numbers – things are always getting larger and more expensive
Give varchar2 attributes and columns plenty of room -- things are always getting
larger and being internationalized. You’d be surprised how compact the English
language is when compared to others
Use CLOBS instead of varchar2(4000) when appropriate – they are now well
supported by all layers of development
use the AL32UTF8 character set
Plan for Re-Use





Design with re-use in mind
Use polymorphic naming – come up with a set of common names before coding;
minimize they set of names used
Inheritance v. code generation – Inheritance fosters consistency, efficiency, and
modularity
Encapsulation improves modularity; modularity reduces interdependence; less
interdependence means better maintainability
Did I say use the AL32UTF8 character set?
Look for the “Gotchas”



Never use = NULL in a SQL statement, because NULL is not equal to anything, not
even NULL
Search you source code for the above mistake
Never use as UPDATE statement without detection, i.e. a WHERE clause that
makes sure you are updating something that has not already been updated
PL/SQL
Prevention and Preparation

I like to think of maintainability in PL/SQL in two facets:
– Prevention
– Preparation

First let’s look at prevention
–
–
–
–

Naming Conventions
Anchors
Explicit Conversion
Blocking
And then, preparation
–
–
–
–
–
Comments
Blocking
Success and Failure Messages
Built-in Troubleshooting Capabilities
Built-in Profiling Capabilities
Naming Conventions
in PL/SQL



Use data-type prefixes (or suffixes). Why?
Parameter prefixes:
– ai – argument IN
– aio – argument IN OUT
– ao – argument OUT
For example:
PROCEDURE get_code_id_description(
aiov_code
in out varchar2,
aon_id
out number,
aov_description
out varchar2,
aid_on
in
date ) is
v_code
varchar2(30);
v_table_name
varchar2(100);
begin
...
end get_code_id_description;
Data Type
Prefix
Argument/Parameter
a_
Cursors
c_
Dates
d_
Numbers
n_
Objects
o_
Records/Rows
r_
Tables/Types
t_
Varchar2
v_
Access Modifiers
IN
i
IN OUT
io
OUT
o
*Anchoring Data Types
in PL/SQL




*The term anchoring originated with Steven.
Use %TYPE for attributes and columns
Use %ROWTYPE for records and rows
Why? You can use the wrong data type. And, it makes it obvious!
Conversions
in PL/SQL


Use explicit conversions where you wrap the conversion in a PL/SQL block in order
to detect a failure to convert a character value to a number or date.
Wrap null-able attributes or columns variables in NVL() with a well known
substitution value so your IF statements don’t fail silently.
Catching Errors
in PL/SQL





Block (BEGIN … EXCEPTION … END;) every SQL statement [except a cursor?]
Block all singletons. Oh yeah. I already stated this above, didn’t I. Must be Important?
Block all character to number conversions
Block all character to date conversions
Never use WHEN OTHERS then NULL; (OK. On a rare occasion I do.) Search your
source code for this and make sure it really needs to exist!
BEGIN
select description
into
v_description
from
GENDER;
EXCEPTION
WHEN NO_DATA_FOUND
then v_description := ' ';
WHEN OTHERS then NULL; -- It'll take you hours to find this insect.
END;

Add PROCEDURE initialize() to every package that uses its initialization section, in
order to exercise its initialization section on demand. -- This is the cause of the error
that happens once and then mysteriously goes away!
test_initialization_section.hello code…
create or replace PACKAGE test_initialization_section as
PROCEDURE hello;
PROCEDURE initialize;
end test_initialization_section;
/
create or replace PACKAGE BODY test_initialization_section as
v_greeting varchar2(4);
PROCEDURE hello is
begin
pl(v_greeting);
end hello;
PROCEDURE initialize is
begin
v_greeting := 'Hello';
end initialize;
-- THE INITIALIZATION SECTION
begin
initialize;
end test_initialization_section;
/
test_initialization_section.hello output…
BEGIN test_initialization_section.hello; END;
*
ERROR at line 1:
ORA-06502: PL/SQL: numeric or value error: character string buffer too small
ORA-06512: at "BPS.TEST_INITIALIZATION_SECTION", line 15
ORA-06512: at "BPS.TEST_INITIALIZATION_SECTION", line 20
ORA-06512: at line 1
BEGIN test_initialization_section.hello; END;
PL/SQL procedure successfully completed.
BEGIN test_initialization_section.hello; END;
PL/SQL procedure successfully completed.
SQL> exec test_initialization_section.initialize;
BEGIN test_initialization_section.initialize; END;
*
ERROR at line 1:
ORA-06502: PL/SQL: numeric or value error: character string buffer too small
ORA-06512: at "BPS.TEST_INITIALIZATION_SECTION", line 15
ORA-06512: at line 1
Comments
in PL/SQL





Explain mysterious code
Document every type specification
Document every package specification
Document other functions and procedures
Add PROCEDURE help(); to every type and package specification. Have it extract
you comments from your types and packages by querying SYS.DBA_SOURCE.
Name
Null?
------------------------------- -------OWNER
NAME
TYPE
LINE
TEXT

Type
---------------------VARCHAR2(30)
VARCHAR2(30)
VARCHAR2(12)
NUMBER
VARCHAR2(4000)
Produce distributable documentation – so somebody else does have to code the
same thing, because they don’t know it already exists!
Error Handling
in PL/SQL









Block (BEGIN … EXCEPTION … END;) every SQL [statement except a cursor?]
Only use an UPDATE statement with detection, i.e. with a WHERE clause that
makes sure you are updating something that has not already been updated!
Block all singletons. Oh yeah. I already stated this above, and again? Important?
Block all character to number conversions
Block all character to date conversions
Every unhandled exception should raise a failure (error) message
Use well formatted and well know success and failure messages
I use ORA-20000 – ORA-200## starting from zero at the top of a type or package,
incrementing by one for each new exception clause.
I use a standard format like this:
when OTHERS then
raise_application_error(-20000, SQLERRM||
' on SELECT GENDERS'||
' in GENDER.get(aiv_code)');
end;
Build-in Trouble-shooting Capability







Add boolean b_debug and then use if (b_debug) for every built-in message
Add a PROCEDURE set_debug_on() and PROCEDURE set_debug_off() to your
packages in order to turn debug logging on and off on demand
Use SYS.DBMS_OUTPUT.put_line() to print debug messages to the console
Or better yet, create a DEBUG type or package that logs your if (b_debug)
messages to a DEBUGS table, so you can view your PL/SQL progam’s progress as it
happens by querying the DEBUGS table.
Further, create a DEBUG package that sets the debug state for each PL/SQL
program in a table that is in turn queried by a given package on start-up
Dumb down your code by declaring intermediate variables that can be viewed by
a debugger
I suggest you also learn how to use the debugger!
Build-in Performance Profiling Capabilities



Use Explain Plan on your SQL statements
Keep track of the start and stop times of your functions and procedures and log
the output to a profiling table
Use SYS.DBMS_PROFILER to profile your PL/SQL programs
Test Everything
Write Test Plans
“It doesn't work until you prove it to me!”



Think about how something should work, and plan on testing it—or better yet,
write the test first!
On average, 40% of my code is defective the first time I run a well written test unit
against it! And yet, I try so hard to prevent any errors!!!
Track defects, categorize them, and then update your test plan and then test units
to make sure you test for them in the future
Write Test Units (first?)
“Ah! The proof!”








Strive for 100% coverage, it’s achievable in this development layer
Write a test package for every type and package
Name the test package after the package or type it tests, just prefix the name with
TEST_
Write one or more test units for each method: function or procedure.
Name each test unit after the method it tests, just prefix the method name with,
guess what? TEST_
Log both success and failure to the console or to a TESTS table. I personally like a
table because I can more easily do a statistic analysis on my defects.
And yes, even write test units for the Oracle packages you use. You know how
they’re supposed to work because you have some documentation—but do they?
Write an automated testing package to query the database for all your test units
and run them all any time you make a change to the source code!
create or replace TYPE base_ as object (
id
number,
CONSTRUCTOR FUNCTION base_(
self
in out nocopy base_)
return
self as result deterministic,
CONSTRUCTOR FUNCTION base_(
self
in out nocopy base_,
id
number)
return
self as result deterministic,
MEMBER FUNCTION sequence_name
return
varchar2,
MEMBER FUNCTION table_name
return
varchar2,
MEMBER FUNCTION type_name
return
varchar2,
MEMBER FUNCTION get_id
return
number,
MEMBER PROCEDURE save,
MAP MEMBER FUNCTION to_varchar2
return
varchar2,
STATIC PROCEDURE help
)
not final;
/
show errors type base_;
create or replace package body test_base_ as
PROCEDURE test is
begin
test_constructor;
test_constructor_with_id;
test_sequence_name('BASE__ID');
test_table_name('BASE_S');
test_type_name('BASE_');
test_help;
test_to_varchar2('"00000000000000000000000000001234567890"');
end;
PROCEDURE test_constructor is
o_ base_;
begin
pl(chr(10)||'base_: test_constructor');
o_ := new base_;
pl('base_: test_constructor passed.');
exception
when OTHERS then
pl('base_: test_constructor ***FAILED***: '||SQLERRM);
raise;
end;
...
PROCEDURE test_constructor_with_id is
o_ base_;
begin
pl(chr(10)||'base_: test_constructor_with_id');
o_ := new base_(1);
pl('base_: test_constructor_with_id passed.');
exception
when OTHERS then
pl('base_: test_constructor_with_id ***FAILED***: '||SQLERRM);
raise;
end;
...
PROCEDURE test_sequence_name(
aiv_expected_name
in
varchar2) is
o_ base_;
v_sequence_name varchar2(100);
begin
pl(chr(10)||'base_: test_sequence_name');
o_ := new base_;
v_sequence_name := o_.sequence_name;
if v_sequence_name = aiv_expected_name then
pl('base_: test_sequence_name passed.');
else
pl('base_: test_sequence_name ***FAILED***: v_sequence_name='||
v_sequence_name||' not '||aiv_expected_name);
end if;
exception
when OTHERS then
pl('base_: test_sequence_name ***FAILED***: '||SQLERRM);
raise;
end;
...
PROCEDURE test_table_name(
aiv_expected_name
in
varchar2) is
o_ base_;
v_table_name
varchar2(100);
begin
pl(chr(10)||'base_: test_table_name');
o_ := new base_;
v_table_name := o_.table_name;
if v_table_name = aiv_expected_name then
pl('base_: test_table_name passed.');
else
pl('base_: test_table_name ***FAILED***: v_table_name='||
v_table_name||' not '||aiv_expected_name);
end if;
exception
when OTHERS then
pl('base_: test_table_name ***FAILED***: '||SQLERRM);
raise;
end;
...
PROCEDURE test_type_name(
aiv_expected_name
in
varchar2) is
o_ base_;
v_type_name
varchar2(100);
begin
pl(chr(10)||'base_: test_type_name');
o_ := new base_;
v_type_name := o_.type_name;
if v_type_name = aiv_expected_name then
pl('base_: test_type_name passed.');
else
pl('base_: test_type_name ***FAILED***: v_type_name='||
v_type_name||' not '||aiv_expected_name);
end if;
exception
when OTHERS then
pl('base_: test_type_name ***FAILED***: '||SQLERRM);
raise;
end;
...
PROCEDURE test_get_id is
o_ base_;
n_id number;
begin
pl(chr(10)||'base_: test_get_id');
o_ := new base_;
n_id := o_.get_id;
if n_id is not NULL then
pl('base_: test_get_id passed.');
else
pl('base_: test_get_id ***FAILED***.');
end if;
exception
when OTHERS then
pl('base_: test_get_id ***FAILED***: '||SQLERRM);
raise;
end;
...
PROCEDURE test_help is
begin
pl(chr(10)||'base_: test_help');
base_.help;
pl('base_: test_help passed.');
exception
when OTHERS then
pl('base_: test_help ***FAILED***: '||SQLERRM);
raise;
end;
...
PROCEDURE test_to_varchar2(
aiv_expected_value
in
varchar2) is
o_ base_;
v_map_value
varchar2(100);
begin
pl(chr(10)||'base_: test_map_value');
o_ := new base_(1234567890);
v_map_value := '"'||o_.to_varchar2||'"';
if v_map_value = aiv_expected_value then
pl('base_: test_map_value passed.');
else
pl('base_: test_map_value ***FAILED***: v_map_value='||v_map_value||' not '||aiv_exp
end if;
exception
when OTHERS then
pl('base_: test_map_value ***FAILED***: '||SQLERRM);
raise;
end;
end test_base_;
/
show errors package body test_base_;
SQL> exec test_base_.test();
base_: test_constructor
base_: CONSTRUCTOR FUNCTION base_()
base_: test_constructor passed.
base_: test_constructor_with_id
base_: CONSTRUCTOR FUNCTION base_(id)
base_: test_constructor_with_id passed.
base_:
base_:
base_:
base_:
base_:
test_sequence_name
CONSTRUCTOR FUNCTION base_()
MEMBER FUNCTION sequence_name()
MEMBER FUNCTION type_name()
test_sequence_name passed.
base_:
base_:
base_:
base_:
base_:
...
test_table_name
CONSTRUCTOR FUNCTION base_()
MEMBER FUNCTION table_name()
MEMBER FUNCTION type_name()
test_table_name passed.
base_:
base_:
base_:
base_:
test_type_name
CONSTRUCTOR FUNCTION base_()
MEMBER FUNCTION type_name()
test_type_name passed.
base_: test_help
base_: MEMBER PROCEDURE help()
No help at this time.
base_: test_help passed.
base_:
base_:
base_:
base_:
test_map_value
CONSTRUCTOR FUNCTION base_(id)
MAP MEMBER FUNCTION to_varchar2()
test_map_value passed.
Document, Advertize, and Educate

Use Wiki-based documentation
–
–
–



it's accessible
it's version controlled
and then everyone's responsible for documentation
Don’t punish people for errors, instead reward them for test unit coverage
Share your best practices
My book, Beginning PL/SQL: From Novice to Professional, has extensive coverage
of this topic. Besides, I really do need a vacation.
Closing Thoughts






Objects (TYPEs) better model the real world, and hence provide a better solution
Well though out use inheritance can significantly reduce the amount of code to
write, the time it takes to write it, and the time it takes to maintain it
Using objects provides better consistency, in turn, better consistency provide
higher quality
In an object-relational setting Packages are better suited as role players that
orchestrate the use of objects
Or perhaps, those roles should be objects too?
You can (almost) never test too much.
References







Beginning PL/SQL: From Novice to Professional by Donald J. Bales (APress)
Java Programming with Oracle JDBC by Donald J. Bales (O'Reilly)
Oracle® Database PL/SQL Language Reference 11g Release 2 (11.2) (Oracle)
Oracle® Database SQL Language Reference 11g Release 2 (11.2) (Oracle)
Oracle® Database Object-Relational Developer's Guide 11g Release 2 (11.2) (Oracle)
Oracle PL/SQL Programming by By Steven Feuerstein, Bill Pribyl (O'Reilly)
Object-Oriented Technology: A Manager's Guide by David A. Taylor (Addison-Wesley)




http://www.pl-sql.org
http://technet.oracle.com
http://www.apress.com/book/catalog?category=148
http://oreilly.com/pub/topic/oracle
Download