Previous: Name Parameters, Up: Refactoring


5.9.3 Extract Subprogram

This refactoring is used to move some code from one place to a separate subprogram. The goal is to simplify the original subprogram, by moving part of its code elsewhere.

Here is an example from the "Refactoring" book. The refactoring will take place in the body of the package pkg.adb, but the spec is needed so that you can compile the source code (a preliminary step mandatory before you can refactor the code).

     pragma Ada_05;
     
     with Ada.Containers.Indefinite_Doubly_Linked_Lists;
     with Ada.Strings.Unbounded;
     
     package Pkg is
     
        type Order is tagged null record;
        function Get_Amount (Self : Order) return Integer;
     
        package Order_Listsis new
           Ada.Containers.Indefinite_Doubly_Linked_Lists (Order);
     
        type Invoice is tagged record
           Orders : Order_Lists.List;
           Name   : Ada.Strings.Unbounded.Unbounded_String;
        end record;
     
        procedure Print_Owing (Self : Invoice);
     
     end Pkg;

The initial implementation for this code is given by the following code:

     pragma Ada_05;
     with Ada.Strings.Unbounded;  use Ada.Strings.Unbounded;
     with Ada.Text_IO;            use Ada.Text_IO;
     
     package body Pkg is
        use Order_Lists;
     
        ----------------
        -- Get_Amount --
        ----------------
     
        function Get_Amount (Self : Order) return Integer is
        begin
           return 0;
        end Get_Amount;
     
        -----------------
        -- Print_Owing --
        -----------------
     
        procedure Print_Owing (Self : Invoice) is
           E : Order_Lists.Cursor := First (Self.Orders);
           Outstanding : Natural := 0;
           Each : Order;
        begin
           --  <<< line 30
           --  Print Banner
     
           Put_Line ("********************************");
           Put_Line ("***** Customer Owes         ****");
           Put_Line ("********************************");  --  << line 35
     
           --  Calculate Outstanding
     
           while Has_Element (E) loop
              Each := Element (E);
              Outstanding := Outstanding + Each.Get_Amount;
              Next (E);
           end loop;
     
           --  Print Details
     
           Put_Line ("Name: " & To_String (Self.Name));
           Put_Line ("Outstanding:" & Outstanding'Img);
        end Print_Owing;
     end Pkg;

The procedure Print_Owing is too long and does several independent actions. We will perform a series of three successive refactoring steps to extract the code and move it elsewhere.

The first is the code that prints the banner. Moving it is easy, since this code does not depend on any context. We could just do a copy-paste, but then we would have to create the new subprogram. Instead, we select lines 30 to 35, and then select the contextual menu Refactoring/Extract Subprogram. GPS will then automatically change Print_Owing and create a new procedure Print_Banner (the name is specified by the user, GPS does not try to guess it). Also, since the chunk of code that is extracted starts with a comment, GPS automatically uses that comment as the documentation for the new subprogram. Here is part of the resulting file:

     package body Pkg is
     
        procedure Print_Banner;
        --  Print Banner
     
        ------------------
        -- Print_Banner --
        ------------------
     
        procedure Print_Banner is
        begin
           Put_Line ("********************************");
           Put_Line ("***** Customer Owes         ****");
           Put_Line ("********************************");
        end Print_Banner;
     
        ... (code not shown)
     
        procedure Print_Owing (Self : Invoice) is
           E : Order_Lists.Cursor := First (Self.Orders);
           Outstanding : Natural := 0;
           Each : Order;
        begin
           Print_Banner;
     
           --  Calculate Outstanding
     
           while Has_Element (E) loop
              Each := Element (E);
              Outstanding := Outstanding + Each.Get_Amount;
              Next (E);
           end loop;
     
           --  Print Details   <<< line  54
     
           Put_Line ("Name: " & To_String (Self.Name));
           Put_Line ("Outstanding:" & Outstanding'Img);  --  line 57
        end Print_Owing;
     
     end Pkg;

A more interesting example is when we want to extract the code to print the details of the invoice. This code depends on one local variable and the parameter to Print_Owing. When we select lines 54 to 57 and extract it into a new Print_Details subprogram, we get the following result. GPS automatically decides which variables to extract, and whether they should become parameters of the new subprogram, or local variables. In the former case, it will also automatically decide whether to create "in", "out" or "in out" parameters. If there is a single "out" parameter, it will automatically create a function rather than a procedure.

GPS will use, for the parameters, the same name that was used for the local variable. Very often, it will make sense to recompile the new version of the source, and then apply the Rename Entity refactoring to have more specific names for the parameters, or the Name Parameters refactoring so that the call to the new method uses named parameters to further clarify the code.

        ... code not shown
     
        procedure Print_Details
          (Self : Invoice'Class;
           Outstanding : Natural);
        --  Print Details
     
        -------------------
        -- Print_Details --
        -------------------
     
        procedure Print_Details
          (Self : Invoice'Class;
           Outstanding : Natural)
        is
        begin
           Put_Line ("Name: " & To_String (Self.Name));
           Put_Line ("Outstanding:" & Outstanding'Img);
        end Print_Details;
     
        procedure Print_Owing (Self : Invoice) is
           E : Order_Lists.Cursor := First (Self.Orders);
           Outstanding : Natural := 0;
           Each : Order;
        begin
           Print_Banner;
     
           --  Calculate Outstanding
     
           while Has_Element (E) loop
              Each := Element (E);
              Outstanding := Outstanding + Each.Get_Amount;
              Next (E);
           end loop;
     
           Print_Details (Self, Outstanding);
        end Print_Owing;

Finally, we want to extract the code that computes the outstanding amount. When this code is moved, the variables E and Each become useless in Print_Owing and are moved into the new subprogram (which we will call Get_Outstanding. Here is the result of that last refactoring (the initial selection should include the blank lines before and after the code, to keep the resulting Print_Owing simpler). GPS will automatically ignore those blank lines.

        ... code not shown
     
        procedure Get_Outstanding (Outstanding : in out Natural);
        --  Calculate Outstanding
     
        ---------------------
        -- Get_Outstanding --
        ---------------------
     
        procedure Get_Outstanding (Outstanding : in out Natural) is
           E : Order_Lists.Cursor := First (Self.Orders);
           Each : Order;
        begin
           while Has_Element (E) loop
              Each := Element (E);
              Outstanding := Outstanding + Each.Get_Amount;
              Next (E);
           end loop;
        end Get_Outstanding;
     
        procedure Print_Owing (Self : Invoice) is
           Outstanding : Natural := 0;
        begin
           Print_Banner;
           Get_Outstanding (Outstanding);
           Print_Details (Self, Outstanding);
        end Print_Owing;

Note that the final version of Print_Owing is not perfect. For instance, passing the initial value 0 to Get_Outstanding is useless, and in fact that should probably be a function with no parameter. But GPS already saves a lot of time and manipulation.

Finally, a word of caution: this refactoring does not check that you are giving a valid input. For instance, if the text you select includes a declare block, you should always include the full block, not just a part of it (or select text between begin and end). Likewise, GPS does not expect you to select any part of the variable declarations, just the code.