DEV Community

Cover image for Method Abstraction and Stepwise Refinement
Paul Ngugi
Paul Ngugi

Posted on

Method Abstraction and Stepwise Refinement

The key to developing software is to apply the concept of abstraction. Method abstraction is achieved by separating the use of a method from its implementation. The client can use a method without knowing how it is implemented. The details of the implementation are encapsulated in the method and hidden from the client who invokes the method. This is also known as information
hiding
or encapsulation. If you decide to change the implementation, the client program will not be affected, provided that you do not change the method signature. The implementation of the method is hidden from the client in a “black box,” as shown below:

Image description

You have already used the System.out.print method to display a string and the max method to find the maximum number. You know how to write the code to invoke these methods in your program, but as a user of these methods, you are not required to know how they are implemented.

The concept of method abstraction can be applied to the process of developing programs. When writing a large program, you can use the divide-and-conquer strategy, also known as stepwise refinement, to decompose it into subproblems. The subproblems can be further
decomposed into smaller, more manageable problems.

Suppose you write a program that displays the calendar for a given month of the year. The program prompts the user to enter the year and the month, then displays the entire calendar for the month, as shown in the following sample run.

Image description

Let us use this example to demonstrate the divide-and-conquer approach.

Top-Down Design

How would you get started on such a program? Would you immediately start coding? Beginning programmers often start by trying to work out the solution to every detail. Although details are important in the final program, concern for detail in the early stages may block
the problem-solving process. To make problem solving flow as smoothly as possible, this example begins by using method abstraction to isolate details from design and only later implements the details.

For this example, the problem is first broken into two subproblems: get input from the user and print the calendar for the month. At this stage, you should be concerned with what the subproblems will achieve, not with how to get input and print the calendar for the
month. You can draw a structure chart to help visualize the decomposition of the problem.

Image description

The structure chart shows that the printCalendar problem is divided into two subproblems, readInput and printMonth in (a), and that printMonth is divided into two smaller subproblems, printMonthTitle and printMonthBody in (b).

You can use Scanner to read input for the year and the month. The problem of printing the calendar for a given month can be broken into two subproblems: print the month title and print the month body, as shown in b above. The month title consists of three lines: month and year, a dashed line, and the names of the seven days of the week. You need to get the month name (e.g., January) from the numeric month (e.g., 1). This is accomplished in getMonthName, as shown below in a:

Image description

(a) To printMonthTitle, you need getMonthName. (b) The printMonthBody problem is refined into several smaller problems.

In order to print the month body, you need to know which day of the week is the first day of the month (getStartDay) and how many days the month has (getNumberOfDaysInMonth), as shown in b. For example, December 2013 has 31 days, and December 1, 2013, is a Sunday.

How would you get the start day for the first date in a month? There are several ways to do so. For now, we’ll use an alternative approach. Assume you know that the start day for January 1, 1800, was a Wednesday (START_DAY_FOR_JAN_1_1800 = 3). You could compute the total number of days (totalNumberOfDays) between January 1, 1800, and the first date of the calendar month. The start day for the calendar month is (totalNumberOfDays + START_DAY_FOR_JAN_1_1800) % 7, since every week has seven days. Thus, the getStartDay problem can be further refined as getTotalNumberOfDays, as shown in below in a:

Image description

(a) To getStartDay, you need getTotalNumberOfDays. (b) The
getTotalNumberOfDays problem is refined into two smaller problems.

To get the total number of days, you need to know whether the year is a leap year and the number of days in each month. Thus, getTotalNumberOfDays can be further refined into two subproblems: isLeapYear and getNumberOfDaysInMonth, as shown in b. The complete structure chart is shown:

Image description

Top-Down and/or Bottom-Up Implementation

Now we turn our attention to implementation. In general, a subproblem corresponds to a method in the implementation, although some are so simple that this is unnecessary. You would need to decide which modules to implement as methods and which to combine with other methods. Decisions of this kind should be based on whether the overall program will be easier to read as a result of your choice. In this example, the subproblem readInput can be
simply implemented in the main method.

