Skip to main content

Programmers are Humans, Too - How to Get Crusty Developers to Change

What? You didn't know that? :-)

On a recent blog post, I received this comment:
Thanks for the video. You've answered my question and given us lots to talk over. Now, do you have any advice on getting those resistant to change, crusty, old, developers to buy in to change?
My immediate response was: incentives and fun.

So now it is time to elaborate a bit.

First of all, the hardest part of programming is not learning new features or absorbing the syntax of a programming language. After all, learning such a language is waaaaay easier than learning a human language - primarily because when we write code we are communicating with something that "thinks" quickly but is not particularly "smart." It, the computer however you want to define that these days, does what we tell it to do. Really, it does - no matter how many sci-fi movies you've watched that indicate otherwise.


Since a computer isn't very smart, we have to communicate with it using very formal, stiff syntax. That's a pain, but it also makes things easier.

Anyway, the hardest part of programming is that programming is performed by humans, and we are complicated, contradictory, flawed organisms. Thus, the social processes that envelop the programming tasks are usually a bigger challenge (and opportunity) than focusing on the technology itself (that is, go to another class on Advanced PL/SQL, and so on).

So when it comes to the question of "How do get change-resistant crusty, old programmers to change their behaviors?", the answers are too fold:
  1. Recognize that we are all change-resistant, not just the Wise Elders.
  2. We (humans in general) change when we are incentivized to change.
Appeals like "Do the right thing." or "Stop acting your age." or "Nobody's done that for decades." will not motivate a person to change their ways.

We change when we feel it is in our best interest to do so - or we change without quite realized it, because someone was very clever in how they approached things.

So my suggestions for being clever and incentivizing for change:
  1. Identify the behaviors you want to see changed.
  2. Figure out how to measure the change.
  3. Make it as easy as possible to make the change.
  4. Make the change as fun as possible.
  5. Reward us as and when we go through the transformation.
Let's look at an example. Suppose that we have a problem in our development team that code re-use is pitiful. Instead different members of the team write the same or similar stuff, over and over again.  How can we get the behavior to change? 

1. Identify the behaviors you want to see changed.

I want my team to find and re-use existing subprograms in package APIs, rather than rewrite stuff.

2. Figure out how to measure the change.

How can I tell when people are reusing code? Ah, the beauty of PL/SQL - code is stored in the database and metadata about that code is stored in various data dictionary views. Of most relevance here is PL/Scope and the ALL_IDENTIFIERS view. I can run a query that shows me how many places a particular or all subprograms are invoked.

I've attached a script at the bottom of this post that Jan-Hendrik van Heusden provided to me, which you might find helpful in this area.

I've also posted a PL/Scope helper package on LiveSQL that may help get you started on PL/Scope more generally. 

3. Make it as easy as possible to make the change.

Now, this is a tough one. How do we make our developers more aware of what they can re-use and how? I do not have a simple or "packaged" solution for you, so I will instead offer some ideas:

  • Standardize documentation of program headers so you can extract documentation of those subprograms
  • Build an intranet site (using Application Express!) to display this generated documentation or make it easy to search the code catalog (ALL_SOURCE)
  • Monthly code review: highlight new program units and potential code re-use opportunities
4. Make the change as fun as possible.

Yes, that's right, I used the word "fun" in the context of not just software, from changing software coding behaviors. Hmmmm. Well, how do people have fun? They play games!

And there is a game you can play - individually and as a group - that will help improve your ability to identify repetitions in code and therefore opportunities for reuse. That game is Set. Set is a wonderfully simple and elegant game. I encourage you to buy the actual deck of cards (but, yes, you can play it online and there is an app, of course).



Play this game with your kids (if they are young enough to still want to play games with you) and play it together as a team. It's fun - and it's all about enhancing your pattern analysis skills. 

5. Reward us as and when we go through the transformation.

Oh how we humans like our rewards! They can come in roughly two forms: tangible and intangible.

Examples of tangible rewards: gift cards, promotions, raises, etc.

