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)
|
¿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.ToBase64Si lo analizamos con calma, el código en si no es excesivamente complicado. En realidad las operaciones que se realizan son las siguientes:
- Creamos el proveedor de encriptación, indicando que el método a usar es Rijndael.
- A continuación se genera la clave de encriptación, quedando almacenada en forma de objeto de tipo Encryption.Data.
- En este momento creamos otro objeto del mismo tipo que almacenará nuestro 'secretillo' una vez encriptado.
- Este paso simplemente ejecuta la encriptación y recupera el objeto que contiene nuestra cadena encriptada.
- 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 StringSimplemente 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 NamespaceComo 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 NamespacePara este caso entiendo que hay dos vías de implementación (probablemente hay más, pero para este caso se me ocurren estas dos):
- La primera es la de crear la variable privada sin inicializarla y lanzar una excepción si se trata de lanzar la encriptación sin asignar una clave.
- La segunda es asignar un valor por defecto y que el programador decida después si asignar una nueva clave o no, dependiendo de sus necesidades y/o gustos. La excepción en este caso se lanzaría si se asigna en blanco a través de la propiedad.
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 TryDim 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 NamespaceEste 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:
- CryptSymmetric(texto)
- CryptSymmetric(texto,formato_devolucion)
- 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:
- 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 NamespacePor 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