Singleton Pattern in RAP: Anwendungsweite Einstellungen verwalten

kategorie
RAP
Veröffentlicht
autor
Johannes

Das Singleton Pattern in RAP ermöglicht Business Objects, die genau eine Instanz haben. Dies ist ideal für anwendungsweite Einstellungen, Konfigurationen oder globale Parameter, die zentral verwaltet werden sollen.

Grundkonzept

Ein Singleton im RAP-Kontext ist ein Business Object, das:

  • Genau eine Instanz haben darf
  • Nicht gelöscht werden kann (die Instanz existiert immer)
  • Keine neuen Instanzen erstellen lässt
  • Nur Update-Operationen erlaubt
Normales BOSingleton BO
Beliebig viele InstanzenGenau eine Instanz
CREATE, UPDATE, DELETENur UPDATE
Eigener SchlüsselFester/virtueller Schlüssel
Liste von ObjektenEin globales Objekt

Anwendungsfälle

Das Singleton Pattern eignet sich für:

  • Systemeinstellungen: Globale Konfigurationsparameter
  • Anwendungskonfiguration: Feature Flags, Schwellwerte
  • Mandanteneinstellungen: Firmenspezifische Defaults
  • Zähler und Statistiken: Anwendungsweite Metriken
  • Letzter Zustand: “Current” Objekte (aktueller Abrechnungszeitraum)

Implementierung

1. Datenbanktabelle

Die Tabelle hat typischerweise einen festen Schlüsselwert:

@EndUserText.label : 'Application Settings'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #C
define table zappsettings {
key client : abap.clnt not null;
key singleton_key : abap.char(1) not null; " Immer 'X'
max_items : abap.int4;
default_currency : abap.cuky;
enable_feature_a : abap_boolean;
enable_feature_b : abap_boolean;
last_changed_at : timestampl;
last_changed_by : abap.uname;
}

2. CDS View für Singleton

Der entscheidende Unterschied ist die Annotation @ObjectModel.singleton.element:

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Application Settings'
define root view entity ZI_AppSettings
as select from zappsettings
{
key singleton_key as SingletonKey,
max_items as MaxItems,
default_currency as DefaultCurrency,
enable_feature_a as EnableFeatureA,
enable_feature_b as EnableFeatureB,
@Semantics.systemDateTime.lastChangedAt: true
last_changed_at as LastChangedAt,
@Semantics.user.lastChangedBy: true
last_changed_by as LastChangedBy
}

3. Behavior Definition mit Singleton

Die Behavior Definition deklariert das Singleton mit dem Keyword singleton:

managed implementation in class zbp_i_appsettings unique;
strict ( 2 );
define behavior for ZI_AppSettings alias Settings
persistent table zappsettings
lock master
authorization master ( instance )
{
// Singleton-Deklaration
static singleton;
// Nur Update erlaubt - kein Create/Delete
update;
// Feldmapping
field ( readonly ) SingletonKey;
field ( readonly ) LastChangedAt, LastChangedBy;
// Automatische Feldbefüllung
mapping for zappsettings corresponding
{
SingletonKey = singleton_key;
MaxItems = max_items;
DefaultCurrency = default_currency;
EnableFeatureA = enable_feature_a;
EnableFeatureB = enable_feature_b;
LastChangedAt = last_changed_at;
LastChangedBy = last_changed_by;
}
}

4. Singleton automatisch initialisieren

Da ein Singleton immer existieren muss, wird es beim ersten Zugriff automatisch erstellt. Dies geschieht über eine read Erweiterung:

managed implementation in class zbp_i_appsettings unique;
strict ( 2 );
define behavior for ZI_AppSettings alias Settings
persistent table zappsettings
lock master
authorization master ( instance )
{
static singleton;
update;
// Erlaubt die automatische Initialisierung
internal create;
field ( readonly ) SingletonKey;
}

5. Behavior Implementation

Die Implementation stellt sicher, dass das Singleton existiert:

