Aracılığıyla paylaş


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 şunların 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

Kanal kilidinin simülasyonu oluşturma

Bu öğreticide, bir C# sınıfı oluşturarak bir kanal kilidininsimülasyonunu yaparsınız. Kısaca, kanal kilidi, farklı seviyelerdeki iki su kütlesi 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 terk ettiği su seviyesine uyacak şekilde değiştirilir. Su seviyesi bu tarafla eşleştiğinde çıkış tarafındaki kapı açılır. Güvenlik önlemleri, 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. CanalLock sınıfı, her iki kapıyı da 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 mevcut durumunu izlemek için özellikleri desteklemesi gerekir. Yöntemleriniz güvenlik önlemlerini uygular.

Sınıf tanımlama

CanalLock sınıfınızı test etmek için bir konsol uygulaması oluşturursunuz. 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 CanalLockolarak adlandırnın. Ardından, genel API'nizi tasarlayın, ancak uygulanmayan yöntemleri bırakın.

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 Main yönteminize 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, CanalLock sınıfındaki 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 ekleyebilirsiniz:

// 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;
}

Şu ana kadar yazdığın 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");
}

Testler başarıyla geçti. Ancak, daha fazla test ekledikçe, daha fazla if yan tümcesi ekler ve farklı özellikleri test edersiniz. Kısa süre içinde, daha fazla koşullu uygulama ekledikçe bu yöntemler çok karmaşık hale gelir.

Komutları desenlerle uygulama

Daha iyi bir yol, nesnenin bir komutu yürütmek için 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çık Yüksek Kapalı
Kapalı Düşük Kapalı
Açık Kapalı Yüksek Açık
Açık Kapalı Düşük Kapatıldı (Hata)
Açık Açık Yüksek Açık
Düşük Kapatıldı (Hata)

Tablodaki dördüncü ve son satırlarda üzeri çizili metin vardır çünkü geçersizdirler. Ş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 (false "Kapalı" olduğunu belirttiğini 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 enum bir 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 ifadesi yalnızca enumiçinde bildirilen değerleri denetler. Uyarıyı kapatmak için, ifadenin son kolu için genel bir yok sayma 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"),

Önce gelen anahtar kolu, tüm girişlerle eşleştiği için switch ifadenizde yer sıralamasında en son olmalıdır. Sıralamada daha erkene taşımayı deneyin. Bu, bir desen içindeki erişilemeyen kod için CS8510 derleyici hatasına neden olur. Switch ifadelerinin doğal yapısı, derleyicinin olası hatalar için hatalar ve uyarılar vermesine olanak tanır. 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. Derleyici, bileşiminiz beklemediğiniz ulaşılamaz kollarla sonuçlanırsa hatalar ve gerekli bir kolu kaldırırsanız uyarılar gönderir.

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

(false, _, _) => false,

Önceki anahtar kolunu ekledikten sonra, komutun falseolduğu her kolunda birer tane olan dört derleyici hatası alacaksınız. Bu kollar zaten yeni eklenen kol tarafından kaplanmış. 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, kapıyı açma komutunun verileceği 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ı istisnayı fırlatmak 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. İşte SetHighGate yönteminin son sürümü:

// 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, SetLowGate ve SetWaterLevel yöntemlerini kendiniz doldurun. 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 düzeyini 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 var. 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 low gate when the water is high"),
        _ => 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.