Temsilciler için ortak desenler

Geri

Temsilciler, bileşenler arasında minimum bağlantı içeren yazılım tasarımları sağlayan bir mekanizma sağlar.

Bu tür bir tasarım için mükemmel bir örnek LINQ'tir. LINQ Sorgu İfadesi Düzeni, tüm özellikleri için temsilcilere dayanır. Şu basit örneği göz önünde bulundurun:

var smallNumbers = numbers.Where(n => n < 10);

Bu, sayıların sırasını yalnızca 10 değerinden küçük olanlara filtreler. yöntemi, Where bir dizinin hangi öğelerinin filtreyi geçirdiğini belirleyen bir temsilci kullanır. Bir LINQ sorgusu oluşturduğunuzda, temsilcinin bu özel amaç için uygulamasını sağlarsınız.

Where yönteminin prototipi:

public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);

Bu örnek, LINQ'in parçası olan tüm yöntemlerle yinelenir. Hepsi, belirli bir sorguyu yöneten kod için temsilcilere güvenir. Bu API tasarım düzeni, öğrenmesi ve anlaması gereken güçlü bir modeldir.

Bu basit örnek, temsilcilerin bileşenler arasında nasıl çok az bağlantı gerektirdiğini göstermektedir. Belirli bir temel sınıftan türetilen bir sınıf oluşturmanız gerekmez. Belirli bir arabirim uygulamanız gerekmez. Tek gereksinim, eldeki görevin temeli olan tek bir yöntemin uygulanmasını sağlamaktır.

Temsilcilerle Kendi Bileşenlerinizi Oluşturma

Şimdi temsilcilere dayalı bir tasarım kullanarak bir bileşen oluşturarak bu örneği oluşturalım.

Şimdi büyük bir sistemde günlük iletileri için kullanılabilecek bir bileşen tanımlayalım. Kitaplık bileşenleri birçok farklı ortamda ve birden çok farklı platformda kullanılabilir. Bileşende günlükleri yöneten birçok yaygın özellik vardır. Sistemdeki herhangi bir bileşenden gelen iletileri kabul etmeniz gerekir. Bu iletiler, temel bileşenin yönetebileceği farklı öncelikler içerir. İletilerin arşivlenmiş son biçiminde zaman damgaları olmalıdır. Daha gelişmiş senaryolar için iletileri kaynak bileşene göre filtreleyebilirsiniz.

Özelliğin sık sık değiştirilecek bir yönü vardır: iletilerin yazıldığı yer. Bazı ortamlarda bunlar hata konsoluna yazılabilir. Diğerlerinde bir dosya. Diğer olasılıklar veritabanı depolama, işletim sistemi olay günlükleri veya diğer belge depolama alanlarıdır.

Farklı senaryolarda kullanılabilecek çıkış bileşimleri de vardır. Konsola ve bir dosyaya ileti yazmak isteyebilirsiniz.

Temsilcilere dayalı bir tasarım çok fazla esneklik sağlar ve gelecekte eklenebilecek depolama mekanizmalarını desteklemeyi kolaylaştırır.

Bu tasarımda, birincil günlük bileşeni sanal olmayan, hatta korumalı bir sınıf olabilir. İletileri farklı depolama medyasına yazmak için herhangi bir temsilci kümesini takabilirsiniz. Çok noktaya yayın temsilcileri için yerleşik destek, iletilerin birden çok konuma (dosya ve konsol) yazılması gereken senaryoları desteklemeyi kolaylaştırır.

İlk Uygulama

Küçük bir başlangıç yapalım: İlk uygulama yeni iletileri kabul eder ve ekli herhangi bir temsilciyi kullanarak bunları yazar. Konsola ileti yazan bir temsilciyle başlayabilirsiniz.

public static class Logger
{
    public static Action<string>? WriteMessage;

    public static void LogMessage(string msg)
    {
        if (WriteMessage is not null)
            WriteMessage(msg);
    }
}

Yukarıdaki statik sınıf, çalışabilecek en basit şeydir. Konsola ileti yazan yöntem için tek bir uygulama yazmamız gerekir:

public static class LoggingMethods
{
    public static void LogToConsole(string message)
    {
        Console.Error.WriteLine(message);
    }
}

Son olarak, temsilciyi günlükçüde bildirilen WriteMessage temsilcisine ekleyerek bağlamanız gerekir:

Logger.WriteMessage += LoggingMethods.LogToConsole;

Uygulamalar

Şimdiye kadarki örneğimiz oldukça basittir, ancak yine de temsilcileri içeren tasarımlar için bazı önemli yönergeleri gösterir.