CLASS zbp_i_appsettings DEFINITION PUBLIC ABSTRACT FINAL
FOR BEHAVIOR OF zi_appsettings.
ENDCLASS.
CLASS zbp_i_appsettings IMPLEMENTATION.
ENDCLASS.
CLASS lhc_settings DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS get_instance_authorizations FOR INSTANCE AUTHORIZATION
IMPORTING keys REQUEST requested_authorizations FOR Settings RESULT result.
METHODS read FOR READ
IMPORTING keys FOR READ Settings RESULT result.
ENDCLASS.
CLASS lhc_settings IMPLEMENTATION.
METHOD get_instance_authorizations.
" Alle dürfen lesen und ändern (hier Berechtigungsprüfung einbauen)
LOOP AT keys ASSIGNING FIELD-SYMBOL(<key>).
APPEND VALUE #(
%tky = <key>-%tky
%update = if_abap_behv=>auth-allowed
) TO result.
ENDLOOP.
ENDMETHOD.
METHOD read.
" Prüfen ob Singleton existiert
SELECT SINGLE * FROM zappsettings
WHERE singleton_key = 'X'
INTO @DATA(ls_settings).
IF sy-subrc <> 0.
" Singleton existiert nicht - initial anlegen
INSERT zappsettings FROM @( VALUE #(
singleton_key = 'X'
max_items = 100
default_currency = 'EUR'
) ).
" Erneut lesen
SELECT SINGLE * FROM zappsettings
WHERE singleton_key = 'X'
INTO @ls_settings.
ENDIF.
" Ergebnis zurückgeben
result = VALUE #( (
SingletonKey = ls_settings-singleton_key
MaxItems = ls_settings-max_items
DefaultCurrency = ls_settings-default_currency
EnableFeatureA = ls_settings-enable_feature_a
EnableFeatureB = ls_settings-enable_feature_b
LastChangedAt = ls_settings-last_changed_at
LastChangedBy = ls_settings-last_changed_by
) ).
ENDMETHOD.
ENDCLASS.

Singleton mit Draft

Für komplexere Einstellungen kann Draft Handling aktiviert werden:

managed implementation in class zbp_i_appsettings unique;
strict ( 2 );
with draft;
define behavior for ZI_AppSettings alias Settings
persistent table zappsettings
draft table zappsettings_d
lock master
authorization master ( instance )
{
static singleton;
update;
internal create;
draft action Edit;
draft action Activate optimized;
draft action Discard;
draft action Resume;
draft determine action Prepare;
field ( readonly ) SingletonKey;
}

Die Draft-Tabelle:

@EndUserText.label : 'Draft: Application Settings'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
define table zappsettings_d {
key client : abap.clnt not null;
key singleton_key : abap.char(1) not null;
max_items : abap.int4;
default_currency : abap.cuky;
enable_feature_a : abap_boolean;
enable_feature_b : abap_boolean;
// Draft Admin Felder
"%admin" : include sych_bdl_draft_admin_inc;
}

Projection View

Die Projection für die Fiori App:

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'App Settings'
@Metadata.allowExtensions: true
define root view entity ZC_AppSettings
provider contract transactional_query
as projection on ZI_AppSettings
{
key SingletonKey,
MaxItems,
DefaultCurrency,
EnableFeatureA,
EnableFeatureB,
LastChangedAt,
LastChangedBy
}

Projection Behavior

projection;
strict ( 2 );
use draft;
define behavior for ZC_AppSettings alias Settings
{
use update;
use action Edit;
use action Activate;
use action Discard;
use action Resume;
use action Prepare;
}

UI-Annotation für Object Page

Ein Singleton wird typischerweise als Object Page ohne vorherige Liste angezeigt:

@Metadata.layer: #CUSTOMER
annotate entity ZC_AppSettings with
{
@UI.facet: [
{
id: 'GeneralSettings',
purpose: #STANDARD,
type: #FIELDGROUP_REFERENCE,
label: 'Allgemeine Einstellungen',
position: 10,
targetQualifier: 'General'
},
{
id: 'Features',
purpose: #STANDARD,
type: #FIELDGROUP_REFERENCE,
label: 'Feature Flags',
position: 20,
targetQualifier: 'Features'
}
]
@UI.fieldGroup: [{ qualifier: 'General', position: 10 }]
MaxItems;
@UI.fieldGroup: [{ qualifier: 'General', position: 20 }]
DefaultCurrency;
@UI.fieldGroup: [{ qualifier: 'Features', position: 10 }]
EnableFeatureA;
@UI.fieldGroup: [{ qualifier: 'Features', position: 20 }]
EnableFeatureB;
}