You can use either a “top-down” or a “bottom-up” approach. The top-down approach implements one method in the structure chart at a time from the top to the bottom. Stubs—a simple but incomplete version of a method—can be used for the methods waiting to be implemented. The use of stubs enables you to quickly build the framework of the program. Implement the main method first, and then use a stub for the printMonth method. For example, let printMonth display the year and the month in the stub. Thus, your program may begin like this:

public class PrintCalendar {
 /** Main method */
public static void main(String[] args) {
 Scanner input = new Scanner(System.in);
 // Prompt the user to enter year
 System.out.print("Enter full year (e.g., 2012): ");
int year = input.nextInt();
 // Prompt the user to enter month
 System.out.print("Enter month as a number between 1 and 12: ");
int month = input.nextInt();
 // Print calendar for the month of the year
printMonth(year, month);
 }
 /** A stub for printMonth may look like this */
public static void printMonth(int year, int month){
 System.out.print(month + " " + year);
 }
 /** A stub for printMonthTitle may look like this */
public static void printMonthTitle(int year, int month){
 }
 /** A stub for getMonthBody may look like this */
public static void printMonthBody(int year, int month){
 }
/** A stub for getMonthName may look like this */
public static String getMonthName(int month) {
return "January"; // A dummy value
 }
 /** A stub for getStartDay may look like this */
public static int getStartDay(int year, int month) {
return 1; // A dummy value
 }
 /** A stub for getTotalNumberOfDays may look like this */
public static int getTotalNumberOfDays(int year, int month) {
return 10000; // A dummy value
 }
 /** A stub for getNumberOfDaysInMonth may look like this */
public static int getNumberOfDaysInMonth(int year, int month) {
return 31; // A dummy value
 }
 /** A stub for isLeapYear may look like this */
public static Boolean isLeapYear(int year) {
return true; // A dummy value
 }
}
Enter fullscreen mode Exit fullscreen mode

Compile and test the program, and fix any errors. You can now implement the printMonth method. For methods invoked from the printMonth method, you can again use stubs.

The bottom-up approach implements one method in the structure chart at a time from the bottom to the top. For each method implemented, write a test program, known as the driver, to test it. The top-down and bottom-up approaches are equally good: Both approaches implement methods incrementally, help to isolate programming errors, and make debugging easy. They can be used together.

Implementation Details

The isLeapYear(int year) method can be implemented using the following code:

return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
Enter fullscreen mode Exit fullscreen mode

Use the following facts to implement getTotalNumberOfDaysInMonth(int year, int month):

  • January, March, May, July, August, October, and December have 31 days.
  • April, June, September, and November have 30 days.
  • February has 28 days during a regular year and 29 days during a leap year. A regular year, therefore, has 365 days, a leap year 366 days. To implement getTotalNumberOfDays(int year, int month), you need to compute the total number of days (totalNumberOfDays) between January 1, 1800, and the first day of the calendar month. You could find the total number of days between the year 1800 and the calendar year and then figure out the total number of days prior to the calendar month in the calendar year. The sum of these two totals is totalNumberOfDays.

To print a body, first pad some space before the start day and then print the lines for every week.
The complete program is given:

package demo;
import java.util.Scanner;

public class PrintCalendar {

    public static void main(String[] args) {
        Scanner input = new Scanner(System.in);

        // Prompt the user to enter year
        System.out.print("Enter full year (e.g., 2012): ");
        int year = input.nextInt();

        // Prompt the user to enter month
        System.out.print("Enter month as a number between 1 and 12: ");
        int month = input.nextInt();

        // Print calendar for the month of the year
        printMonth(year, month);
    }

    /** Print the calendar for a month in a year */
    public static void printMonth(int year, int month) {
        // Print the headings of the calendar
        printMonthTitle(year, month);

        // Print the body of the calendar
        printMonthBody(year, month);
    }

    /** Print the month title, e.g., March 2012 */
    public static void printMonthTitle(int year, int month) {
        System.out.println("         " + getMonthName(month) + " " + year);
        System.out.println("-----------------------------");
        System.out.println(" Sun Mon Tue Wed Thu Fri Sat");
    }

