ABAP OO Design part 7: Step 3 Reuse optimization example: Send Sales Order document by Email to Customer

This blog post describes a full “Reuse optimization” of the case “Sending Sales Order Document by Email to the Customer”

Because there are many steps, it has become a large blog post. Therefor a table of contents is added.

Table of contents

Introduction

Naming of classes and properties

Sales Order DP

Sales Order Document

Sales Order Email

Sales Order BO

Sales Order Output Processing

Conclusion

These actions are needed to send a sales order email.

  1. Creating an output message (NAST) during creation of the Sales Order.
  2. Create Print program which is needed for the Classic Output Message Framework (TNAPR/NAST).
  3. Read Sales Order Document Data.
  4. Create Sales Order Form
  5. Create Sales Order Adobe Document
  6. Create and Send Email

Based on these actions this concept class diagram was created.

This conceptual class diagram will now be optimized for reuse and the result will be this generalized class diagram.

You can click on the picture to enlarge it.

The orange classes are generic reusable classes, and the yellow classes are specific classes. So the next time we have to create an email with a document for another business object, all orange classes can be reused.

Below the generalization is executed for each class. A tip is to open this blog in two browsers and show the browsers side by side so that in one browser you show the class diagrams and in the other you can read the text below.

Without going too deep in naming, I want to mention that I base my naming on some of the CCTS naming conventions. In short the naming convention looks like is:

Class naming

[Classifier 3 ][Classifier 2 ][Classifier 1 ]Object type name

Examples:

  • Employee (No classifiers used)
  • External Employee (one classifier used)
  • Sales Manager External Employee (two classifiers used)

Property naming

[Class naming ][Classifier 3 ][Classifier 2 ][Classifier 1 ][Property name ]Property type

Examples:

  • Name (property name and type are both Name, than use only property type)
  • Last Name (one classifier)
  • Employee Last Name (Class naming + Property classifier 1 + Property type)

The property types are the CCTS Core Component Data Types.
For more information search on the internet for “Core Components Data Type Catalogue”.

Generalization is preferably started with the independent classes. Independent classes are classes which do not depend on a class in the diagram. These classes are often on the right side of the class diagram. So in this case the Sales Order Data Provider is a good class to start with.

The responsibility of this data provider class is to read “Sales Order Document Content” which is needed for the Adobe Form to generate the Adobe Document. 

Data provider classes can be split into the categories:

  • Entity
    Data provider reads data for one entity.
  • List
    Data provider returns a list of records.

The Sales Order DP data provider is an entity data provider.

Entity Data provider – read document content

The interface of the class can be determined by the way you to “talk” to the object. This is the same approach as Test Driven Development. First the class call is designed before designing the implementation.

The class call will be like this:

DATA(sales_order_bo_dp) = zsd_sales_order_bo_dp=>get_instance( '100000001' ). DATA(sls_ord_document_content) = sales_order_bo_dp->get_document_content( ).

OO design is about objects and an object is an instance of a class. For that reason the first line instantiates the class and than call instance methods. The data provider will be instantiated with the Sales Order number. The data provider class will have a private section instance variable to store this value. I do not model this in the class diagram, because it makes the class diagram too large and it is implicit.

After instantiation the method getDocumentContent() will return a deep structure variable with the needed content for the Adobe Form. The data type of the returning variable will be defined in the public section of this class.

Later on this class will be extended for retrieving content needed for the email.

Design considerations

Why not using the constructor?

For example:

DATA(sales_order_bo_dp) = NEW zsd_sales_order_bo_dp( '100000001' ). DATA(sls_ord_document_content) = sales_order_bo_dp->get_document_content( ).

In this case it does not matter whether a static factory method or a constructor method is used. In case you want more ways of constructing an object than you are stuck when using the constructor method. That’s why I prefer the static factory method.

