20 de jan. de 2011

C# Extension Methods (e outros temas tranversais)

O problema

Quem aqui né amigo, não precisou de uma funcionalidade em um tipo básico e como não tinha acabou por criar classes com nome -tipo-Util.

Não entendeu? Bem, vou dar um exemplo. Imagine que no Java eu queira saber se uma String é um número (algo que seria útil no recebimento de parâmetros em requisições Web -que são todos string).

Não existe o método isNumber, isInteger, etc, em String. Existem métodos estáticos em Character, como isDigit.

Então, lá vem os TipoUtil's. Exemplo:

public class Main {
    public static void main(String[] args) {
        
        String parametro = "15.2";
        
        System.out.println(StringUtil.isInteger(parametro));    
    }
}

class StringUtil {
    
    public static boolean isInteger(String string) {        
        for (char c : string.toCharArray())
            if ( ! Character.isDigit(c)) return false;
        
        return true;        
    }
    
}

Neste caso, mostra: false.

Obs.: o método poderia ter sido implementado com expressões regulares.
Obs2.: Existem implementações de bibliotecas utilitárias por vários frameworks, como o Apache: http://commons.apache.org/lang/api-2.5/org/apache/commons/lang/StringUtils.html

E se eu chamar:
if ("641".isInteger()) System.out.println("É um Inteiro");

Não, embora pareça legível, não dá. Eu tenho que usar o método estático:
if (StringUtil.isInteger("641")) System.out.println("É um Inteiro");


Procedural vs Orientado a Objetos

Métodos Estáticos são implementações procedurais -sim, é possível programar proceduralmente em uma linguagem orientada a objetos.

O que denuncia isto é a passagem de um objeto para outro, o qual te dá informações sobre o primeiro. Em outras palavras, não é o próprio objeto que sabe de seu estado, eu tenho que passá-lo a outro para obter informações.

Algumas linguagens, mesmo adicionando orientação a objetos, tem aspectos procedurais. Por exemplo, não sei muito de PHP, mas pelo que conheço para saber o tamanho de um array devo chamar a função count (ou seu alias sizeof- corrijam-me se eu estiver errado):



Poderia ser assim:
echo $frutas.count(); // mas não é

Bem, o C#, assim como o Java, é uma linguagem com chamadas estáticas -o próprio Main é um caso. A implementação do isInteger, como foi feito no Java, a princípio não ficaria muito diferente, veja abaixo:

using System;

class Program {
    static void Main(string[] args) {

        string parametro = "123";
        Console.WriteLine(StringUtil.IsInteger(parametro));

    }
}

class StringUtil {
    public static bool IsInteger(string str) {
        foreach (char c in str.ToCharArray())
            if ( ! Char.IsDigit(c)) return false;
            return true;
    }
}

Quase a mesma coisa não é mesmo?

Então como alterar a classe String para que eu possa fazer "123".IsInteger()? Não tem jeito, a classe String é selada (sealed), ou seja, não é possível estendê-la.



Extension Methods

Entretanto, há um subterfúgio que pode deixar o código "menos feio", os Extension Methods, ou métodos de Extensão.

Exemplo:

using System;

class Program {
    static void Main(string[] args) {

        string parametro = "123";
        Console.WriteLine(parametro.IsInteger());

    }
}

static class StringUtil {
    public static bool IsInteger(this string str) {
        foreach (char c in str.ToCharArray())
            if ( ! Char.IsDigit(c)) return false;

        return true;
    }
}

É necessário adicionar a palavra-chave static na classe (uma classe estática só pode ter métodos estáticos) e a palavra-chave this no parâmetro.

Na prática, é um açúcar sintático, se por exemplo eu chamar: "9971".IsInteger()
O Runtime traduz para StringUtil.IsInteger("9971").

Legal, não? Existem várias implementações que são feitas com Extension Methods, inclusive nos Frameworks ORM da Microsoft, o Entity Framework ou o Linq to SQL.

Obs.: Algumas linguagens permitem que eu reabra qualquer tipo, mesmo os internos, como no Ruby. No Ruby eu posso adicionar qualquer método a qualquer classe em tempo de execução, como por exemplo um método para testar se é inteiro "123123".is_integer?. Abaixo a implementação usando o IRB:

class String
     def is_integer?
         self.to_i.to_s == self
     end
end

"23123".is_integer?
# devolve true
"231.23".is_integer?
# devolve false

Obs.: Na convenção de nomes do Ruby, métodos que retornam booleano terminam com ? (interrogação)


Cultura

Quando tu conheces mais de uma linguagem isto ajuda a mudar a visão, uma mudança de cultura. Por exemplo, em C# e Java, para transformar uma string em inteiro, normalmente chamamos um método estático (procedural) da classe Int32/Integer, como no exemplo:

C#:
int valor = int.Parse("12");

Java:
int valor = Integer.parseInt("12");

No Ruby, por exemplo, o método para converter uma string em inteiro está na própria classe string, exemplo:
"12".to_i

Então, como eu gosto dessa "forma" do Ruby, eu implemento em C# assim nos meus projetos:

public static class StringUtils {

    public static int ToInteger(this string text) {
        int number;
        return int.TryParse(text, out number) ? number : 0;
    }

}

Então eu posso fazer isso em C#:
"12".ToInteger();


Design com Exceções ou Não

Optei por fazer um design sem exceções, ou seja, se eu chamar "dsd".ToInteger() retorna 0. Se na implementação eu tivesse usado int.Parse poderia lançar uma exceção se a string não for válida.

O desenvolvedor sempre pode optar pode fazer o design de funcionalidades para exceção ou não, desde que seja elegante e de acordo com o contexto.

Por exemplo, em um sistema Web, se por exemplo for passado na URL o parâmetro domino/produtos?page=12, e a listagem tiver apenas 10 páginas, o que fazer? Mostrar uma tela de página inválida? Ou mostrar a última página (no caso 10)? Particularmente eu optaria por mostrar a última, e se a página fosse inválida (algo como page=dsfsd) eu mostraria a primeira.

Outro exemplo, se recebo na URL domino/cliente/action=excluir?id=200. O ID não existe, o que fazer? Eliminar o último ID? hehehehe, penso que não, este é um bom exemplo de lançar a exceção.

Cada caso é um caso. Não sei se me fiz entender.



Programação Funcional

O C# tem suporte ao paradigma de programação funcional. Ainda não abordei detalhes a respeito, mas adianto que é muito comum usar extension methods e delegados (ou lambdas).

Por exemplo, supondo que com frequência eu tenha que executar alguma tarefas certo número (finito) de vezes. Normalmente usamos um for com um contador, tipo, i = 0 e i < 20, certo? Bem, então eu posso criar um aspecto funcional, usando Extension Methods e Delegates (delegados) para fazer esse processamento, como no código abaixo:

public static class IntegerUtils {

    public delegate void Proc(int value);

    public static void Times(this int value, Proc proc) {
        for (int i = 0; i < value; i++) proc(i + 1);
    }
}

Para usar, um exemplo:
5.Times(delegate (int i) { Console.WriteLine("Iteração " + i); });

Ou, usando uma lambda:
5.Times(i => Console.WriteLine("Iteração " + i));


Obs.: O Ruby também tem suporte a programação funcional através de blocos e procs. Esta implementação que fiz no C# existe no Ruby e poderia ser feito assim:
5.times { |n| puts "Iteração #{n}" }


Acho que é isso. Se alguém testar e tiver algum erro poste aí, fiz os exemplo na correria.

Abraço a todos