Examples of intangible rewards: recognition, mostly.

In terms of promoting re-use, I could envision a monthly or quarterly awarding of prizes for (a) who built the code that is reused the most (likely a more senior developer) and (b) who is reusing code most effectively (likely a more junior developer).

How could you tell? Again, PL/Scope would help this.

=================

Well, I hope you find these ideas helpful. No doubt about it, it is way harder to change human patterns of behavior than to pick up new technical skills.


Using PL/Scope to analyse identifier usages in PL/SQL

/*
From: Jan-Hendrik van Heusden
Sent: Tuesday, September 17, 2013 10:49 AM
To: Steven Feuerstein
Subject: Query to analyse identifier usages in PL/SQL


Hi Steven,

I was working in a project where quite some obsolete code was being removed, 
and I wanted to know if certain methods, variables were actually used yet. 
I first made sure that everything was compiled with the appropriate settings (IDENTIFIERS:ALL).

Then I created the attached query, I thought you might be interested in it. 
Note that this query does of course not show all possible usages; 
it does not list usages in views or over database links, and it definitely 
does not list usage in external souces (sql files, queries made from external applications etc.).

Nevertheless I found it very useful for what it does (usage of PL/SQL 
identifiers within PL/SQL). Other PL/SQL developers may be interested, 
so feel free to use or publish it. You may also want to change things like 
the sorting order, feel free to do so.

Best regards, 
Jan-Hendrik van Heusden
*/

  SELECT pl_id.declaring_owner,
         pl_id.declaring_object_type,
         pl_id.declaring_object_name,
         pl_id.declaring_line,
         pl_id.declared_type,
         pl_id.declared_name,
         pl_id.overload,
         pl_id.referring_owner,
         pl_id.referring_object_type,
         pl_id.referring_object_name,
         pl_id.usage,
         pl_id.external_call,
         pl_id.referring_line,
         pl_id.referring_column,
         pl_id.referring_source_text,
         CASE
            WHEN     pl_id.usage = 'DECLARATION'
                 AND pl_id.declared_type NOT IN ('PACKAGE', 'TRIGGER')
            THEN
               COUNT (
                  CASE
                     WHEN pl_id.usage NOT IN ('DECLARATION', 'DEFINITION')
                     THEN
                        1
                  END)
               OVER (PARTITION BY pl_id.signature)
         END
            nr_of_references,
         CASE
            WHEN     pl_id.usage = 'DECLARATION'
                 AND pl_id.declared_type NOT IN ('PACKAGE',
                                                 'FUNCTION',
                                                 'PROCEDURE',
                                                 'TRIGGER')
            THEN
               COUNT (CASE
                         WHEN pl_id.usage NOT IN ('DECLARATION',
                                                  'DEFINITION',
                                                  'ASSIGNMENT',
                                                  'CALL')
                         THEN
                            1
                      END)
               OVER (PARTITION BY pl_id.signature)
         END
            var_read_references,
         CASE
            WHEN     pl_id.usage = 'DECLARATION'
                 AND pl_id.declaring_object_type != 'TRIGGER'
                 AND pl_id.declared_type NOT IN ('PACKAGE', 'TRIGGER', 'LABEL')
            THEN
               COUNT (pl_id.external_call) OVER (PARTITION BY pl_id.signature)
         END
            nr_of_external_references,
         CASE
            WHEN UPPER (
                    SUBSTR (pl_id.referring_source_text,
                            pl_id.referring_column,
                            LENGTH (pl_id.declared_name))) =
                    pl_id.declared_name
            THEN
                  REGEXP_SUBSTR (
                     SUBSTR (pl_id.referring_source_text,
                             1,
                             pl_id.referring_column - 1),
                     '("?[a-zA-Z][a-zA-Z0-9_$#]*"?[.%]"?)*"?$')
               || SUBSTR (pl_id.referring_source_text,
                          pl_id.referring_column,
                          LENGTH (pl_id.declared_name))
               || REGEXP_SUBSTR (
                     SUBSTR (
                        pl_id.referring_source_text,
                        pl_id.referring_column + LENGTH (pl_id.declared_name),
                        1),
                     '"')
         END
            qualified_called_name,
         pl_id.signature
    FROM (WITH pl_declaration
               AS (SELECT decl.*,
                          CASE
                             WHEN TYPE IN ('FUNCTION', 'PROCEDURE')
                             THEN
                                DENSE_RANK ()
                                   OVER (PARTITION BY decl.owner,
                                                      decl.object_type,
                                                      decl.object_name,
                                                      decl.TYPE,
                                                      decl.name
                                         ORDER BY decl.line, decl.col)
                          END
                             overload
                     FROM all_identifiers decl
                    WHERE decl.usage = 'DECLARATION' -- Leave out calls to Oracle internals
                          AND decl.owner != 'SYS')
          SELECT pld.owner declaring_owner,
                 pld.object_type declaring_object_type,
                 pld.object_name declaring_object_name,
                 pld.line declaring_line,
                 pld.TYPE declared_type,
                 pld.name declared_name,
                 pld.overload,
                 i.owner referring_owner,
                 i.object_type referring_object_type,
                 i.object_name referring_object_name,
                 i.usage,
                 CASE
                    WHEN    i.object_name != pld.object_name
                         OR i.owner != pld.owner
                    THEN
                       'External'
                 END
                    external_call,
                 i.line referring_line,
                 i.col referring_column,
                 (SELECT s.text
                    FROM all_source s
                   WHERE     s.owner = i.owner
                         AND s.TYPE = i.object_type
                         AND s.name = i.object_name
                         AND s.line = i.line)
                    referring_source_text,
                 pld.signature
            FROM pl_declaration pld
                 JOIN all_identifiers i ON i.signature = pld.signature) pl_id
   WHERE     -- leave out obvious references to a package, these are always redundant
             -- because you can not just refer to a packagewithout referring
             -- to a subprogram, constant etc.; these are included in the query result anyway
             NOT (pl_id.declared_type = 'PACKAGE' AND pl_id.usage = 'REFERENCE')
         -- leave out obvious self references to a type, these are always redundant
         AND NOT (    pl_id.declared_type = 'TYPE'
                  AND pl_id.usage = 'REFERENCE'
                  AND pl_id.declaring_owner = pl_id.referring_owner
                  AND pl_id.declaring_object_name = pl_id.referring_object_name
                  AND pl_id.declaring_object_type = 'TYPE'
                  AND pl_id.referring_object_type = 'TYPE')
         AND (   pl_id.declaring_owner IN (USER,
                                           SYS_CONTEXT ('userenv',
                                                        'current_schema'))
              OR pl_id.referring_owner IN (USER,
                                           SYS_CONTEXT ('userenv',
                                                        'current_schema')))