Why not passing the Sales Order number to the getDocumentContent method?
DATA(sales_order_bo_dp) = NEW zsd_sales_order_bo_dp( ). DATA(sls_ord_document_content) = sales_order_bo_dp->get_document_content( '100000001' ).

Before making clear what the drawbacks are, let’s add method getEmailContent() to the class.

Option 1: Sales order number passed in the instance method

DATA(sales_order_bo_dp) = NEW zsd_sales_order_bo_dp( ). DATA(sls_ord_document_content) = sales_order_bo_dp->get_document_content( '100000001' ). DATA(sls_ord_email_content) = sales_order_bo_dp->get_email_content( '100000001' ).

Now the Sales order number has to be passed twice to the instance.

The example below is showing passing the Sales order number to the constructor.

Option 2: Sales order number passed in the constructor method

DATA(sales_order_bo_dp) = NEW zsd_sales_order_bo_dp( '100000001' ). DATA(sls_ord_document_content) = sales_order_bo_dp->get_document_content( ). DATA(sls_ord_email_content) = sales_order_bo_dp->get_email_content( ).

The design considerations:

  1. In option 1 the entity data provider is not about one entity. It is actually an “empty object”. Because it has no constructional data.
  2. If you want to check the Sales Order Number on existence first, than in option 2 it only has to be done in het constructor and in option 1 in both getter methods.
  3. If you want to buffer data for that single entity, than that is easier in option 2, because that object is about one Sales Order. For example if you call getDocumentContent first, than it can buffer data so getEmailContent does not have to read it again.
    Be always aware that methods should stay independent. If getEmailContent() is called first, than it has no buffered data and it should read the data from the database.

That’s why option 2 is the best option.

Reuse optimization

This class cannot be optimized, because the reading of the data (the data type) is specific coupled to the Sales Order. The deep structure document data type is also specific for the Sales Order Adobe Form. So the Adobe Form will get all the data in one deep structure. That’s also a nice design decision.

Texts based on a Standard text object

This step generalization step is not really needed, but it is a way I prefer. The language translation of Adobe Forms is not always working fine. Sometimes it gets messed up with the field GUIDs. I solved that problem by making use of Standard text objects and it gives me some more options like…

  • Standard text objects can be adjusted by non-developers.
  • It can have extra possibility to read texts from Data element field labels (SE11)
  • It can have extra possibility to read additional Standard texts (SO10)
  • It is easier to unit test than Standard objects added in the Adobe interface.

This is an example of a Standard text object with functions GET_SHORT_LABEL and GET_STANDARD_TEXT to get data from the database. These functions are interpreted by a custom class.

* sales_order_no = Sales order no.
* customer_order_no = GET_SHORT_LABEL( 'VBKD-BSTKD' ).
* payment_terms = The invoice has to be payed within <payment_term_days> days after receiving the invoice.
* footer = GET_STANDARD_TEXT( 'ZSD_SALES_DOCUMENT_FOOTER' ).

Document Standard Text Object class

The class Document Standard Text Object is designed to read the texts from the standard text object class and interpreting the texts.