Çekirdek çerçevede tanımlanan temsilci türlerinin kullanılması, kullanıcıların temsilcilerle çalışmasını kolaylaştırır. Yeni türler tanımlamanız gerekmez ve kitaplığınızı kullanan geliştiricilerin yeni, özelleştirilmiş temsilci türlerini öğrenmesi gerekmez.

Kullanılan arabirimler olabildiğince az ve esnektir: Yeni bir çıkış günlükçü oluşturmak için bir yöntem oluşturmanız gerekir. Bu yöntem statik bir yöntem veya bir örnek yöntemi olabilir. Herhangi bir erişimi olabilir.

Çıktıyı Biçimlendir

Şimdi bu ilk sürümü biraz daha sağlam hale getirelim ve ardından diğer günlük mekanizmalarını oluşturmaya başlayalım.

Şimdi, günlük sınıfınızın daha yapılandırılmış iletiler oluşturması için yöntemine LogMessage() birkaç bağımsız değişken ekleyelim:

public enum Severity
{
    Verbose,
    Trace,
    Information,
    Warning,
    Error,
    Critical
}
public static class Logger
{
    public static Action<string>? WriteMessage;

    public static void LogMessage(Severity s, string component, string msg)
    {
        var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
        if (WriteMessage is not null)
            WriteMessage(outputMsg);
    }
}

Şimdi bu bağımsız değişkeni kullanarak günlüğün Severity çıkışına gönderilen iletileri filtreleyelim.

public static class Logger
{
    public static Action<string>? WriteMessage;

    public static Severity LogLevel { get; set; } = Severity.Warning;

    public static void LogMessage(Severity s, string component, string msg)
    {
        if (s < LogLevel)
            return;

        var outputMsg = $"{DateTime.Now}\t{s}\t{component}\t{msg}";
        if (WriteMessage is not null)
            WriteMessage(outputMsg);
    }
}

Uygulamalar

Günlük altyapısına yeni özellikler eklediniz. Günlükçü bileşeni herhangi bir çıkış mekanizmasına çok gevşek bir şekilde bağlı olduğundan, bu yeni özellikler günlükçü temsilcisini uygulayan kodun hiçbirine etkisi olmadan eklenebilir.

Bunu oluşturmaya devam ettikçe, bu gevşek bağlantının diğer konumlarda değişiklik yapmadan sitenin bölümlerini güncelleştirmede nasıl daha fazla esneklik sağladığına ilişkin daha fazla örnek göreceksiniz. Aslında, daha büyük bir uygulamada günlükçü çıkış sınıfları farklı bir derlemede olabilir ve hatta yeniden oluşturulması gerekmez.

İkinci Bir Çıkış Altyapısı Oluşturma

Günlük bileşeni iyi geliyor. Şimdi bir dosyaya iletileri günlüğe kaydeden bir çıkış altyapısı daha ekleyelim. Bu biraz daha ilgili bir çıkış altyapısı olacaktır. Dosya işlemlerini kapsülleyen ve her yazmadan sonra dosyanın her zaman kapatılmasını sağlayan bir sınıf olacaktır. Bu, her ileti oluşturulduktan sonra tüm verilerin diske boşaltılmasını sağlar.

Dosya tabanlı günlükçü şu şekildedir:

public class FileLogger
{
    private readonly string logPath;
    public FileLogger(string path)
    {
        logPath = path;
        Logger.WriteMessage += LogMessage;
    }

    public void DetachLog() => Logger.WriteMessage -= LogMessage;
    // make sure this can't throw.
    private void LogMessage(string msg)
    {
        try
        {
            using (var log = File.AppendText(logPath))
            {
                log.WriteLine(msg);
                log.Flush();
            }
        }
        catch (Exception)
        {
            // Hmm. We caught an exception while
            // logging. We can't really log the
            // problem (since it's the log that's failing).
            // So, while normally, catching an exception
            // and doing nothing isn't wise, it's really the
            // only reasonable option here.
        }
    }
}

Bu sınıfı oluşturduktan sonra örneği oluşturabilirsiniz ve LogMessage yöntemini Logger bileşenine ekler:

var file = new FileLogger("log.txt");

Bu ikisi birbirini dışlamaz. Hem günlük yöntemlerini ekleyebilir hem de konsola ve bir dosyaya ileti oluşturabilirsiniz:

var fileOutput = new FileLogger("log.txt");
Logger.WriteMessage += LoggingMethods.LogToConsole; // LoggingMethods is the static class we utilized earlier