ORDER BY pl_id.declaring_owner,
         CASE pl_id.declaring_object_type
            WHEN 'PACKAGE' THEN 10
            WHEN 'PACKAGE BODY' THEN 20
            WHEN 'TYPE' THEN 30
            WHEN 'TYPE BODY' THEN 40
            WHEN 'FUNCTION' THEN 50
            WHEN 'PROCEDURE' THEN 60
            WHEN 'TRIGGER' THEN 70
            ELSE 900
         END,
         pl_id.declaring_object_name,
         CASE WHEN pl_id.declared_type LIKE 'FORMAL%' THEN 20 ELSE 10 END,
         CASE pl_id.declared_type
            WHEN 'PACKAGE' THEN 10
            WHEN 'PACKAGE BODY' THEN 15
            WHEN 'TYPE' THEN 20
            WHEN 'TYPE BODY' THEN 20
            WHEN 'FUNCTION' THEN 20
            WHEN 'PROCEDURE' THEN 20
            WHEN 'CURSOR' THEN 30
            WHEN 'REFCURSOR' THEN 40
            WHEN 'CONSTANT' THEN 50
            WHEN 'NESTED TABLE' THEN 60
            WHEN 'VARRAY' THEN 70
            WHEN 'RECORD' THEN 80
            WHEN 'VARIABLE' THEN 90
            ELSE 500
         END,
         pl_id.declared_type,
         pl_id.declared_name,
         pl_id.overload,
         pl_id.signature,
         CASE pl_id.usage
            WHEN 'DECLARATION' THEN 10
            WHEN 'DEFINITION' THEN 20
            WHEN 'CALL' THEN 600
            WHEN 'ASSIGNMENT' THEN 600
            WHEN 'REFERENCE' THEN 900
            ELSE 500
         END,
         pl_id.external_call ASC NULLS FIRST,
         pl_id.usage,
         pl_id.referring_owner,
         pl_id.referring_object_type,
         pl_id.referring_object_name,
         pl_id.referring_line,
         pl_id.referring_column
