Monday, October 13, 2014

OO Programming with ABAP Objects: Encapsulation

In my OO Programming with ABAP Objects: Classes and Objects blog entry, I introduced the concept of classes and objects, showing you how to create and use both in ABAP Objects. If this is your first exposure to OO programming, you might be wondering what's so great about it. After all, on the surface, a class looks a lot like a function group or subroutine pool. In this blog, we will dig deeper to see where classes differentiate themselves from procedural concepts.


What's Wrong with the Procedural Approach?


One common misconception about OO programming is that it is different from procedural programming in every way - not true. There are many important lessons to be taken from the procedural approach. However, there are certain limitations of this approach that influenced early researchers in their design of the OO paradigm. These limitations are best described with an example. Let's imagine that you want to build a code library to make it easier to work with dates. To do so, you create a function group called ZDATE_API that contains various function modules to manipulate and display dates.

From a data perspective, you have a couple of options. Function groups allow you to define group-specific data objects that can be utilized within function modules (similar to the use of attributes in methods). However, in practice, such data objects can be difficult to use. This is because it is not possible to create "instances" of function groups. For example, in the ZDATE_API function group, I might define a global data object of type SCALS_DATE to keep track of the date being manipulated by the function modules. However, if I need to keep track of multiple dates in my program (e.g. created on date, changed on date, document date, etc.), I need to build an internal table to keep track of each date "instance". I also need to keep track of the key to this table externally - otherwise I have no way of identifying a particular date instance. This limitation causes most developers to keep track of their data objects outside of the function group. If you think back to the last time you tried to call a BAPI and all the data objects you had to define beforehand, you'll appreciate what I mean. We'll explore some of the implications of this approach to data in a moment.

Assuming that we elected to keep track of data separately, let's look at what a function module might look like in our ZDATE_API function group. For the purposes of this discussion, we'll keep it simple and just look at a function module used to set the "day" value of the date:

FUNCTION z_date_set_day.
* Local Interface IMPORTING VALUE (lv_day) TYPE I
*                 CHANGING (cs_date) TYPE SCALS_DATE
*                 EXCEPTIONS invalid_date
DATA: lv_month_end TYPE i. "Last Day of Month

CASE cs_date-month.
WHEN 1.
lv_month_end = 31.
WHEN 2.
...
ENDCASE.

IF iv_day LT 1 OR iv_day GT lv_month_end.
RAISE invalid_date.
ELSE.
cs_date-day = iv_day.
ENDIF.
ENDFUNCTION.

This contrived example simply ensures that we initialize the "day" value of a date to a proper value. Clearly, there are probably better ways to implement something like this, but the point is that we have defined some business rules inside of a function module that is part of an API designed to simplify the way that we work with dates.

Now that we have our function group in place, let's imagine that you are asked to troubleshoot a program that is using your function group to display dates in various formats but it is outputting them incorrectly (e.g., 02/31/2009). At first, you might think that there is a problem with Z_DATE_SET_DAY as the day value is incorrect. However, after further review, you discover that the invalid assignment was made in the program itself. After all, there's nothing stopping a program from changing the day value of a local variable directly. To that program, the day component of the SCALS_DATE structure is nothing more than a 2 digit numeric character with a valid range of 00-99 - the semantic meaning of the day value is defined within the confines of our ZDATE_API function group.

Beyond the issue of data corruption, think about how clumsy the typical function group is. The separation of data and behavior in the ZDATE_API function group limits the usefulness of the abstraction, making the use of the API awkward as we have to pass the SCALS_DATE object back and forth between function calls. This becomes something more than a nuisance when the library expands. For example, think of the impact of expanding this API to support timestamps. A better approach would be to hide this data such that callers don't have to worry about it.


Hiding the Implementation


In programming terms, a function group like ZDATE_API is an abstract data type (or ADT). As the term suggests, an ADT abstracts a particular concept into an easy-to-use data type. Ideally, the creation of a date API would imply that we no longer need to worry about how dates work. Rather, we can leverage the hard work (and testing) that went into the creation of the date API and focus in on other important tasks. However, this is difficult to do if the ADT is not wellencapsulated. The term encapsulate implies that we're combining something into an enclosure (or capsule). In the case of an ADT, we're grouping data and behavior together. Moreover, encapsulation also suggests that we are protecting these resources from external tampering. Initially, most programmers balk at this, preferring to have complete control over all parts of the code. The problem with this is that taking control of any code also implies that you assume some of the risk for ensuring that it operates correctly. In our date example, look at how problematic it was to allow external programs to modify the date structure. Ideally, we would prefer that any modifications to this structure pass through business rule checkpoints to make sure that data is not corrupted.

