Colaboraciones en el Guille

El paradigma SAV o "Simply a Video"

Como simplificar el uso de nuestro código para cuando llegue 'el momento de la verdad'.

 

Fecha: 26/Sep/2005 (25/Sep/05)
Autor: Raul Carrillo Garrido - aka metsuke (rcarrillo@metsuke.com)

 

Introducción:

Ante todo comenzar indicando que soy bastante novato en esto de la orientación a objetos, y que quizás el desconocimiento en algunos casos me haga afirmar cuestiones no del todo correctas (incluso a veces barbaridades, que no soy perfecto). Trataré de explicarme (soy famoso por explicarme como un libro cerrado) y plasmar lo mejor posible el razonamiento que he seguido en cada momento, de forma que equivocado o no, este al menos se pueda seguir. Dicho esto paso a comentar directamente la cuestión:

A lo largo del tiempo he visto muchos casos de código (y aplicaciones) de una factura magnífica que no llegaban a tener la aceptación esperada ¿porqué?. Bueno esta es una respuesta compleja, pero creo que uno de los factores es simplemente la complejidad.

 


¿La complejidad?.

- Si, la complejidad.

Todo comenzó con una conversación aparentemente inocente en la que comentábamos el éxito que tienen ciertos reproductores mp3 y similares, y también el algunos reproductores multimedia. Esta conversación nos llevó a una frase (entre muchas otras) 'es que son tan simples como un video'. En ese momento no le dí mayor importancia pero después dando vueltas al asunto caí en la cuenta de la relación tan estrecha que tiene esta frase con el paradigma de la Orientación a Objetos.

¿Y que tiene que ver la POO con esto?

- Bien, en realidad la POO (Programación Orientada a Objetos) es el centro de esta historia.

Uno de los ejemplos que utilicé cuando comencé a aprender POO, es el del video que tiene varios métodos (los botones) a los que se le pasan datos (la cinta) y obtiene un resultado (la imagen, que luego enviaremos a un objeto que la muestre...). Una de las frases que se me quedó marcada es aquella de 'y no necesitamos saber como funciona por dentro, sólo proporcionarle los datos adecuados y decirle que queremos que haga, del resto se encarga la implementación del objeto'. WOW! pensé, la encapsulación definitiva...

Bueno, en realidad ha resultado ser 'demasiado' definitiva, a menudo se dan casos en los que el diseño interno de las clases es especialmente espectacular, pero que olvidan que esas clases se supone que también deberían simplificar 'in extremis' el uso de las funcionalidades que implementan (sino para qué tanto trabajo ¿para complicarnos más aún que con un diseño incorrecto?). Es habitual encontrar clases especialmente bien diseñadas que pecan tan sólo de una cosa : hay que escribir varias líneas de código cada vez que queremos hacer algo con ellas. Esto las hace cuando menos 'incómodas' de usar, y en muchos casos hasta nos provocan retrasos cuando necesitamos sacar un proyecto adelante, que era 'teóricamente simple' pero que nos trae de cabeza por una 'pequeña e insignificante' clase que resultó ser mucho más difícil de manejar de lo previsto inicialmente.

Vale, aceptamos barco... pero ¿qué es eso del SAV?.

- Ya voy, ya voy. Dejadme afilar el hacha, ya explico de que va todo esto...

En realidad se trata simplemente de generar una capa final de 'publicación' para aquellas clases que no estén diseñadas según el paradigma o simplemente de adoptar un método 'diferente' de publicación en los casos de diseño nuevo. La diferencia está en la publicación de funcionalidades vs publicación de funciones. En el primer caso publicamos todo lo que la clase sabe hacer, en el caso del paradigma sav, aún manteniendo esta 'funcionalidad' pública (por si se necesita), se trata de dotar de métodos a la clase que directamente ejecuten las acciones que el objeto 'sabe' hacer sin necesidad de tediosos bloques de código declarativos, simplemente le daremos órdenes.

¡Que barbaridad! ¡Esto machacará el rendimiento! ¡Es imposible!