/


Comments

  1. Nice list, I hope it gets expanded as more people add their ideas. With regard to #2, one must keep in mind the dangers of measuring productivity - e.g. when you reward someone based on code reuse, there is a real risk that you substitute the desired behaviour (reusing code *appropriately*) for undesired behaviour (reusing code whenever possible, even when not needed or appropriate). Never forget the bug bounty fiasco: http://thedailywtf.com/articles/The-Defect-Black-Market

    ReplyDelete

Post a Comment

Popular posts from this blog

Running out of PGA memory with MULTISET ops? Watch out for DISTINCT!

A PL/SQL team inside Oracle made excellent use of nested tables and MULTISET operators in SQL, blending data in tables with procedurally-generated datasets (nested tables).  All was going well when they hit the dreaded: ORA-04030: out of process memory when trying to allocate 2032 bytes  They asked for my help.  The error occurred on this SELECT: SELECT  *    FROM header_tab trx    WHERE (generated_ntab1 SUBMULTISET OF trx.column_ntab)       AND ((trx.column_ntab MULTISET             EXCEPT DISTINCT generated_ntab2) IS EMPTY) The problem is clearly related to the use of those nested tables. Now, there was clearly sufficient PGA for the nested tables themselves. So the problem was in executing the MULTISET-related functionality. We talked for a bit about dropping the use of nested tables and instead doing everything in SQL, to avoid the PGA error. That would, however require lots of wo...

How to Pick the Limit for BULK COLLECT

This question rolled into my In Box today: In the case of using the LIMIT clause of BULK COLLECT, how do we decide what value to use for the limit? First I give the quick answer, then I provide support for that answer Quick Answer Start with 100. That's the default (and only) setting for cursor FOR loop optimizations. It offers a sweet spot of improved performance over row-by-row and not-too-much PGA memory consumption. Test to see if that's fast enough (likely will be for many cases). If not, try higher values until you reach the performance level you need - and you are not consuming too much PGA memory.  Don't hard-code the limit value: make it a parameter to your subprogram or a constant in a package specification. Don't put anything in the collection you don't need. [from Giulio Dottorini] Remember: each session that runs this code will use that amount of memory. Background When you use BULK COLLECT, you retrieve more than row with each fetch, ...

PL/SQL 101: Three ways to get error message/stack in PL/SQL

The PL/SQL Challenge quiz for 10 September - 16 September 2016 explored the different ways you can obtain the error message / stack in PL/SQL. Note: an error stack is a sequence of multiple error messages that can occur when an exception is propagated and re-raised through several layers of nested blocks. The three ways are: SQLERRM - The original, traditional and (oddly enough) not currently recommended function to get the current error message. Not recommended because the next two options avoid a problem which you are unlikely  to run into: the error stack will be truncated at 512 bytes, and you might lose some error information. DBMS_UTILITY.FORMAT_ERROR_STACK - Returns the error message / stack, and will not truncate your string like SQLERRM will. UTL_CALL_STACK API - Added in Oracle Database 12c, the UTL_CALL_STACK package offers a comprehensive API into the execution call stack, the error stack and the error backtrace.  Note: check out this LiveSQL script if...