Encapsulation is a good engineering practice used in many disciplines. For instance, you don't have to know how a car works in order to drive it. Of course, it does help if you have power steering, automatic transmission, etc. These features represent the "interface" that users utilize to interact with the car. ADTs also have an interface (namely the signature of the function modules, method, etc.). Good interface design should make it easy to use an API without diminishing any of its capabilities. Another advantage of this engineering approach is that parts become more interchangeable. For example, imagine that a car manufacturer decides to redesign their fuel injector to improve performance. As long as the new fuel injector has the same interface (e.g. same "hookup"), the manufacturer can swap the parts and nobody's the wiser. In my next two blogs, I'll show how inheritance and polymorphism allows you to do some powerful things here with your OO programs.

Hopefully by now you agree that it is a good idea to group data and behavior together in a class. This, by itself, does not mean that a class is encapsulated. Remember, to achieve this, we must also place a protective capsule around the resources. OO languages such as ABAP Objects allow you to define component visibilities using access specifiers. The following class shows how to define these component visibilities:

CLASS lcl_visible DEFINITION.
PUBLIC SECTION.
DATA: x TYPE i.
PROTECTED SECTION.
DATA: y TYPE i.
PRIVATE SECTION.
DATA: z TYPE i.
ENDCLASS.

As you can see, the components of class lcl_visible are partitioned into three distinct sections: the PUBLIC SECTION, the PROTECTED SECTION, and the PRIVATE SECTION. Components defined in the PUBLIC SECTION are visible everywhere. Components defined in the PRIVATE SECTION are only visible within the class itself. Thus, the only place that you can access the attribute "z" would be inside of an instance method of class lcl_visible. We'll talk about the PROTECTED SECTION when we talk about inheritance.

In this way, we can reproduce our date API in a class like this:

CLASS lcl_date DEFINITION.
PUBLIC SECTION.
METHODS: set_month IMPORTING im_month TYPE i
EXCEPTIONS invalid_date,
set_day   IMPORTING im_day TYPE i
EXCEPTIONS invalid_date,
set_year  IMPORTING im_year TYPE i
EXCEPTIONS invalid_date.
PRIVATE SECTION.
DATA: date TYPE scals_date.
ENDCLASS.

CLASS lcl_date IMPLEMENTATION.
METHOD set_month.
"Implementation of method set_month goes here...
ENDMETHOD.

METHOD set_day.
DATA: lv_month_end TYPE i. "Last Day of Month

CASE date-month.
WHEN 1.
lv_month_end = 31.
WHEN 2.
...
ENDCASE.

IF iv_day LT 1 OR iv_day GT lv_month_end.
RAISE invalid_date.
ELSE.
date-day = iv_day.
ENDIF.
ENDMETHOD.

METHOD set_year.
"Implementation of method set_year goes here...
ENDMETHOD.
ENDCLASS.

Notice that the "date" attribute is defined in the PRIVATE SECTION of the class. Now, any accesses to the "date" attribute must go through public instance methods. This ensures that an external program cannot accidentally (or purposefully) modify the value incorrectly. It also makes the date API easier to use as client programs now only need to define dates like this:

DATA: lr_date TYPE REF TO lcl_date.

With a simple API like this, it's not such a big deal. However, consider how you might implement an API for working with SAP Business Partners, etc. Having the objects keep track of all that data simplifies API use considerably.


Reflections


As you have seen in this blog, encapsulation is a good engineering practice that you can use to develop qualityreusable class libraries. I emphasize reusable here to demonstrate an important point. One of the most common reasons why a library is not reused is because it has too many dependencies. Developing classes using implementation hiding techniques forces you to enter into a mindset whereby classes begin to take on a certain amount of autonomy. In other words, you start to ask yourself questions like "What data does an object of my class need in order to do its job?", etc. Once you have figured this out, you design your interface in such a way as to only provide what the class with what it needs - reducing unnecessary dependencies along the way. The fewer dependencies a class has, the less likely things are to go wrong. And, as we will see in my next blog, it also allows us to expand our libraries in interesting ways without jeopardizing code that has been proven to work.
Post a Comment