Los Feature Flags (tambien llamados Feature Toggles) permiten activar o desactivar funcionalidades en tiempo de ejecucion sin cambiar codigo. Son esenciales para despliegue continuo, testing A/B y lanzamientos controlados.
Conceptos
| Tipo | Descripcion | Duracion |
|---|---|---|
| Release Toggle | Funcionalidades en desarrollo | Dias/Semanas |
| Ops Toggle | Control operacional | Permanente |
| Experiment Toggle | A/B Testing | Dias/Semanas |
| Permission Toggle | Funcionalidades premium | Permanente |
Implementacion
1. Tabla de Feature Flags
@EndUserText.label : 'Feature Flags'@AbapCatalog.enhancement.category : #NOT_EXTENSIBLEdefine table zfeature_flags { key client : abap.clnt not null; key feature_id : abap.char(40) not null;
description : abap.char(100); is_enabled : abap_boolean; enabled_from : abap.dats; enabled_to : abap.dats; rollout_percent : abap.int1; // 0-100
created_by : abap.uname; created_at : timestampl; changed_by : abap.uname; changed_at : timestampl;}2. Servicio basico de Feature Flags
CLASS zcl_feature_flags DEFINITION PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION. CLASS-METHODS: get_instance RETURNING VALUE(ro_instance) TYPE REF TO zcl_feature_flags.
METHODS: is_enabled IMPORTING iv_feature_id TYPE zfeature_id RETURNING VALUE(rv_enabled) TYPE abap_bool,
is_enabled_for_user IMPORTING iv_feature_id TYPE zfeature_id iv_user_id TYPE syuname DEFAULT sy-uname RETURNING VALUE(rv_enabled) TYPE abap_bool.
PRIVATE SECTION. CLASS-DATA: go_instance TYPE REF TO zcl_feature_flags.
DATA: mt_cache TYPE HASHED TABLE OF zfeature_flags WITH UNIQUE KEY feature_id.
METHODS: constructor, load_flags, get_flag IMPORTING iv_feature_id TYPE zfeature_id RETURNING VALUE(rs_flag) TYPE zfeature_flags.ENDCLASS.
CLASS zcl_feature_flags IMPLEMENTATION. METHOD get_instance. IF go_instance IS NOT BOUND. go_instance = NEW zcl_feature_flags( ). ENDIF. ro_instance = go_instance. ENDMETHOD.
METHOD constructor. load_flags( ). ENDMETHOD.
METHOD load_flags. SELECT * FROM zfeature_flags INTO TABLE @mt_cache. ENDMETHOD.
METHOD get_flag. rs_flag = VALUE #( mt_cache[ feature_id = iv_feature_id ] DEFAULT VALUE #( ) ). ENDMETHOD.
METHOD is_enabled. DATA(ls_flag) = get_flag( iv_feature_id ).
" No existe = deshabilitado IF ls_flag IS INITIAL. rv_enabled = abap_false. RETURN. ENDIF.
" Verificar flag basico IF ls_flag-is_enabled = abap_false. rv_enabled = abap_false. RETURN. ENDIF.
" Verificar rango de fechas DATA(lv_today) = sy-datum.
IF ls_flag-enabled_from IS NOT INITIAL AND lv_today < ls_flag-enabled_from. rv_enabled = abap_false. RETURN. ENDIF.
IF ls_flag-enabled_to IS NOT INITIAL AND lv_today > ls_flag-enabled_to. rv_enabled = abap_false. RETURN. ENDIF.
rv_enabled = abap_true. ENDMETHOD.
METHOD is_enabled_for_user. " Primero verificar flag global IF is_enabled( iv_feature_id ) = abap_false. rv_enabled = abap_false. RETURN. ENDIF.
DATA(ls_flag) = get_flag( iv_feature_id ).
" Rollout progresivo basado en hash del usuario IF ls_flag-rollout_percent < 100. DATA(lv_hash) = get_user_hash( iv_user_id ). DATA(lv_bucket) = lv_hash MOD 100.
rv_enabled = xsdbool( lv_bucket < ls_flag-rollout_percent ). ELSE. rv_enabled = abap_true. ENDIF. ENDMETHOD.ENDCLASS.3. Uso en codigo de negocio
METHOD process_order. DATA(lo_flags) = zcl_feature_flags=>get_instance( ).
" Verificar feature flag antes de usar nueva funcionalidad IF lo_flags->is_enabled( 'NEW_PRICING_ENGINE' ). " Nuevo algoritmo de precios calculate_price_v2( ). ELSE. " Algoritmo antiguo calculate_price_v1( ). ENDIF.
" Feature flag con rollout por usuario IF lo_flags->is_enabled_for_user( 'ENHANCED_UI' ). enable_enhanced_ui( ). ENDIF.ENDMETHOD.4. Feature Flags con contexto
" Tabla adicional para contexto@EndUserText.label : 'Feature Flag Context'define table zfeature_context { key client : abap.clnt not null; key feature_id : abap.char(40) not null; key context_type : abap.char(20) not null; // USER, ROLE, COMPANY, etc. key context_value : abap.char(100) not null;
is_enabled : abap_boolean;}
" Servicio extendidoMETHOD is_enabled_with_context. " Verificar contextos en orden de prioridad " 1. Usuario especifico DATA(lv_user_enabled) = check_context( iv_feature_id = iv_feature_id iv_context_type = 'USER' iv_context_value = sy-uname ).
IF lv_user_enabled IS NOT INITIAL. rv_enabled = lv_user_enabled. RETURN. ENDIF.
" 2. Rol del usuario SELECT role_name FROM agr_users WHERE uname = @sy-uname INTO TABLE @DATA(lt_roles).
LOOP AT lt_roles INTO DATA(lv_role). DATA(lv_role_enabled) = check_context( iv_feature_id = iv_feature_id iv_context_type = 'ROLE' iv_context_value = lv_role ).
IF lv_role_enabled = abap_true. rv_enabled = abap_true. RETURN. ENDIF. ENDLOOP.
" 3. Flag global rv_enabled = is_enabled( iv_feature_id ).ENDMETHOD.5. API RAP para administracion
" CDS View@EndUserText.label: 'Feature Flags'define root view entity ZI_FeatureFlag as select from zfeature_flags{ key feature_id as FeatureId, description as Description, is_enabled as IsEnabled, enabled_from as EnabledFrom, enabled_to as EnabledTo, rollout_percent as RolloutPercent,
@Semantics.user.createdBy: true created_by as CreatedBy,
@Semantics.systemDateTime.createdAt: true created_at as CreatedAt,
@Semantics.user.lastChangedBy: true changed_by as ChangedBy,
@Semantics.systemDateTime.lastChangedAt: true changed_at as ChangedAt}
" Behavior Definitionmanaged implementation in class zbp_i_featureflag unique;strict ( 2 );
define behavior for ZI_FeatureFlag alias FeatureFlagpersistent table zfeature_flagslock masterauthorization master ( instance ){ create; update; delete;
field ( readonly ) CreatedBy, CreatedAt, ChangedBy, ChangedAt; field ( mandatory ) FeatureId, Description;
action enable; action disable; action setRollout parameter ZA_RolloutParams;}6. Patron: Feature Flag Guard
" Clase guard para encapsular verificacionCLASS zcl_feature_guard DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. METHODS: constructor IMPORTING iv_feature_id TYPE zfeature_id,
when_enabled IMPORTING io_action TYPE REF TO zif_action RETURNING VALUE(ro_self) TYPE REF TO zcl_feature_guard,
when_disabled IMPORTING io_action TYPE REF TO zif_action RETURNING VALUE(ro_self) TYPE REF TO zcl_feature_guard,
execute.
PRIVATE SECTION. DATA: mv_feature_id TYPE zfeature_id, mo_enabled_action TYPE REF TO zif_action, mo_disabled_action TYPE REF TO zif_action.ENDCLASS.
CLASS zcl_feature_guard IMPLEMENTATION. METHOD constructor. mv_feature_id = iv_feature_id. ENDMETHOD.
METHOD when_enabled. mo_enabled_action = io_action. ro_self = me. ENDMETHOD.
METHOD when_disabled. mo_disabled_action = io_action. ro_self = me. ENDMETHOD.
METHOD execute. IF zcl_feature_flags=>get_instance( )->is_enabled( mv_feature_id ). IF mo_enabled_action IS BOUND. mo_enabled_action->execute( ). ENDIF. ELSE. IF mo_disabled_action IS BOUND. mo_disabled_action->execute( ). ENDIF. ENDIF. ENDMETHOD.ENDCLASS.
" Uso fluidoNEW zcl_feature_guard( 'NEW_CHECKOUT' ) ->when_enabled( NEW zcl_new_checkout_action( ) ) ->when_disabled( NEW zcl_old_checkout_action( ) ) ->execute( ).7. A/B Testing con Feature Flags
" Tabla para experimentos@EndUserText.label : 'Feature Experiments'define table zfeature_experiment { key client : abap.clnt not null; key experiment_id : abap.char(40) not null; key user_id : abap.uname not null;
variant : abap.char(10); // A, B, C... assigned_at : timestampl; converted : abap_boolean; converted_at : timestampl;}
" Servicio de experimentosCLASS zcl_ab_testing DEFINITION PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION. METHODS: get_variant IMPORTING iv_experiment_id TYPE string RETURNING VALUE(rv_variant) TYPE char10,
record_conversion IMPORTING iv_experiment_id TYPE string.ENDCLASS.
CLASS zcl_ab_testing IMPLEMENTATION. METHOD get_variant. " Verificar asignacion existente SELECT SINGLE variant FROM zfeature_experiment WHERE experiment_id = @iv_experiment_id AND user_id = @sy-uname INTO @rv_variant.
IF sy-subrc <> 0. " Asignar variante aleatoriamente DATA(lv_random) = cl_abap_random_int=>create( seed = CONV i( sy-uzeit ) min = 1 max = 2 )->get_next( ).
rv_variant = COND #( WHEN lv_random = 1 THEN 'A' ELSE 'B' ).
" Guardar asignacion INSERT zfeature_experiment FROM @( VALUE #( experiment_id = iv_experiment_id user_id = sy-uname variant = rv_variant assigned_at = utclong_current( ) ) ). ENDIF. ENDMETHOD.
METHOD record_conversion. UPDATE zfeature_experiment SET converted = @abap_true converted_at = @( utclong_current( ) ) WHERE experiment_id = @iv_experiment_id AND user_id = @sy-uname. ENDMETHOD.ENDCLASS.
" UsoDATA(lo_ab) = NEW zcl_ab_testing( ).DATA(lv_variant) = lo_ab->get_variant( 'CHECKOUT_FLOW_2024' ).
CASE lv_variant. WHEN 'A'. " Flujo de checkout original show_original_checkout( ). WHEN 'B'. " Flujo de checkout nuevo show_new_checkout( ).ENDCASE.
" Si el usuario completa la compralo_ab->record_conversion( 'CHECKOUT_FLOW_2024' ).8. Feature Flags en CDS/RAP
" Extender CDS con logica de feature flagdefine view entity ZC_SalesOrder as projection on ZI_SalesOrder{ key OrderId, CustomerName, OrderDate, TotalAmount,
// Mostrar campo solo si feature esta habilitado @UI.hidden: #( FeatureFlagHidden ) NewField,
// Campo calculado para visibilidad case when ( select single is_enabled from zfeature_flags where feature_id = 'SHOW_NEW_FIELD' ) = 'X' then ' ' else 'X' end as FeatureFlagHidden}9. Cache y rendimiento
CLASS zcl_feature_flags_cached DEFINITION PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION. CONSTANTS: c_cache_ttl_seconds TYPE i VALUE 300. " 5 minutos
CLASS-METHODS: get_instance RETURNING VALUE(ro_instance) TYPE REF TO zcl_feature_flags_cached.
METHODS: is_enabled IMPORTING iv_feature_id TYPE zfeature_id RETURNING VALUE(rv_enabled) TYPE abap_bool,
invalidate_cache.
PRIVATE SECTION. TYPES: BEGIN OF ty_cache_entry, feature_id TYPE zfeature_id, is_enabled TYPE abap_bool, cached_at TYPE timestampl, END OF ty_cache_entry.
CLASS-DATA: go_instance TYPE REF TO zcl_feature_flags_cached.
DATA: mt_cache TYPE HASHED TABLE OF ty_cache_entry WITH UNIQUE KEY feature_id.
METHODS: is_cache_valid IMPORTING is_entry TYPE ty_cache_entry RETURNING VALUE(rv_valid) TYPE abap_bool,
load_from_db IMPORTING iv_feature_id TYPE zfeature_id RETURNING VALUE(rv_enabled) TYPE abap_bool.ENDCLASS.
CLASS zcl_feature_flags_cached IMPLEMENTATION. METHOD is_enabled. " Buscar en cache DATA(ls_entry) = VALUE #( mt_cache[ feature_id = iv_feature_id ] DEFAULT VALUE #( ) ).
IF ls_entry IS NOT INITIAL AND is_cache_valid( ls_entry ). rv_enabled = ls_entry-is_enabled. RETURN. ENDIF.
" Cargar de BD y cachear rv_enabled = load_from_db( iv_feature_id ).
ls_entry = VALUE #( feature_id = iv_feature_id is_enabled = rv_enabled cached_at = utclong_current( ) ).
INSERT ls_entry INTO TABLE mt_cache. ENDMETHOD.
METHOD is_cache_valid. DATA(lv_now) = utclong_current( ). DATA(lv_age) = cl_abap_tstmp=>subtract( tstmp1 = lv_now tstmp2 = is_entry-cached_at ).
rv_valid = xsdbool( lv_age < c_cache_ttl_seconds ). ENDMETHOD.
METHOD invalidate_cache. CLEAR mt_cache. ENDMETHOD.ENDCLASS.10. Logging y metricas
METHOD is_enabled. DATA(lv_start) = utclong_current( ).
" Obtener valor DATA(lv_enabled) = get_flag_value( iv_feature_id ).
" Log de acceso INSERT zfeature_log FROM @( VALUE #( feature_id = iv_feature_id user_id = sy-uname is_enabled = lv_enabled accessed_at = lv_start ) ).
" Metricas (si esta disponible) TRY. DATA(lo_metrics) = cl_abap_metrics=>get_instance( ). lo_metrics->increment( iv_metric_name = |feature_flag.access.{ iv_feature_id }| ). CATCH cx_root. " Metricas no disponibles ENDTRY.
rv_enabled = lv_enabled.ENDMETHOD.Mejores Practicas
| Practica | Descripcion |
|---|---|
| Nombre descriptivo | ENABLE_NEW_CHECKOUT no FLAG_123 |
| Documentar | Agregar descripcion del proposito |
| Fecha de expiracion | Establecer enabled_to para limpieza |
| Limpieza regular | Eliminar flags obsoletos |
| Valores por defecto | Comportamiento si flag no existe |
| Testing | Probar ambos estados del flag |
Resumen
| Componente | Proposito |
|---|---|
| Tabla de flags | Almacenamiento persistente |
| Servicio singleton | Acceso centralizado |
| Cache | Rendimiento |
| Contexto | Activacion granular |
| Logging | Auditoria y metricas |
Notas importantes
- Los Feature Flags no son configuracion - son temporales.
- Eliminar flags despues de rollout completo.
- Usar nombres claros y descriptivos.
- Implementar fallback para flags no existentes.
- Considerar rendimiento con caching.
- Documentar el ciclo de vida de cada flag.
- En ABAP Cloud: aprovechar RAP para administracion.