Daha sonra, aynı uygulamada bile, sistemde başka bir sorun olmadan temsilcilerden birini kaldırabilirsiniz:

Logger.WriteMessage -= LoggingMethods.LogToConsole;

Uygulamalar

Şimdi, günlüğe kaydetme alt sistemi için ikinci bir çıkış işleyicisi eklediniz. Bunun dosya sistemini doğru şekilde desteklemek için biraz daha altyapıya ihtiyacı var. Temsilci bir örnek yöntemidir. Ayrıca özel bir yöntemdir. Temsilci altyapısı temsilcileri birbirine bağlayabildiği için daha fazla erişilebilirlik gerekmez.

İkincisi, temsilci tabanlı tasarım ek kod olmadan birden çok çıkış yöntemini etkinleştirir. Birden çok çıkış yöntemini desteklemek için ek altyapı oluşturmanız gerekmez. Çağrı listesinde başka bir yöntem haline gelirler.

Dosya günlüğü çıkış yöntemindeki koda özellikle dikkat edin. Özel durum oluşturmadığından emin olmak için kodlanır. Bu her zaman kesinlikle gerekli olmasa da, genellikle iyi bir uygulamadır. Temsilci yöntemlerinden biri özel durum oluşturursa, çağrıda bulunan kalan temsilciler çağrılmayacak.

Son not olarak, dosya günlükçüsunun her günlük iletisinde dosyayı açıp kapatarak kaynaklarını yönetmesi gerekir. Dosyayı açık tutmayı ve tamamlandığında dosyayı kapatmak için uygulamayı IDisposable seçebilirsiniz. Her iki yöntemin de avantajları ve dezavantajları vardır. Her ikisi de sınıflar arasında biraz daha fazla bağlama oluşturur.

Her iki senaryoyu da desteklemek için sınıftaki Logger kodlardan hiçbirinin güncelleştirilmesi gerekmez.

Null Temsilcileri İşleme

Son olarak LogMessage yöntemini, hiçbir çıkış mekanizması seçilmediğinde bu durumlar için sağlam olacak şekilde güncelleştirelim. Geçerli uygulama, temsilcinin WriteMessage bir çağrı listesi eklenmediğinde bir NullReferenceException oluşturur. Hiçbir yöntem eklenmediğinde sessizce devam eden bir tasarım tercih edebilirsiniz. Bu, null koşullu işlecini kullanarak yöntemiyle birlikte kolayca değiştirilebilir Delegate.Invoke() :

public static void LogMessage(string msg)
{
    WriteMessage?.Invoke(msg);
}

Sol işlenen (bu örnekte) null olduğunda null koşullu işleç (?.WriteMessage) kısa devreler, bir iletiyi günlüğe kaydetme girişiminde bulunulmadığı anlamına gelir.

veya System.MulticastDelegatebelgelerinde System.Delegate listelenen yöntemi bulamazsınızInvoke(). Derleyici, bildirilen herhangi bir temsilci türü için tür güvenli Invoke bir yöntem oluşturur. Bu örnekte bu, tek string bir bağımsız değişken aldığı ve geçersiz bir dönüş türüne sahip olduğu anlamına gelirInvoke.

Uygulamaların Özeti

Diğer yazıcılar ve diğer özelliklerle genişletilebilen bir günlük bileşeninin başlangıçlarını gördünüz. Tasarımda temsilciler kullanıldığında, bu farklı bileşenler gevşek bir şekilde birleştirilmiştir. Bu, çeşitli avantajlar sağlar. Yeni çıkış mekanizmaları oluşturmak ve bunları sisteme eklemek kolaydır. Bu diğer mekanizmalar yalnızca bir yönteme ihtiyaç duyar: günlük iletisini yazan yöntem. Yeni özellikler eklendiğinde dayanıklı olan bir tasarımdır. Herhangi bir yazar için gereken sözleşme tek bir yöntem uygulamaktır. Bu yöntem statik veya örnek bir yöntem olabilir. Genel, özel veya başka herhangi bir yasal erişim olabilir.

Logger sınıfı, hataya neden olan değişiklikler yapmadan herhangi bir sayıda geliştirme veya değişiklik yapabilir. Herhangi bir sınıf gibi, değişiklikleri bozma riski olmadan genel API'yi değiştiremezsiniz. Ancak, günlükçü ile herhangi bir çıkış altyapısı arasındaki bağlantı yalnızca temsilci üzerinden olduğundan, başka hiçbir tür (arabirimler veya temel sınıflar gibi) söz konusu değildir. Bağlantı mümkün olduğunca küçüktür.

İleri