Daha iyi kod için sınıf davranışınızı oluşturmak için desen eşleştirmeyi kullanma

C# dilindeki desen eşleştirme özellikleri, algoritmalarınızı ifade etmek için söz dizimi sağlar. Sınıflarınızdaki davranışı uygulamak için bu teknikleri kullanabilirsiniz. Gerçek dünya nesnelerini modellerken kısa kod sağlamak için nesne odaklı sınıf tasarımını veri odaklı bir uygulamayla birleştirebilirsiniz.

Bu öğreticide aşağıdakilerin nasıl yapılacağını öğreneceksiniz:

  • Veri desenlerini kullanarak nesne odaklı sınıflarınızı ifade edin.
  • C# desen eşleştirme özelliklerini kullanarak bu desenleri uygulayın.
  • Uygulamanızı doğrulamak için derleyici tanılamalarından yararlanın.

Önkoşullar

Makinenizi .NET çalıştıracak şekilde ayarlamanız gerekir. Visual Studio 2022'yi veya .NET SDK'sını indirin.

Kanal kilidinin simülasyonu oluşturma

Bu öğreticide, kanal kilidinin benzetimini yapacak bir C# sınıfı oluşturacaksınız. Kısaca, kanal kilidi, farklı seviyelerde iki su esnetme arasında seyahat eden tekneleri yükselten ve indiren bir cihazdır. Bir kilidin iki kapısı ve su seviyesini değiştirmek için bir mekanizma vardır.

Normal çalışmasında, kilitteki su seviyesi teknenin girdiği taraftaki su seviyesiyle eşleşirken, bir tekne kapılardan birine girer. Kilitlendikten sonra, su seviyesi teknenin kilidi bırakacağı su seviyesiyle eşleşecek şekilde değiştirilir. Su seviyesi bu tarafla eşleştiğinde çıkış tarafındaki kapı açılır. Kasa ölçüler, operatörün kanalda tehlikeli bir durum oluşturamamasını sağlar. Su seviyesi yalnızca her iki kapı da kapalı olduğunda değiştirilebilir. En fazla bir kapı açık olabilir. Bir kapıyı açmak için kilitteki su seviyesi, açılan kapının dışındaki su seviyesiyle eşleşmelidir.

Bu davranışı modellemek için bir C# sınıfı oluşturabilirsiniz. Bir CanalLock sınıf, iki geçidi de açma veya kapatma komutlarını destekler. Suyu yükseltmek veya düşürmek için başka komutlar da vardır. Sınıfın ayrıca hem kapıların hem de su seviyesinin geçerli durumunu okumak için özellikleri desteklemesi gerekir. Yöntemleriniz güvenlik önlemlerini uygular.

Sınıf tanımlama

Sınıfınızı CanalLock test etmek için bir konsol uygulaması oluşturacaksınız. Visual Studio veya .NET CLI kullanarak .NET 5 için yeni bir konsol projesi oluşturun. Ardından, yeni bir sınıf ekleyin ve adını verin CanalLock. Ardından, genel API'nizi tasarlar, ancak uygulanmayan yöntemleri bırakır:

public enum WaterLevel
{
    Low,
    High
}
public class CanalLock
{
    // Query canal lock state:
    public WaterLevel CanalLockWaterLevel { get; private set; } = WaterLevel.Low;
    public bool HighWaterGateOpen { get; private set; } = false;
    public bool LowWaterGateOpen { get; private set; } = false;

    // Change the upper gate.
    public void SetHighGate(bool open)
    {
        throw new NotImplementedException();
    }

    // Change the lower gate.
    public void SetLowGate(bool open)
    {
        throw new NotImplementedException();
    }

    // Change water level.
    public void SetWaterLevel(WaterLevel newLevel)
    {
        throw new NotImplementedException();
    }

    public override string ToString() =>
        $"The lower gate is {(LowWaterGateOpen ? "Open" : "Closed")}. " +
        $"The upper gate is {(HighWaterGateOpen ? "Open" : "Closed")}. " +
        $"The water level is {CanalLockWaterLevel}.";
}

Yukarıdaki kod, her iki kapının da kapatılması ve su düzeyinin düşük olması için nesneyi başlatır. Ardından, sınıfın ilk uygulamasını oluştururken size yol göstermesi için yönteminize Main aşağıdaki test kodunu yazın:

