Thumbnail image

Adopta la mentalidad de extensiones con Dynamics 365 for Finance and Operations #2 – Framework SysExtension

!
Atención: Este artículo se publicó hace más de 365 días. La información podría estar desactualizada.

Índice

Advertencia
Este artículo ha sido traducido automáticamente con Copilot y todavía está pendiente de revisión manual. Ante cualquier error por favor revise el original el inglés cambiando el idioma en la parte superior-derecha

En mi publicación anterior Adopta la mentalidad de extensiones con Dynamics 365 for Finance and Operations reflexionamos sobre algunos de los patrones que podemos aprovechar para crear nuestras personalizaciones utilizando solo cambios no intrusivos, basándonos en un ejemplo real: Añadir una nueva Secuencia de Números a un módulo estándar.

En particular, discutimos:

  • Extensiones de Metadatos — para añadir un nuevo valor de Enum en un Base Enum estándar.
  • Extensiones de Clase — para añadir un nuevo método a una clase estándar.
  • Chain-of-Command — para añadir un bloque de código a un método existente de una clase estándar.
  • Suscripción a Event-Handler — para suscribir nuestro propio método a un delegate estándar existente proporcionado como punto de extensión.

Si aún no has leído esa publicación, por favor hazlo ahora. Si ya lo hiciste (¡gracias!), continuemos con otro patrón que debemos considerar: Framework SysExtension (también SysPlugin, que es bastante similar).

Este patrón nos permite crear nuevas subclases para métodos de fábrica sin ningún over-layering ni acoplamiento con la jerarquía original. Por lo tanto, podemos añadir nuevas subclases en nuestros propios paquetes sin modificaciones intrusivas y reemplazar un patrón muy común, ampliamente utilizado en toda la aplicación, como este (tomado de AX 2012 R3):

static SalesCopying construct(SalesPurchCopy salesPurchCopy)
{
    switch(salesPurchCopy)
    {
        case SalesPurchCopy::CreditNoteHeader       : return new SalesCopying_CreditNote();
        case SalesPurchCopy::CreditNoteLines        : return SalesCopyingCreditNoteLine::construct();

        case SalesPurchCopy::CopyAllHeader          :
        case SalesPurchCopy::CopyAllLines           :
        case SalesPurchCopy::CopyJournalHeader      :
        case SalesPurchCopy::CopyJournalLines       : return new SalesCopying();

        // 
        case SalesPurchCopy::VoidFiscalDocument_BR  : return new SalesCopying_VoidFiscalDocument_BR();
        // 

        default                                     : throw error(strFmt("@SYS19306",funcName()));
    }

    throw error(strFmt("@SYS19306",funcName()));
}

Este patrón tiene muchos problemas para ser extensible. El más obvio es probablemente el throw error en el caso default, que hace imposible que una clase de extensión se suscriba al Post event del método para añadir nuevos casos. Pero incluso eliminando esta sentencia throw (que de hecho ha sido eliminada en muchos métodos estándar como una forma rápida de hacerlos extensibles), el patrón en sí sigue siendo un problema. Si una nueva personalización de cliente o partner, o incluso un nuevo módulo estándar, necesita un nuevo caso, esta clase debe ser modificada y todo el paquete recompilado y desplegado.

Sin embargo, tener este patrón de Fábrica es útil, así que los frameworks SysExtension (y SysPlugin) nos permiten crear el mismo comportamiento con un solo constructor pero sin todo el acoplamiento creado por la sentencia switch-case. El framework SysExtension utiliza una clase atributo para identificar la jerarquía y decidir su comportamiento particular, y este atributo es usado por un constructor genérico (la Fábrica) para instanciar la subclase correcta dependiendo de los parámetros del constructor. El SysExtensionIAttribute nos guía sobre cómo implementar nuevas subclases. Dichas clases atributo tendrán estas relaciones:

Como siempre, la mejor manera de aprender es mirar el código estándar. Tenemos un buen ejemplo de este patrón en la jerarquía SalesLineCopyFromSource (clase SalesLineCopy y subclases en Dynamics AX 2012). Esta es la clase atributo extraída de MSDyn365FO 8.0:

/// <summary>
/// The SalesLineCopyFromSourceFactoryAttribute is an attribute used for instantiating the SalesLineCopyFromSource class.
/// </summary>
class SalesLineCopyFromSourceFactoryAttribute extends SysAttribute implements SysExtensionIAttribute
{
    TableName tableName;

    public void new(TableName _tableName)
    {
        tableName = _tableName;
    }

    public str parmCacheKey()
    {
        return classStr(SalesLineCopyFromSourceFactoryAttribute)+';'+tableName;
    }

    public boolean useSingleton()
    {
        return false;
    }
}