Bueno, parece complicado y aparentemente no comulga con la POO, pero en realidad la pretensión es llevar la POO a su extremo, a ese extremo que sobre papel queda muy bonito pero que finalmente no se lleva a la practica (o al menos no todo lo que se debería), y es simplemente llegar al punto en que demos los datos a un objeto, le indiquemos la acción que queremos que realice y que simplemente este 'lo haga' y nos devuelva 'el resultado'.

Habrá casos en los que podamos diseñar nuestro código de este modo, y otros en los que simplemente la 'adaptación sav' consistirá en una clase que actúe como 'control remoto' de otra/s ya existentes, 'solucionando' las funcionalidades que necesitemos y convirtiendo el resto del desarrollo en un conjunto de órdenes a objetos como si del manejo de un video se tratase. Sea cual fuere el método, de lo que se trata es de simplificar el desarrollo al máximo.

Estoy preparando una librería (que se pretende mantenga el código abierto en su totalidad) en el que toma el 'diseño sav' como bandera, que pretendo publicar en próximas entregas y que irá incorporando funcionalidades poco a poco. Sin embargo, antes de iniciar el proceso es necesaria una introducción 'teórica' menos ambiciosa, y con ese fin trataré de explicar el método creando una de esas clases 'control remoto' que nos simplificarán de una vez por todas el trabajo... con la encriptación de .Net.

¡Manos a la Obra!

En primer lugar indicar que vamos a construir una clase 'control remoto' de una librería existente, de código abierto, y que es una de las mas completas que he encontrado, no es mia, así que comenzaré por indicar la dirección del artículo correspondiente, en el que se explica de forma simple (dentro de lo posible) el uso de las clases de encriptación que proporciona .NET.

Simple Encryption - Articulo de Wumpus1 (En Inglés)

He seleccionado este artículo concreto no sólo porque realmente simplifica la tarea (a la manera tradicional), sino que las explicaciones que incluye creo que podrían ser usadas como capítulo en un libro de texto sin demasiadas modificaciones. En mi caso con un poco de trabajo he podido entender en buena medida como funcionan las clases que proporciona el Framework. Por otro lado cubre encriptación simétrica , asimétrica y algoritmos hash, lo cual la convierte en uno de los artículos más completos sobre la materia que he encontrado en la red.

Aún con todo, para realizar la encriptación más simple (simétrica), hacen falta varias líneas de código, tal y como podemos observar en el ejemplo propuesto por el autor:

Dim sym As New Encryption.Symmetric(Encryption.Symmetric.Provider.Rijndael)
Dim key As New Encryption.Data("My Password")
Dim encryptedData As Encryption.Data
encryptedData = sym.Encrypt(New Encryption.Data("Secret Sauce"), key)
Dim base64EncryptedString as String = encryptedData.ToBase64

Si lo analizamos con calma, el código en si no es excesivamente complicado. En realidad las operaciones que se realizan son las siguientes:

  1. Creamos el proveedor de encriptación, indicando que el método a usar es Rijndael.
  2. A continuación se genera la clave de encriptación, quedando almacenada en forma de objeto de tipo Encryption.Data.
  3. En este momento creamos otro objeto del mismo tipo que almacenará nuestro 'secretillo' una vez encriptado.
  4. Este paso simplemente ejecuta la encriptación y recupera el objeto que contiene nuestra cadena encriptada.
  5. Recuperamos la cadena encriptada indicando el formato (Base64 o Hexadecimal).

A pesar de su sencillez, este método nos obliga a realizar todos estos pasos cada vez que deseemos encriptar una cadena de texto. Lo que propondría para este caso es construir un objeto 'control remoto' que nos permita ejecutar esta misma llamada con el siguiente formato ( o uno similar):

CryptSymmetric(Cadena_A_Encriptar as String,Formato_A_Devolver,Metodo_De_Encriptacion) As String

Simplemente solicitaríamos al objeto que encripte la cadena que le proporcionamos , nos la devuelva en encriptada en el formato que le indiquemos y use para ello el método de encriptación establecido por nosotros. Sería algo en realidad muy sencillo de manejar y que entiendo nos facilitaría en gran medida la programación. Fijado el primer objetivo, vamos a construir el 'control remoto':