// Create a new canal lock:
var canalGate = new CanalLock();

// State should be doors closed, water level low:
Console.WriteLine(canalGate);

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate:  {canalGate}");

Console.WriteLine("Boat enters lock from lower gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate:  {canalGate}");

canalGate.SetWaterLevel(WaterLevel.High);
Console.WriteLine($"Raise the water level: {canalGate}");

canalGate.SetHighGate(open: true);
Console.WriteLine($"Open the higher gate:  {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");
Console.WriteLine("Boat enters lock from upper gate");

canalGate.SetHighGate(open: false);
Console.WriteLine($"Close the higher gate: {canalGate}");

canalGate.SetWaterLevel(WaterLevel.Low);
Console.WriteLine($"Lower the water level: {canalGate}");

canalGate.SetLowGate(open: true);
Console.WriteLine($"Open the lower gate:  {canalGate}");

Console.WriteLine("Boat exits lock at upper gate");

canalGate.SetLowGate(open: false);
Console.WriteLine($"Close the lower gate:  {canalGate}");

Ardından, sınıfındaki CanalLock her yöntemin ilk uygulamasını ekleyin. Aşağıdaki kod, güvenlik kurallarıyla ilgilenmeden sınıfının yöntemlerini uygular. Güvenlik testlerini daha sonra ekleyeceksiniz:

// Change the upper gate.
public void SetHighGate(bool open)
{
    HighWaterGateOpen = open;
}

// Change the lower gate.
public void SetLowGate(bool open)
{
    LowWaterGateOpen = open;
}

// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
    CanalLockWaterLevel = newLevel;
}

Şimdiye kadar yazdığınız testler geçti. Temel bilgileri uyguladınız. Şimdi ilk hata koşulu için bir test yazın. Önceki testlerin sonunda her iki kapı da kapatılır ve su seviyesi düşük olarak ayarlanır. Üst geçidi açmayı denemek için bir test ekleyin:

Console.WriteLine("=============================================");
Console.WriteLine("     Test invalid commands");
// Open "wrong" gate (2 tests)
try
{
    canalGate = new CanalLock();
    canalGate.SetHighGate(open: true);
}
catch (InvalidOperationException)
{
    Console.WriteLine("Invalid operation: Can't open the high gate. Water is low.");
}
Console.WriteLine($"Try to open upper gate: {canalGate}");

Kapı açıldığından bu test başarısız oluyor. İlk uygulama olarak aşağıdaki kodla düzeltebilirsiniz:

// Change the upper gate.
public void SetHighGate(bool open)
{
    if (open && (CanalLockWaterLevel == WaterLevel.High))
        HighWaterGateOpen = true;
    else if (open && (CanalLockWaterLevel == WaterLevel.Low))
        throw new InvalidOperationException("Cannot open high gate when the water is low");
}

Testlerin geçti. Ancak, daha fazla test ekledikçe, daha fazla if yan tümce ekleyip farklı özellikleri test edersiniz. Kısa süre içinde, daha fazla koşullu uygulama ekledikçe bu yöntemler çok karmaşık hale gelecektir.

Komutları desenlerle uygulama

Daha iyi bir yol, komutu yürütmek için nesnenin geçerli bir durumda olup olmadığını belirlemek için desenleri kullanmaktır. Bir komuta üç değişkenin işlevi olarak izin verilip verilmediğini ifade edebilirsiniz: geçidin durumu, su düzeyi ve yeni ayar:

Yeni ayar Kapı durumu Su Seviyesi Sonuç
Kapalı Kapalı Yüksek Kapalı
Kapalı Kapalı Düşük Kapalı
Kapalı Açılış Yüksek Kapalı
Kapalı Düşük Kapalı
Açık Closed Yüksek Açılış
Açık Closed Düşük Kapatıldı (Hata)
Açılış Açılış Yüksek Açılış
Düşük Kapatıldı (Hata)

Tablodaki dördüncü ve son satırlar geçersiz oldukları için metnin üzerini çizer. Şimdi eklediğiniz kod, su düşük olduğunda yüksek su kapısının asla açılmadığından emin olmalıdır. Bu durumlar tek bir anahtar ifadesi olarak kodlanabilir (bunun "Kapalı" olduğunu belirttiğini false unutmayın):

HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
{
    (false, false, WaterLevel.High) => false,
    (false, false, WaterLevel.Low) => false,
    (false, true, WaterLevel.High) => false,
    (false, true, WaterLevel.Low) => false, // should never happen
    (true, false, WaterLevel.High) => true,
    (true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
    (true, true, WaterLevel.High) => true,
    (true, true, WaterLevel.Low) => false, // should never happen
};

Bu sürümü deneyin. Testleriniz geçer ve kodu doğrular. Tam tablo, olası giriş ve sonuç birleşimlerini gösterir. Bu, sizin ve diğer geliştiricilerin tabloya hızla bakabileceğiniz ve olası tüm girişleri kapsadığınız anlamına gelir. Daha da kolay, derleyici de yardımcı olabilir. Önceki kodu ekledikten sonra, derleyicinin bir uyarı oluşturduğunu görebilirsiniz: CS8524 , anahtar ifadesinin tüm olası girişleri kapsamadığını gösterir. Bu uyarının nedeni, girişlerden birinin bir enum tür olmasıdır. Derleyici ,"tüm olası girişleri" temel alınan türdeki tüm girişler (genellikle bir int) olarak yorumlar. Bu switch ifade yalnızca içinde enumbildirilen değerleri denetler. Uyarıyı kaldırmak için, ifadenin son kolu için tümünü yakala atma deseni ekleyebilirsiniz. Bu koşul, geçersiz girişi gösterdiği için bir özel durum oluşturur:

_  => throw new InvalidOperationException("Invalid internal state"),

Önceki anahtar kolu, tüm girişlerle eşleştiğinden ifadenizde switch son olmalıdır. Daha önce sırasıyla taşıyarak denemeler yapın. Bu, bir desende erişilemeyen kod için CS8510 derleyici hatasına neden olur. Anahtar ifadelerinin doğal yapısı, derleyicinin olası hatalar için hatalar ve uyarılar oluşturmasını sağlar. Derleyici "safety net" daha az yinelemede doğru kod oluşturmanızı ve anahtar kollarını joker karakterlerle birleştirme özgürlüğünü kolaylaştırır. Birleşiminiz beklemediğiniz ulaşılamaz kollarla sonuçlanırsa derleyici hata ve gerekli bir kolu kaldırırsanız uyarı gönderir.

İlk değişiklik, komutun geçidi kapatmak olduğu tüm kolları birleştirmektir; Buna her zaman izin verilir. Switch ifadenizde ilk kol olarak aşağıdaki kodu ekleyin:

(false, _, _) => false,

Önceki anahtar kolunu ekledikten sonra, komutun falseolduğu her kolda biri olan dört derleyici hatası alırsınız. Bu kollar zaten yeni eklenen kolla kaplı. Bu dört satırı güvenle kaldırabilirsiniz. Bu yeni anahtar kolunu bu koşulları değiştirmek için tasarlamıştınız.

Ardından, komutanın geçidi açması gereken dört kolu basitleştirebilirsiniz. Su seviyesinin yüksek olduğu her iki durumda da kapı açılabilir. (Birinde zaten açık.) Su seviyesinin düşük olduğu durumlardan biri özel durum oluşturur ve diğeri gerçekleşmemelidir. Su kilidi zaten geçersiz durumdaysa aynı özel durumu atmak güvenli olmalıdır. Bu kollar için aşağıdaki basitleştirmeleri yapabilirsiniz:

(true, _, WaterLevel.High) => true,
(true, false, WaterLevel.Low) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
_ => throw new InvalidOperationException("Invalid internal state"),

Testlerinizi tekrar çalıştırın ve geçerler. Yöntemin son sürümü aşağıdadır SetHighGate :

// Change the upper gate.
public void SetHighGate(bool open)
{
    HighWaterGateOpen = (open, HighWaterGateOpen, CanalLockWaterLevel) switch
    {
        (false, _,    _)               => false,
        (true, _,     WaterLevel.High) => true,
        (true, false, WaterLevel.Low)  => throw new InvalidOperationException("Cannot open high gate when the water is low"),
        _                              => throw new InvalidOperationException("Invalid internal state"),
    };
}

Desenleri kendiniz uygulayın

Tekniği gördüğünüze göre ve SetWaterLevel yöntemlerini kendiniz doldurunSetLowGate. Bu yöntemlerde geçersiz işlemleri test etmek için aşağıdaki kodu ekleyerek başlayın:

Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetWaterLevel(WaterLevel.High);
    canalGate.SetLowGate(open: true);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't open the lower gate. Water is high.");
}
Console.WriteLine($"Try to open lower gate: {canalGate}");
// change water level with gate open (2 tests)
Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetLowGate(open: true);
    canalGate.SetWaterLevel(WaterLevel.High);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't raise water when the lower gate is open.");
}
Console.WriteLine($"Try to raise water with lower gate open: {canalGate}");
Console.WriteLine();
Console.WriteLine();
try
{
    canalGate = new CanalLock();
    canalGate.SetWaterLevel(WaterLevel.High);
    canalGate.SetHighGate(open: true);
    canalGate.SetWaterLevel(WaterLevel.Low);
}
catch (InvalidOperationException)
{
    Console.WriteLine("invalid operation: Can't lower water when the high gate is open.");
}
Console.WriteLine($"Try to lower water with high gate open: {canalGate}");