Y aquí está el método Fábrica, el constructor estático que devolverá una instancia de subclase dependiendo de los parámetros recibidos. Observa las 2 primeras líneas ya que aquí es donde principalmente se crea la instancia basada en la clase atributo, que será la misma en todas las clases para una jerarquía de fábrica específica:

abstract class SalesLineCopyFromSource
{
    public static SalesLineCopyFromSource construct(SalesLine _salesLine, SalesTable _salesTable, TmpFrmVirtual _tmpFrmVirtualLines, SalesCopying _salesCopying)
    {
        SalesLineCopyFromSourceFactoryAttribute attr = new SalesLineCopyFromSourceFactoryAttribute(tableId2Name(_tmpFrmVirtualLines.TableNum));
        SalesLineCopyFromSource salesLineCopyFromSource = SysExtensionAppClassFactory::getClassFromSysAttribute(classStr(SalesLineCopyFromSource), attr) as SalesLineCopyFromSource;

        if (!salesLineCopyFromSource)
        {
            throw error(Error::wrongUseOfFunction(funcName()));
        }

        salesLineCopyFromSource.initialize(_salesLine, _salesTable, _tmpFrmVirtualLines, _salesCopying);
        return salesLineCopyFromSource;
    }
}

Este simple constructor detectará automáticamente nuevas subclases cuando estén decoradas con el atributo especificado sin ninguna modificación, incluyendo lanzar un error si no se encuentra una subclase, algo que nunca debería ocurrir. Todas las subclases se ven así:

/// <summary>
/// The SalesLineCopyFromSource class is responsible for copying SalesLine from a SalesLine.
/// </summary>
[SalesLineCopyFromSourceFactory(tableStr(SalesLine))]
class SalesLineCopyFromSalesLine extends SalesLineCopyFromSource
{
    ...
}

/// <summary>
/// The SalesLineCopyFromSource class is responsible for copying SalesLine from a CustConfirmTrans.
/// </summary>
[SalesLineCopyFromSourceFactory(tableStr(CustConfirmTrans))]
class SalesLineCopyFromCustConfirmTrans extends SalesLineCopyFromSource
{
    ...
}

Podemos ver un ejemplo de cómo consumir esta jerarquía, por ejemplo en la clase SalesCopying, método copyLines. En este caso, esta clase se usa de la misma manera que en la versión anterior, no hay cambio en el lado del consumidor (excepto el nuevo nombre de clase):

SalesLineCopyFromSource salesLineCopyFromSource = **SalesLineCopyFromSource::construct(salesLine, salesTable, tmpFrmVirtualLines, this);**

// ... eliminado por simplicidad

salesLineCopyFromSource.copy();

La jerarquía completa se ve así:

Ahora, imagina que queremos añadir una nueva subclase a esta estructura o a una similar. Solo necesitamos crear nuestra propia clase y decorarla con el atributo de la jerarquía. Para este ejemplo, supongamos que queremos extender esta estructura para trabajar con la tabla MyTable:

[SalesLineCopyFromSourceFactory(tableStr(MyTable))]
class SalesLineCopyFromMyTable extends SalesLineCopyFromSource
{
    ....
}

Entonces, como estamos heredando de la clase abstracta SalesLineCopyFromSource, el compilador se quejará si no implementamos todos los métodos abstractos de la superclase:

Mirando una de las subclases es fácil entender cómo implementar estos métodos (no he probado yo mismo si esta implementación funciona, solo la muestro con fines ilustrativos):

[SalesLineCopyFromSourceFactory(tableStr(MyTable))]
class SalesLineCopyFromMyTable extends SalesLineCopyFromSource
{
    MyTable myTable;

    public Qty retrieveSourceQty()
    {
        return myTable.SalesQty;
    }

    public Common retrieveSource()
    {
        return myTable;
    }

    public InventTransId retrieveSourceInventTransId()
    {
        return myTable.InventTransId;
    }
}

Probablemente ya habrás notado que no hay extensiones en absoluto en este patrón. Solo Clean Code puro y diseño orientado a objetos que será extensible por sus propios méritos. Por lo tanto, en nuestro próximo viaje para cambiar nuestra mentalidad hacia la personalización basada en extensiones, es especialmente importante que empecemos a pensar en clean code como una forma de desarrollar código extensible desde el principio y estar preparados para identificar estos patrones. Refactorizaciones como la mostrada aquí a menudo ocurrirán en el código estándar como resultado de solicitudes de extensibilidad recibidas de partners y clientes.

NOTA: sorprendentemente este framework no es exactamente nuevo, ya estaba disponible en Dynamics AX 2012 pero su uso nunca fue plenamente adoptado. ¡Ha llegado su momento ahora con nuestro cambio de mentalidad!

Publicado primero en “Dynamics AX in the Field”, el blog del equipo de Premier Field Engineering de Microsoft.

Publicaciones en esta serie

Publicaciones similares