Lo primero es lo primero.

En primer lugar, creamos un namespace, una clase e importamos la libreria "EncryptionClassLibrary"

Imports EncryptionClassLibrary

Namespace Cryptography
    Public Class Crypto

#Region "Enums"
    Public Enum CryptedStringFormat
        Hex = 1
        Base64 = 2
    End Enum

    Public Enum SymmetricEncryptionMethod
        DES = 1
        RC2 = 2
        TripleDES = 4
        Rijndael = 8
    End Enum
#End Region

End Class
End Namespace

Como podemos ver en el ejemplo, he creado dos enum para describir los distintos métodos de encriptación disponibles y los formatos de cadena encriptada que el objeto nos brinda. El peculiar método de numeración es una herencia que me queda de mi época de trabajo con el API de Win32, en el que nos venía bien poder realizar operaciones del tipo TripleDes + Rijndael para indicar en un solo valor dos elementos.

Encriptando.

Para realizar una encriptacion simetrica, lo primero que necesitaremos será una clave de encriptacion, para ello procedemos a generar la correspondiente variable privada acompañada de la necesaria propiedad:

Imports EncryptionClassLibrary

Namespace Cryptography
    Public Class Crypto

        Private pvSymmetricKey As String = "1r+!1-esKªPZyL"

#Region "Enums" [...]

#Region "Properties"
    Public Property SymmetricKeyWord() As String
        Get
            Try
                Return pvSymmetricKey
            Catch ex As Exception
                Throw
            End Try
        End Get
        Set(ByVal Value As String)
            Try
                If Value.Length = 0 Then
                    Throw New ArgumentNullException("SymmetricKeyWord")
                Else
                    pvSymmetricKey = Value
                End If
            Catch ex As Exception
                Throw
            End Try
        End Set
    End Property
#End Region

End Class
End Namespace

Para este caso entiendo que hay dos vías de implementación (probablemente hay más, pero para este caso se me ocurren estas dos):

He optado por una filosofía basada en simplificar el uso del código generado y esta me exige que lo haga del segundo modo. Lógicamente para obtener una encriptación verdaderamente 'segura' tendré que asignar una clave nueva, pero aquel que use la clase no tendrá que introducir código extra para controlar la excepción, sólo asignar o no nueva clave.

También es necesario que la clase controle el parámetro de entrada de la propiedad de modo que si el usuario trata de asignar una cadena vacía el sistema lance (ahora si) una excepción.

Nota Importante: Si observamos bien las instrucciones del autor para el uso de la clase de encriptación, podremos ver que en dicha clase la clave de encriptación no es obligatoria, y si no se proporciona generará automáticamente una clave válida que podremos recuperar junto con la cadena encriptada. Es por ello que en un código 'no explicativo' lo propio sería no obligar a introducir clave y en el caso de que no sea introducida recuperar lo proporcionado por la clase Simple Encryption .

Dado que el objetivo es ilustrar la metodología de trabajo y no llegar a afinar al 100% el control 'remoto', he tomado como axioma la obligatoriedad de la clave, lo cual me permite explicar el porque de inicializar la variable privada y el punto que viene a continuación. Si alguien quiere recoger el testigo para completar la clase y publicarla en esta web, es libre de hacerlo, sólo le pediría que explicara el nuevo código e hiciera referencia a este articulo para que todo aquel que se acerque a la cuestión tenga fácil acceso a este material.

Lógica Implícita vs. Lógica Explícita

Hago este pequeño inciso para hacer referencia a otro detalle del código anterior, y es el modo en que se ha construido el bloque de comprobación del parámetro "SymmetricKeyWord". Si observamos el Property.Set, vemos que si la longitud de la cadena es cero lanzaremos una excepción y si no asignaremos la propiedad. Esta construcción if.else podría evitarse dejándolo en un simple if ya que, de saltar la excepción, el código saltaría al Catch y las siguientes instrucciones no serían ejecutadas.