Uygulamanızı yeniden çalıştırın. Yeni testlerin başarısız olduğunu ve kanal kilidinin geçersiz bir duruma geldiğini görebilirsiniz. Kalan yöntemleri kendiniz uygulamayı deneyin. Alt geçidi ayarlama yöntemi, üst geçidi ayarlama yöntemine benzer olmalıdır. Su seviyesini değiştiren yöntemin farklı denetimleri vardır, ancak benzer bir yapıyı izlemelidir. Su seviyesini ayarlayan yöntem için aynı işlemi kullanmayı yararlı bulabilirsiniz. Dört girişle de başlayın: Her iki kapının durumu, su seviyesinin geçerli durumu ve istenen yeni su seviyesi. Switch ifadesi şu şekilde başlamalıdır:

CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
{
    // elided
};

Doldurmanız gereken toplam 16 anahtar kolu olacak. Ardından test edin ve basitleştirin.

Bunun gibi bir yöntem mi yaptınız?

// Change the lower gate.
public void SetLowGate(bool open)
{
    LowWaterGateOpen = (open, LowWaterGateOpen, CanalLockWaterLevel) switch
    {
        (false, _, _) => false,
        (true, _, WaterLevel.Low) => true,
        (true, false, WaterLevel.High) => throw new InvalidOperationException("Cannot open high gate when the water is low"),
        _ => throw new InvalidOperationException("Invalid internal state"),
    };
}

// Change water level.
public void SetWaterLevel(WaterLevel newLevel)
{
    CanalLockWaterLevel = (newLevel, CanalLockWaterLevel, LowWaterGateOpen, HighWaterGateOpen) switch
    {
        (WaterLevel.Low, WaterLevel.Low, true, false) => WaterLevel.Low,
        (WaterLevel.High, WaterLevel.High, false, true) => WaterLevel.High,
        (WaterLevel.Low, _, false, false) => WaterLevel.Low,
        (WaterLevel.High, _, false, false) => WaterLevel.High,
        (WaterLevel.Low, WaterLevel.High, false, true) => throw new InvalidOperationException("Cannot lower water when the high gate is open"),
        (WaterLevel.High, WaterLevel.Low, true, false) => throw new InvalidOperationException("Cannot raise water when the low gate is open"),
        _ => throw new InvalidOperationException("Invalid internal state"),
    };
}

Testlerinizin geçmesi ve kanal kilidinin güvenli bir şekilde çalışması gerekir.

Özet

Bu öğreticide, bu duruma herhangi bir değişiklik uygulamadan önce nesnenin iç durumunu denetlemek için desen eşleştirme kullanmayı öğrendiniz. Özelliklerin birleşimlerini de kontrol edebilirsiniz. Bu geçişlerden herhangi biri için tablolar derledikten sonra kodunuzu test eder, ardından okunabilirlik ve bakım için basitleştirirsiniz. Bu ilk yeniden düzenlemeler, iç durumu doğrulayan veya diğer API değişikliklerini yöneten daha fazla yeniden düzenleme önerebilir. Bu öğreticide, bu sınıfları uygulamak için daha veri odaklı, desen tabanlı bir yaklaşımla sınıflar ve nesneler birleştirildi.