Atrybuty w C# vs Dekoratory w TypeScript

Czas czytania ~ 150 sekund

Ucząc się TypeScripta możemy dostrzec pewne podobieństwo pomiędzy dostępną tam konstrukcją dekoratorów a atrybutami z C#. Ponieważ oba języki łączy osoba pana Andersa Hejlsberga to coś musi być na rzeczy. Mimo wszystko jednak mamy tu do czynienia z dwoma zupełnie różnymi językami. Jak zatem zrealizować to, co dają atrybuty w TypeScript ? Najpierw zobaczmy, jak działają w C#.

Atrybuty w C#

Atrybuty w C# to nic innego jak metadane na temat naszego kodu. Umożliwiają one wyrażenie pewnych funkcjonalności w sposób deklaratywny – definiujemy tylko co ma się wydarzyć z daną konstrukcją, a nie jak ma to być wykonane, oddzielając w ten sposób definicję od implementacji. Wszystkie informacje, które wyrazimy poprzez atrybuty są emitowane przez kompilator do metadanych assembly i to jedyne, co wnosi sam atrybut. Zdefiniowana w innym miejscu logika może przy użyciu Reflection czytać, jakie atrybuty dekorują nasz kod i wywoływać w zależności od tych atrybutów różne akcje.

Jednym z popularniejszych zastosowań są DataAnnotations zdefiniowane w System.ComponentModel.DataAnnotations. Ich zadaniem jest dekorowanie typów POCO, które później będą serializowane do bazy danych. Na podstawie zadeklarowanych ograniczeń Entity Framework stworzy schemat bazy danych. Zadaniem programisty jest tylko wskazanie ograniczeń. Przykładowo

public class Invoice
{
    [Key]
    public string Id { get; set; }

    [MaxLength(30)]
    public string InternalId { get; set; }

    [Required]
    public string OwnerName { get; set; }

    //...
}

Własne atrbuty

Nic nie stoi na przeszkodzie by budować własne atrybuty. Aby móc dekorować kod wystarczy, stworzyć klasę, która dziedziczy po Attribute. Dodatkowo warto ograniczyć innym programistom możliwości użycia naszego atrybutu, do tych, które nas interesują. Służy do tego atrybut AttributeUsage. A więc atrybutem deklarujemy, gdzie ma być użyty atrybut. Visual Studio kompiluje w tle nasze assembly i jeśli ograniczmy użycie naszego customowego atrybutu to momentalnie dostajemy błędy kompilacji.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
    Inherited = true, AllowMultiple = true)]
public class MyCustomAttribute : Attribute
{
    public int Order { get; set; }
    public string Name { get; set; }
}

Mamy dostępną specjalną składnię do inicjalizowania wartości propertiesów. Wygląda to trochę jak konstruktor, ale działa tylko dla atrybutów i nazywa się positional / named parameters. Warto zapoznać się także z enumem AttributeTargets, jego definicja świadczy o tym, że atrybuty możemy ustawiać dosłownie nad wszystkim.

namespace System
{
    //
    // Summary:
    //     Specifies the application elements on which it is valid to apply an attribute.
    [ComVisible(true)]
    [Flags]
    public enum AttributeTargets
    {
        Assembly = 1,
        Module = 2,
        Class = 4,
        Struct = 8,
        Enum = 16,
        Constructor = 32,
        Method = 64,
        Property = 128,
        Field = 256,
        Event = 512,
        Interface = 1024,
        Parameter = 2048,
        Delegate = 4096,
        ReturnValue = 8192,
        GenericParameter = 16384,
        All = 32767
    }
}

Czytanie z atrybutów

Wartości ustawione w atrybutach możemy czytać za pomocą reflection. W przykładzie poniżej warto zwrócić uwagę na fakt, iż atrybut ograniczony do klas i metod można umieszczać także nad getterami – get to po kompilacji metoda właśnie.

[MyCustom(Order = 0)]
public class Program
{
    public static int MyProperty { [MyCustom(Order = 2)] get; set; }

    [MyCustom(Order = 1)]
    static void Main(string[] args)
    {
        var order = typeof(Program).GetCustomAttributes(typeof(MyCustomAttribute), false)
                                   .OfType<MyCustomAttribute>()
                                   .First().Order;
        Console.WriteLine(order);
        Console.ReadKey();
    }
}

Dekoratory w TypeScript

W samym TypeScript na ten moment dekoratory są funkcjonalnością eksperymentalną (wymagają flagi –experimentalDecorators podczas kompilacji). Nie umniejsza to jednak ich rangi, zwłaszcza, że mocno wykorzystuje je Angular 2+ zwany także po prostu Angularem. Obserwując to, jak szybko zmieniają się specyfikacje ECMAScript, całkiem możliwe, że już wkrótce znajdą się one w oficjalnym standardzie ECMA, w końcu programowanie deklaratywne ma swoje niewątpliwe zalety. A jak działają?

Dekorator jest funkcją, która przyjmuje informacje o dekorowanej metodzie / klasie / property i może “udekorować” taką konstrukcję. Poniższy dekorator nie wywoła funkcji jeśli programista spróbuje wywołać ją bez parametrów.

function paramRequired(target: Object, propertyKey: string, descriptor: TypedPropertyDescriptor<any>) {
    let originalMethod = descriptor.value;
    descriptor.value = function(...args: any[]) {
        if(args.length > 0){
            return originalMethod.apply(this, args);
        } else {
            console.log('parameter is required');
        }
        return null;
    };

    return descriptor;
}

A więc tak na prawdę nadpisujemy definicję funkcji dekorując ją sprawdzeniem argumentów. Samo użycie to po prostu adnotacja podobna składniowo do tych z Javy. Nasz kod wywoła się w momencie wywołania funkcji myMethod z klasy MyClass.

class MyClass {
    @paramRequired
    myMethod(arg: string) { 
        return arg.toUpperCase();
    }
}

 

Tak więc mimo analogii pomiędzy językami nie można tutaj mówić o dokładnie takim samym rozwiązaniu. W C# atrybut to tylko dane na temat kodu, w TypeScript dostarczamy od razu implementację.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s