Aún sabiendo esto, suelo preferir escribir el código de este modo, ya que lo considero mas fácil de seguir. Del mismo modo que (2*5) + 2 es más fácil de leer que 2 * 5 + 2, en este caso el modo en que esta escrito evita que tengamos que deducir la lógica, ya que de un solo vistazo nuestro cerebro la asimilará, ahorrándonos el tener que ser conscientes de ello.

Aunque parezca extraño, hemos de tener en cuenta que el código es escrito una vez pero leído y/o modificado varias (a veces cientos). Personalmente suelo preferir invertir este tiempo extra en la escritura con tal de ahorrarme tiempo después al volver sobre el mismo código.

A lo que íbamos... que me pongo demasiado 'Filósofo' y se me escapa Pitufina <(^_^)>.

Una vez realizados los preparativos, proseguimos con el método que ejecuta realmente la encriptacion simetrica:

Imports EncryptionClassLibrary

Namespace Cryptography
    Public Class Crypto

        Private pvSymmetricKey As String = "1r+!1-esKªPZyL"

#Region "Enums" [...]

#Region "Symmetric"

    Public Function CryptSymmetric( _
        ByVal pString As String _
        , Optional ByVal pFormat As CryptedStringFormat = CryptedStringFormat.Base64 _
        , Optional ByVal pEncryptionMethod As SymmetricEncryptionMethod = SymmetricEncryptionMethod.Rijndael _
        ) As String
        Try
		
            Dim Secret As String = pString
            Dim Key As String = pvSymmetricKey
            Dim symAlgorithm As Encryption.Symmetric.Provider

            Select Case pEncryptionMethod
                Case SymmetricEncryptionMethod.DES
                    symAlgorithm = Encryption.Symmetric.Provider.DES
                Case SymmetricEncryptionMethod.RC2
                    symAlgorithm = Encryption.Symmetric.Provider.RC2
                Case SymmetricEncryptionMethod.TripleDES
                    symAlgorithm = Encryption.Symmetric.Provider.TripleDES
                Case SymmetricEncryptionMethod.Rijndael
                    symAlgorithm = Encryption.Symmetric.Provider.Rijndael
            End Select

            Dim sym As New Encryption.Symmetric(symAlgorithm)
            sym.Key.Text = Key

            Dim encryptedData As Encryption.Data
            encryptedData = sym.Encrypt(New Encryption.Data(Secret))

            Select Case pFormat
                Case CryptedStringFormat.Base64 : Return encryptedData.Base64
                Case CryptedStringFormat.Hex : Return encryptedData.Hex
            End Select
        Catch ex As Exception
            Throw
        End Try
    End Function
#End Region

#Region "Properties" [...]

End Class
End Namespace

Este es el código de la función para encriptar, en esencia realiza la misma operación que el codigo de ejemplo propuesto por el autor, simplificando su uso. Hay algunas cuestiones que comentar:

Sobrecarga vs Parámetros Opcionales

Cada cual debe decidir el método que mejor le conviene en cada momento, para este ejemplo he considerado más util una sola función con parámetros opcionales en lugar de tres sobrecargas. Prefiero parámetros opcionales siempre que sea posible, ya que reduce el volumen de código a mantener y simplifica su lectura. En el caso de sobrecargas tendríamos que haber creado las siguientes:

  1. CryptSymmetric(texto)
  2. CryptSymmetric(texto,formato_devolucion)
  3. CryptSymmetric(texto,formato_devolucion,metodo_encriptacion)

Y escribir el control y flujo de parámetros por defecto entre ellas. Sin embargo con parámetros opcionales lo que hemos generado es:

  1. CryptSymmetric(texto,formato_devolucion opcional,metodo_encriptacion opcional)

Y tras especificar los parámetros por defecto hemos conseguido el mismo efecto con menos código. Desde mi punto de vista el mejor método siempre será usando parámetros opcionales salvo que no sea posible conseguir la misma funcionalidad , en cuyo caso se requerirá el uso de sobrecarga de métodos.