TYPES: BEGIN OF t_document_content_texts, sales_order_no TYPE string, customer_order_no TYPE string, payment_terms TYPE string, footer TYPE string, END OF t_document_content_texts. DATA(document_texts_object) = NEW ztxd_doc_standard_text_object( text_name = 'ZSD_SLS_ORD_DOCUMENT_TEXTS' language_id = sls_ord_document_content-fields-head-language_id parameters = VALUE #( ( name = '<payment_term_days>' value = '30' ) ) ). DATA document_content_texts TYPE t_document_content_texts. document_texts_object->get_texts( CHANGING texts = document_content_texts ).

The method getTexts() contains parameter TEXTS which is of type ANY. The method could check whether all CONTENT_TEXTS structure fields are in the Standard text object and visa versa. Which means that if a field is added, this field has to be added to the Standard text objects for all necessary languages and to the structure T_DOCUMENT_CONTENT_TEXTS. So it is advised to create a unit test which tests the text object for all necessary languages.

The statement below…

DATA(sls_ord_document_content) = sales_order_bo_dp->get_document_content( ).

… will now return the Sales Order BO fields and the Sales Order Document texts. The content will be passed to the Adobe form.

Data type implementation

The public section of the data provider will contain something like this.

PUBLIC SECTION. TYPES: ... BEGIN OF t_document_content, fields TYPE t_content_fields, texts TYPE t_content_texts, END OF document_content.

The type t_document_content will be used in the Adobe Form interface and in the Adobe Form. This is the root structure will contain the fields and the texts.

The content fields will have only the Sales Order fields which are needed for the Adobe form. No extra fields are read from the database. This is for performance reasons, readability of the code and debugging of the code.

 ... BEGIN OF t_document_head, sales_order_no TYPE vbak-vbeln, ... END OF t_document_head, BEGIN OF t_content_fields, head TYPE t_document_head, items TYPE STANDARD TABLE OF ... END OF t_content_fields, ...

This is the structure for the content texts will contain all the labels and long texts of the Adobe Form.

 ... BEGIN OF t_content_texts, sales_order_no TYPE string, customer_order_no TYPE string, payment_terms TYPE string, footer TYPE string, END OF t_content_texts, ...

Query implementation

After the design, the implementation will follow. The implementation out of scope for this blog post, but here are some tips. To make the most out of query reuse, we can use CDS views instead of ABAP Open SQL. The standard CDS views already contain longer descriptive field names and also contain the composition and association relations.

We could go one step further with RAP (ABAP RESTful Programming model) by using statement READ ENTITY or calling the Service Binding by using the OData local proxy the get in one call the full deep structure. I haven’t used it yet, but it is worth to try I think. From an architectural point this is, I think, the best solution.

Conclusion

In OO the reading of document content is not executed in the print program but in the data provider class. The reason for this is that the code is now also callable by a RAP OData service for example without calling print program logic.

A document contains 3 parts:

  • Content fields
  • Content texts
  • Layout

For this design I decided to do the reading of both content parts (fields + texts) in the data provider because I want to have one ABAP deep structure with the fields and the texts for the Adobe Form.

This statement will return the Sales Order fields and the Document texts. This is useful for making a unit test for this class, so you can see in the result both content parts.

DATA(sales_order_bo_dp) = NEW zsd_sales_order_bo_dp( '100000001' ). DATA(sls_ord_document_content) = sales_order_bo_dp->get_document_content( ).

The next class is Sales Order Document. It’s task is “Create Sales Order Adobe Document”. It is not an independent class. It is dependent on Sales Order Data Provider for the document content. The data provider is already designed, so we can start designing this class.

DATA(sales_order_document) = zsd_sales_order_document( document_content = sls_ord_document_content ). DATA(pdf_binary) = sales_order_document->get_pdf_binary( ).

The construction is based on the Sales Order Document Content. And than it can generate the Adobe Document and gets the PDF binary data.

Reuse optimization

Step 1: Class generalization

The class can be optimized for reuse. The name of the Adobe Form is now “hard-coded” in the class. By applying parametrization the name of the Adobe Form name can be moved to a parameters called FormName. Now the class is renamed to a generic name called Adobe Document.
And the parameter Content must become a dynamic type. For example TYPE ANY or TYPE REF TO DATA.

DATA(sales_order_document) = zadb_adobe_document( form_name = 'ZSD_SALES_ORDER_DOCUMENT' content = sls_ord_document_content ). DATA(pdf_binary) = sales_order_document->get_pdf_binary( ).

This class is now a generic class. All Adobe Forms which will be called by this class will have one deep structure named content. I think that is a nice design decision.

Sales Order Document class

The class needs a form name and content.

DATA(sales_order_document) = zadb_adobe_document( form_name = 'ZSD_SALES_ORDER_DOCUMENT' content = sls_ord_document_content ). DATA(pdf_binary) = sales_order_document->get_pdf_binary( ).

The responsibility of this class is to “Create and Send Email”. The data needed for this is

  • Sender address, receiver address.
  • Attachment.

The call looks like this.

DATA(sales_order_email) = zsd_sales_order_email=>create( VALUE #( language_id = sales_order_document-head-language_id sender = VALUE #( name = 'Sales myCompany' address = 'sales@mycompany.nl' ) receivers = VALUE #( ( name = 'My Customer' address = 'you@mycustomer.nl' ) ) attachments = VALUE #( ( name = |{ sales_order_document-head-sales_order_no}.pdf| binary = sls_order_pdf_binary ) ) ) ). sales_order_email->send( ).

Step 1: decoupling the email content

This class can be generalized. The specific data can be retrieved from the class.
The specific data is:

  • Email subject
  • Email body
    • Layout
    • Content texts
    • Content fields

The requirement is to create a HTML email instead of a Plain text email.

XSLT programming is good option for creating a HTML. XSLT has well formed validation functionality for XML and HTML. It can loop through lists and has many more options for formatting text. So XSLT will be used for the email layout. The HTML <title> tag will contain the subject of the email.

The email texts will be stored in a Standard text object just like the texts for the Sales Order Document.

And the email content will be read by the data provider, which was already designed for the Sales Order Document.

The class will be renamed to a generic class name “Email”.

DATA(sales_order_email) = zeml_email=>create( VALUE #( language_id = sales_order_document-head-language_id sender = VALUE #( name = 'Sales myCompany' address = 'sales@mycompany.nl' ) receivers = VALUE #( ( name = 'My Customer' address = 'you@mycustomer.nl' ) ) layout_xslt_name = 'ZSD_SALES_ORDER_EMAIL' content = sales_order_email_content attachments = VALUE #( ( name = |{ sales_order_document-head-sales_order_no}.pdf| binary = sls_order_pdf_binary ) ) ) ). sales_order_email->send( ).

Entity Data provider – read email content

The email content will be read by the Sales Order Entity Data Provider.

DATA(sales_order_bo_dp) = NEW zsd_sales_order_bo_dp( '100000001' ). DATA(sls_ord_email_content) = sales_order_bo_dp->get_email_content( ).

Email standard text object

The email texts will be read by the Document standard text object class in the Sales Order Entity Data Provider.

TYPES: BEGIN OF t_email_texts, saluation TYPE string, .... END OF t_email_texts. DATA(email_text_object) = NEW ztxd_doc_standard_text_object( text_name = 'ZSD_SLS_ORD_EMAIL_TEXTS' language_id = sls_ord_document_content-fields-head-language_id parameters = VALUE #( ( name = '<payment_term_days>' value = '30' ) ) ). DATA email_texts TYPE t_email_texts. email_texts_object->get_texts( CHANGING texts = email_texts ).

Step 2: decoupling email class from XSLT logic

The email class is now coupled to the XSLT logic. To make a class more generic it should not depend on other classes.

The email class is now coupled to the XSLT program. So we have to split this class into:

  • Extended Email
  • Email

The current class email class will be renamed to Extended class and a new class Email is created.
Tip: Do not rename Email to “Email Framework”. It is a framework, but the object is not a framework, it is an email. You might also call it a XSLT Email or Framework Email.

The Extended Email is now a Facade class. A Facade class also a relative name for a class which applies the Facade pattern. Facade pattern is a “front-facing interface masking more complex underlying or structural code”.

Now the new generic Email class must be generic independent from the XSLT functionality. The result is this call:

DATA(sales_order_email) = zeml_email=>create( VALUE #( language_id = sales_order_document-head-language_id sender = VALUE #( name = 'Sales myCompany' address = 'sales@mycompany.nl' ) receivers = VALUE #( ( name = 'My Customer' address = 'you@mycustomer.nl' ) ) body_type = zeml_email=>body_types-html content = VALUE #( subject_text = |Sales order 100000001| body_text = |<h1>Sales order 100000001<h1><p>...the text...<p>| ) attachments = VALUE #( ( name = |{ sales_order_document-head-sales_order_no}.pdf| binary = sls_order_pdf_binary ) ) ) ). sales_order_email->send( ).
  • Parameter CONTENT is added as a replacement of the XSLT program and content parameter.
  • Parameter BODY_TYPE is added for creating plain text emails and HTML emails.

XSLT Email Content

From the Extended email also the generation of the content by the XSLT can be retrieved.
Let’s call it “XSLT Email Content”.

The email content is based on:

  • Layout -> stored in XSLT program
  • Sales Order Email content -> read by Sales order data provider
DATA(xslt_email_content) = zeml_xslt_email_content=>create_instance( content = sls_ord_email_content xslt_name = 'ZSD_SALES_ORDER_EMAIL' ). "email_content_text structure contains the fields "SUBJECT_TEXT and BODY_TEXT
DATA(email_content_text) = xslt_email_content->get_text( ).

Implementation

The CALL TRANSFORMATION will look like this.

 CALL TRANSFORMATION (me->properties-xslt_template_name) SOURCE content = content settings = settings syst = sy RESULT XML email_html_content_string.

This is an example of an XSLT which generates HTML. The <title> will used for the email subject.

<xsl:transform version="1.0" ...> <xsl:template match="asx:values"> <html> <head> <title>Subject text of the email <xsl:value-of select="CONTENT/FIELDS/INVOICE_NO"/></title></head> <body> <!-- Saluation --> <p> <xsl:value-of select="CONTENT/TEXTS/SALUATION"/> <xsl:text> </xsl:text> <xsl:value-of select="CONTENT/FIELDS/VENDOR_NAME"/>, </p> <!-- the rest of the text --> </body> </html> </xsl:template>
</xsl:transform>

Generalizing XSLT Email Content class

The public section of the “XSLT Email Content” class can be generalized by making an Interface “Email Content Interface”.

If for example a plain text email is required, than the layout could be stored in a Standard text object instead of a XSLT. In that case the “Email Content Interface” can be realized by a new class called “Standard Text Email Content”. So for being prepared for the future the interface “Email Content Interface” is added to the class diagram.

The Sales Order controls the creation of the email. It can also be seen as a Facade class, which hides all the complexity of creating a Sales Order Document Email.

It makes use of the Class Interface “Output Message Process – External send” and implements it’s only method execute().

The actions are

  • Read document content
  • Read email content
  • Create and send email
DATA(sales_order_bo) = zsd_sales_order_bo=>get_instance( '10000001' ). sales_order_bo->zopt_outp_man_external_send~execute( output_message_data = VALUE #( message_type = tnapr-kschl form_name = tnapr-fnam language_id = nast-spras partner_no = nast-parnr address_no = nast-adrnr ... ).

The Output Message framework has more Mediums than External send [nacha 5]. It has also 1 Print output, 7 Simple email, 6 EDI, 8 Special function and more. But I won’t add extra methods to the class interface for those mediums, because that would force the Sales Order BO to implement all these methods. And that would violate the SOLID principle I = Interface Segregation Principle. It says “A client should never be forced to implement an interface that it doesn’t use, or clients shouldn’t be forced to depend on methods they do not use.”.

Implementation

The code in the method zopt_outp_man_external_send~execute would look like:

DATA(sales_order_bo_dp) = NEW zsd_sales_order_bo_dp( '100000001' ).
DATA(sls_ord_document_content) = sales_order_bo_dp->get_document_content( ).
DATA(sls_ord_email_content) = sales_order_bo_dp->get_email_content( ). DATA(sales_order_email) = zeml_extended_email=>create( VALUE #( language_id = sales_order_document-head-language_id sender = VALUE #( name = sls_ord_document_content-fields-head-company-name address = sls_ord_document_content-fields-company-email_address ) receivers = VALUE #( ( name = sls_ord_document_content-fields-head-sold_to_party-name address = sls_ord_document_content-fields-head-sold_to_party-email_address ) ) content = VALUE #( layout_xslt_name = 'ZSD_SALES_ORDER_EMAIL' content = sls_ord_email_content ) adobe_document_attachments = VALUE #( ( file_name = |order_{ sls_ord_document_content-fields-head-sales_order_no}.pdf| form_name = 'ZSD_SALES_ORDER_DOCUMENT' content = sls_ord_document_content ) ) ) ). sales_order_email->send( ).

All parameters of the create call are Sales Order specific because all parameters start with ‘ZSD_SALES..’,  sls_ord or sales_order_.

The class Extended Email is completely generic. The method create of this class is a well-organized interface, so it can be reused in many other business objects. And it can be extended for extra functionality. So also SOLID principle: Open / Closed is met, which means Open for extension but closed for modification.

The Classic Output Management framework requires a program (TNAPR-PGNAM) and a FORM-routine (TNAPR-RONAM). FORM-routines  are procedural programming, so we have to switch to OO programming by introducing a class. The processing is all about processing an Output Message so the name of the class will be “Output Message”.

The program is generic. It does not contain any Sales Order specific logic. Therefor the program is renamed from “Sales Order Output Processing” to the generic name “Output Message Processing”. The ABAP  program only instantiates and starts the Output Message.

Do not call the class “Output Message Processing”. The object is the Output Message itself, and it can do something. It can process (or execute) itself. So the word Processing should be left out.

Output Message class

The client call to this class would be.

DATA(sales_order_output_message) = zsd_sales_order_output_message=>get_instance( output_program = tnapr output_message = nast ). sales_order_output_message->process( ).

Business Object Factory

The instantiation of the class Sales Order BO can be retrieved from the class Output Message, so it can also be reused in other designs.

So we have to design a class which instantiates Business Objects. The logic name for this class is  Business Object Factory. The table Output Message Definition (TNAPR) contain the field Application (KAPPL). Value V1 means that it is a Sales Order. This field is used to determine the Business Object class. Interface Business Object IF is created to give all Business Objects a generic type which is needed als return variable for method getBoInstance().

Implementation

Implementation is out of scope, but this gives an idea how to program it.

CLASS zbo_business_object_factory DEFINITION. METHODS get_bo_instance IMPORTING key TYPE string RETURNING VALUE(business_object) TYPE zbo_business_object_if. ENDCLASS. CLASS zbo_business_object_factory DEFINITION. METHOD get_bo_instance. CASE tnapr-kappl. WHEN 'V1'. business_object = zsd_sales_order_bo=>create( conv #( key ) ). ENDCASE. ENDMETHOD. ENDCLASS.

The output message class can CAST the class interface ZBO_BUSINESS_OBJECT_IF to  class interface ZOPT_OUTP_MESS_EXTERNAL SEND_IF.

CASE TYPE OF business_object. WHEN TYPE zopt_outp_mess_external_send INTO DATA(outp_mess_external_send). outp_mess_external_send->execute( ... ). WHEN OTHERS. RAISE EXCEPTION ... ENDCASE.

This blog post contains a full “Reuse optimization” process containing different types of generalization. The class diagram was transferred from 4 to 14 classes. That is a good example of going from a course grained to fine grained class diagram. Only fine grained classes make optimal reuse possible.

If you want to get more experience on OO programming, you can practice programming the generalized class diagram in this blog post.

There is one step to go and that is “Constructional optimization” which is about choosing the way of instantiation. Some options for instantiation (a.k.a. construction or creation) are constructor method, static factory method, singleton, factory method pattern, abstract factory pattern, builder pattern and prototype pattern.