Singleton vom Code konsumieren

EML Zugriff

" Singleton lesen
READ ENTITIES OF zi_appsettings
ENTITY Settings
ALL FIELDS WITH VALUE #( ( SingletonKey = 'X' ) )
RESULT DATA(lt_settings).
DATA(ls_settings) = lt_settings[ 1 ].
" Singleton ändern
MODIFY ENTITIES OF zi_appsettings
ENTITY Settings
UPDATE FIELDS ( MaxItems EnableFeatureA )
WITH VALUE #( (
SingletonKey = 'X'
MaxItems = 200
EnableFeatureA = abap_true
) )
FAILED DATA(failed)
REPORTED DATA(reported).
" Speichern
COMMIT ENTITIES.

Helper-Klasse für einfachen Zugriff

CLASS zcl_app_settings DEFINITION PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION.
CLASS-METHODS:
get_instance
RETURNING VALUE(ro_instance) TYPE REF TO zcl_app_settings,
get_max_items
RETURNING VALUE(rv_value) TYPE i,
get_default_currency
RETURNING VALUE(rv_value) TYPE waers,
is_feature_a_enabled
RETURNING VALUE(rv_enabled) TYPE abap_bool.
PRIVATE SECTION.
CLASS-DATA: go_instance TYPE REF TO zcl_app_settings.
DATA: ms_settings TYPE zi_appsettings.
METHODS constructor.
METHODS load_settings.
ENDCLASS.
CLASS zcl_app_settings IMPLEMENTATION.
METHOD get_instance.
IF go_instance IS NOT BOUND.
go_instance = NEW #( ).
ENDIF.
ro_instance = go_instance.
ENDMETHOD.
METHOD constructor.
load_settings( ).
ENDMETHOD.
METHOD load_settings.
READ ENTITIES OF zi_appsettings
ENTITY Settings
ALL FIELDS WITH VALUE #( ( SingletonKey = 'X' ) )
RESULT DATA(lt_settings).
IF lt_settings IS NOT INITIAL.
ms_settings = lt_settings[ 1 ].
ENDIF.
ENDMETHOD.
METHOD get_max_items.
rv_value = get_instance( )->ms_settings-MaxItems.
ENDMETHOD.
METHOD get_default_currency.
rv_value = get_instance( )->ms_settings-DefaultCurrency.
ENDMETHOD.
METHOD is_feature_a_enabled.
rv_enabled = get_instance( )->ms_settings-EnableFeatureA.
ENDMETHOD.
ENDCLASS.

Singleton mit Parent-Entity

Ein Singleton kann auch als Child eines anderen BO existieren:

define behavior for ZI_Project alias Project
{
create;
update;
delete;
// Jedes Projekt hat genau eine Settings-Instanz
association _ProjectSettings { create; }
}
define behavior for ZI_ProjectSettings alias ProjectSettings
lock dependent by _Project
authorization dependent by _Project
{
// Singleton pro Parent
singleton;
update;
internal create;
association _Project;
field ( readonly ) ProjectID;
}

Best Practices

EmpfehlungBegründung
Fester Schlüsselwert verwendenEinfacher Zugriff ohne Suche
internal create nutzenAutomatische Initialisierung ermöglichen
Helper-Klasse bereitstellenVereinfachter Zugriff aus ABAP-Code
Caching implementierenPerformance bei häufigem Zugriff
Berechtigungen prüfenNicht jeder sollte Einstellungen ändern dürfen
Draft für komplexe SettingsBessere UX bei vielen Einstellungen

Zusammenfassung

Das Singleton Pattern in RAP ist die ideale Lösung für:

  • Globale Konfigurationen die zentral verwaltet werden
  • Feature Flags zur Steuerung von Anwendungsverhalten
  • Systemparameter die nur ein Mal existieren dürfen

Mit dem Keyword static singleton in der Behavior Definition und internal create für die automatische Initialisierung ist die Implementierung straightforward.

Weiterführende Themen