    /** Get the English name for the month */
    public static String getMonthName(int month) {
        String monthName = "";
        switch(month) {
        case 1: monthName = "January"; break;
        case 2: monthName = "February"; break;
        case 3: monthName = "March"; break;
        case 4: monthName = "April"; break;
        case 5: monthName = "May"; break;
        case 6: monthName = "June"; break;
        case 7: monthName = "July"; break;
        case 8: monthName = "August"; break;
        case 9: monthName = "September"; break;
        case 10: monthName = "October"; break;
        case 11: monthName = "November"; break;
        case 12: monthName = "December"; break;
        }

        return monthName;
    }

    /** Print month body */
    public static void printMonthBody(int year, int month) {
        // Get start day of the week for the first date in the month
        int startDay = getStartDay(year, month);

        // Get number of days in the month
        int numberOfDaysInMonth = getNumberOfDaysInMonth(year, month);

        // Pad space before the first day of the month
        int i = 0;
        for(i = 0; i < startDay; i++)
            System.out.print("    ");

        for(i = 1; i <= numberOfDaysInMonth; i++) {
            System.out.printf("%4d", i);

            if((i + startDay) % 7 == 0)
                System.out.println();
        }

        System.out.println();
    }

    /** Get the start day of month/1/year */
    public static int getStartDay(int year, int month) {
        final int START_DAY_FOR_JAN_1_1800 = 3;
        // Get total number of days from 1/1/1800 to month/1/year
        int totalNumberOfDays = getTotalNumberOfDays(year, month);

        // return the start day for month/1/year
        return(totalNumberOfDays + START_DAY_FOR_JAN_1_1800) % 7;
    }

    /** Get the total number of days since January 1, 1800 */
    public static int getTotalNumberOfDays(int year, int month) {
        int total = 0;

        // Get the total days from 1800 to 1/1/year
        for(int i = 1800; i < year; i++)
            if(isLeapYear(i))
                total = total + 366;
            else
                total = total + 365;

        // Add days from Jan to the month prior to the calendar month
        for(int i = 1; i < month; i++)
            total = total + getNumberOfDaysInMonth(year, i);

        return total;
    }

    /** Get the number of days in a month */
    public static int getNumberOfDaysInMonth(int year, int month) {
        if(month == 1 || month == 3 || month == 5 || month == 7 || month == 8 || month == 10 || month == 12)
            return 31;

        if(month == 4 || month == 6 || month == 9 || month == 11)
            return 30;

        if(month == 2) return isLeapYear(year) ? 29 : 28;

        return 0; // If month is incorrect
    }

    /** Determine if it is a leap year */
    public static boolean isLeapYear(int year) {
        return year % 400 == 0 || (year % 4 == 0 && year % 100 != 0);
    }

}

Enter fullscreen mode Exit fullscreen mode

The program does not validate user input. For instance, if the user enters either a month not in the range between 1 and 12 or a year before 1800, the program displays an erroneous calendar. To avoid this error, add an if statement to check the input before printing the calendar.

This program prints calendars for a month but could easily be modified to print calendars for a whole year. Although it can print months only after January 1800, it could be modified to print months before 1800.

Benefits of Stepwise Refinement

Stepwise refinement breaks a large problem into smaller manageable subproblems. Each subproblem can be implemented using a method. This approach makes the program easier to write, reuse, debug, test, modify, and maintain.

Simpler Program

The print calendar program is long. Rather than writing a long sequence of statements in one method, stepwise refinement breaks it into smaller methods. This simplifies the program and makes the whole program easier to read and understand.

Reusing Methods

Stepwise refinement promotes code reuse within a program. The isLeapYear method is defined once and invoked from the getTotalNumberOfDays and getNumberOfDayInMonth methods. This reduces redundant code.

Easier Developing, Debugging, and Testing

Since each subproblem is solved in a method, a method can be developed, debugged, and tested individually. This isolates the errors and makes developing, debugging, and testing easier. When implementing a large program, use the top-down and/or bottom-up approach. Do not write the entire program at once. Using these approaches seems to take more development time (because you repeatedly compile and run the program), but it actually saves time and makes debugging easier.

Better Facilitating Teamwork

When a large problem is divided into subprograms, subproblems can be assigned to different programmers. This makes it easier for programmers to work in teams.

Top comments (0)