Evitar que el usuario tenga que decidir obligatoriamente cuando sea posible.

Hay otro concepto que se aplica en esta metodología y es aquel según el cual el ordenador debe tomar las decisiones por el usuario, aunque debe permitirle configurar el mayor numero posible de ellas.

En este caso, aunque el usuario puede decidir sobre el método de encriptación y el formato de devolución, el software tomará inicialmente por el la mejor decisión posible (que en este caso hemos introducido nosotros en forma de valores por defecto), así si el usuario solo proporciona el texto a encriptar, el sistema establecerá como formato de devolución Base64 (el mas 'compacto') y como método de encriptación Rijndael (el 'mejor' de los disponibles). Si por el contrario el usuario introduce los parámetros, el sistema simplemente le obedecerá, obviando la decisión tomada previamente de forma automatizada.

Este método también ayuda a simplificar el uso de nuestro código porque reducimos al mínimo las decisiones necesarias para hacerlo funcionar, aunque siempre mantenemos la posibilidad de configurarlo todo. El usuario de nuestras clases sólo necesitará especificar aquello en lo que esté interesado, dejando a la máquina el resto. Al fin y al cabo la máquina está para simplificar el trabajo a los humanos , no para complicarlo más (al menos así lo veo yo).

Desencriptando.

Para desencriptar el método y parámetros por defecto serán los mismos:

Imports EncryptionClassLibrary

Namespace Cryptography
    Public Class Crypto

        Private pvSymmetricKey As String = "1r+!1-esKªPZyL"

#Region "Enums" [...]

#Region "Symmetric"

    Public Function CryptSymmetric( _
        ByVal pString As String _
        , Optional ByVal pFormat As CryptedStringFormat = CryptedStringFormat.Base64 _
        , Optional ByVal pEncryptionMethod As SymmetricEncryptionMethod = SymmetricEncryptionMethod.Rijndael _
        ) As String [...]

        Public Function DecryptSymmetric( _
            ByVal pString As String _
            , Optional ByVal pFormat As CryptedStringFormat = CryptedStringFormat.Base64 _
            , Optional ByVal pEncryptionMethod As SymmetricEncryptionMethod = SymmetricEncryptionMethod.Rijndael _
            ) As String
            Try
                Dim Secret As String = pString.ToString
                Dim Key As String = pvSymmetricKey
                Dim symAlgorithm As Encryption.Symmetric.Provider

                Select Case pEncryptionMethod
                    Case SymmetricEncryptionMethod.DES
                        symAlgorithm = Encryption.Symmetric.Provider.DES
                    Case SymmetricEncryptionMethod.RC2
                        symAlgorithm = Encryption.Symmetric.Provider.RC2
                    Case SymmetricEncryptionMethod.TripleDES
                        symAlgorithm = Encryption.Symmetric.Provider.TripleDES
                    Case SymmetricEncryptionMethod.Rijndael
                        symAlgorithm = Encryption.Symmetric.Provider.Rijndael
                End Select

                Dim sym As New Encryption.Symmetric(symAlgorithm)
                sym.Key.Text = Key

                Dim decryptedData As New Encryption.Data
                Select Case pFormat
                    Case CryptedStringFormat.Base64
                        decryptedData.Base64 = Secret
                    Case CryptedStringFormat.Hex
                        decryptedData.Hex = Secret
                End Select

                decryptedData = sym.Decrypt(decryptedData)
                Return decryptedData.Text
            Catch ex As Exception
                Throw
            End Try
        End Function     

#End Region

#Region "Properties" [...]

End Class
End Namespace

Por ahora dejamos el ejemplo en este punto. En próximas entregas completaremos la clase para incluir encriptación asimétrica y algoritmos hash.


Espacios de nombres usados en el código de este artículo:

System.Security.Cryptography (usado por la libreria sobre la que se basa el articulo)

 


Fichero con el código de ejemplo: metsuke_savIntroduccion.zip. - 4 KB


Creative Commons License


ir al índice principal del Guille