Bagikan melalui


Gunakan pencocokan pola untuk membangun perilaku kelas Anda untuk kode yang lebih baik

Fitur pencocokan pola dalam C# memberikan sintaks untuk mengekspresikan algoritma Anda. Anda dapat menggunakan teknik ini untuk mengimplementasikan perilaku di kelas Anda. Anda dapat menggabungkan desain kelas berorientasi objek dengan implementasi berorientasi data untuk menyediakan kode ringkas saat memodelkan objek dunia nyata.

Dalam tutorial ini, Anda mempelajari cara:

  • Mengekspresikan kelas berorientasi objek Anda menggunakan pola data.
  • Terapkan pola tersebut menggunakan fitur pencocokan pola C#.
  • Manfaatkan diagnostik kompilator untuk memvalidasi implementasi Anda.

Prasyarat

Membangun simulasi kunci kanal

Dalam tutorial ini, Anda membangun kelas C# yang mensimulasikan kunci kanal . Secara singkat, kunci kanal adalah perangkat yang menaikkan dan menurunkan perahu saat mereka melakukan perjalanan antara dua bentang air pada tingkat yang berbeda. Pintu air memiliki dua pintu dan beberapa mekanisme untuk mengubah ketinggian air.

Dalam operasi normalnya, perahu memasuki salah satu pintu air saat ketinggian air di pintu air menyamai ketinggian air di sisi tempat perahu memasuki. Setelah kapal berada di dalam pintu air, ketinggian air diubah agar sesuai dengan ketinggian air tempat perahu meninggalkan pintu air. Setelah ketinggian air cocok dengan sisi itu, gerbang di sisi keluar terbuka. Langkah-langkah keamanan memastikan operator tidak dapat menciptakan situasi berbahaya di kanal. Ketinggian air hanya dapat diubah ketika kedua gerbang ditutup. Paling banyak satu gerbang dapat terbuka. Untuk membuka gerbang, ketinggian air di kunci harus sesuai dengan ketinggian air di luar gerbang yang dibuka.

Anda dapat membangun kelas C# untuk memodelkan perilaku ini. Kelas CanalLock akan mendukung perintah untuk membuka atau menutup salah satu gerbang. Ini akan memiliki perintah lain untuk menaikkan atau menurunkan air. Kelas ini juga harus mendukung properti untuk memantau keadaan kedua gerbang dan ketinggian air saat ini. Metode Anda menerapkan langkah-langkah keamanan.

Menentukan kelas

Anda membangun aplikasi konsol untuk menguji kelas CanalLock Anda. Buat proyek konsol baru untuk .NET 5 menggunakan Visual Studio atau .NET CLI. Kemudian, tambahkan kelas baru dan beri nama CanalLock. Selanjutnya, rancang API publik Anda, tetapi biarkan metode tidak diimplementasikan:

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}.";
}

Kode sebelumnya menginisialisasi objek sehingga kedua gerbang ditutup, dan tingkat air rendah. Selanjutnya, tulis kode pengujian berikut dalam metode Main Anda untuk memandu Anda saat Membuat implementasi pertama kelas:

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

Selanjutnya, tambahkan implementasi pertama dari setiap metode di kelas CanalLock. Kode berikut mengimplementasikan metode kelas tanpa memperhatikan aturan keselamatan. Anda menambahkan tes keselamatan nanti:

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

Tes yang Anda tulis sejauh ini berhasil. Anda menerapkan dasar-dasarnya. Sekarang, tulis tes untuk kondisi kegagalan pertama. Pada akhir tes sebelumnya, kedua gerbang ditutup, dan ketinggian air diatur ke rendah. Tambahkan pengujian untuk mencoba membuka gerbang atas:

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}");

Pengujian ini gagal karena gerbang terbuka. Sebagai implementasi pertama, Anda dapat memperbaikinya dengan kode berikut:

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

Tes Anda berhasil. Namun, saat menambahkan lebih banyak pengujian, Anda menambahkan lebih banyak klausa if dan menguji properti yang berbeda. Segera, metode ini menjadi terlalu rumit saat Anda menambahkan lebih banyak kondisional.

Menerapkan perintah dengan pola

Cara yang lebih baik adalah menggunakan pola untuk menentukan apakah objek dalam keadaan valid untuk menjalankan perintah. Anda dapat mengekspresikan apakah perintah diizinkan sebagai fungsi dari tiga variabel: status gerbang, tingkat air, dan pengaturan baru:

Pengaturan baru Status gerbang Tingkat Air Hasil
Tertutup Tertutup Tinggi Tertutup
Tertutup Tertutup Rendah Tertutup
Tertutup Buka Tinggi Tertutup
Tertutup Buka Rendah Tertutup
Buka Tertutup Tinggi Buka
Buka Tertutup Rendah Ditutup (Kesalahan)
Buka Buka Tinggi Buka
Buka Buka Rendah Ditutup (Kesalahan)

Baris keempat dan terakhir dalam tabel diberi coretan karena teks tersebut tidak valid. Kode yang Anda tambahkan sekarang harus memastikan pintu air tinggi tidak pernah dibuka ketika air rendah. Status tersebut dapat dikodekan sebagai ekspresi sakelar tunggal (ingat bahwa false menunjukkan "Tertutup"):

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

Coba versi ini. Tes Anda berhasil, memvalidasi kode. Tabel lengkap menunjukkan kemungkinan kombinasi input dan hasil. Itu berarti Anda dan pengembang lain dapat dengan cepat melihat tabel dan melihat bahwa Anda mencakup semua input yang mungkin. Bahkan lebih mudah, pengkompilasi juga dapat membantu. Setelah menambahkan kode sebelumnya, Anda dapat melihat bahwa pengkompilasi menghasilkan peringatan: CS8524 menunjukkan ekspresi pengalihan tidak mencakup semua input yang mungkin. Alasan peringatan tersebut adalah bahwa salah satu input adalah jenis enum. Pengkompilasi menginterpretasikan "semua input yang mungkin" sebagai semua input dari tipe dasar, biasanya int. Ekspresi switch ini hanya memeriksa nilai yang dideklarasikan dalam enum. Untuk menghapus peringatan, Anda dapat menambahkan pola tangkapan serbaguna untuk bagian terakhir dari ekspresi. Kondisi ini melemparkan pengecualian, karena menunjukkan input yang tidak valid:

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

Lengan sakelar sebelumnya harus terakhir dalam ekspresi switch Anda karena cocok dengan semua input. Bereksperimenlah dengan memindahkannya lebih awal dalam urutan. Itu menyebabkan kesalahan kompilator CS8510 untuk kode yang tidak dapat dijangkau dalam pola. Struktur alami ekspresi switch memungkinkan pengkompilasi menghasilkan kesalahan dan peringatan untuk kemungkinan kesalahan. Kompilator "jaring pengaman" memudahkan Anda untuk membuat kode yang benar dalam lebih sedikit iterasi, serta memberi kebebasan untuk menggabungkan cabang sakelar dengan karakter pengganti. Kompilator mengeluarkan kesalahan jika kombinasi Anda menghasilkan lengan yang tidak dapat dijangkau yang tidak Anda harapkan, dan peringatan jika Anda menghapus lengan yang diperlukan.

Perubahan pertama adalah menggabungkan semua lengan di mana perintahnya adalah menutup gerbang; itu selalu diperbolehkan. Tambahkan kode berikut sebagai lengan pertama dalam ekspresi pengalihan Anda:

(false, _, _) => false,

Setelah Anda menambahkan cabang switch sebelumnya, Anda akan mendapatkan empat error kompilator, satu di setiap cabang tempat perintah falseberada. Lengan-lengan itu sudah ditutupi oleh lengan yang baru ditambahkan. Anda dapat dengan aman menghapus empat baris tersebut. Anda bermaksud lengan sakelar baru ini menggantikan kondisi tersebut.

Selanjutnya, Anda dapat menyederhanakan empat lengan di mana perintahnya adalah membuka gerbang. Dalam kedua kasus di mana ketinggian air tinggi, gerbang dapat dibuka. (Dalam satu, sudah terbuka.) Satu kasus di mana ketinggian air rendah menghasilkan pengecualian, dan yang lainnya seharusnya tidak terjadi. Jika kunci air sudah tidak valid, seharusnya aman untuk melempar pengecualian yang sama. Anda dapat membuat penyederhanaan berikut untuk lengan tersebut:

(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"),

Jalankan pengujian Anda lagi, dan mereka lulus. Berikut adalah versi akhir metode 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"),
    };
}

Menerapkan pola sendiri

Sekarang setelah Anda melihat tekniknya, lengkapi metode SetLowGate dan SetWaterLevel sendiri. Mulailah dengan menambahkan kode berikut untuk menguji operasi yang tidak valid pada metode tersebut:

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}");

Jalankan aplikasi Anda lagi. Anda dapat melihat pengujian baru gagal, dan kunci kanal masuk ke status tidak valid. Cobalah untuk menerapkan metode yang tersisa sendiri. Metode untuk mengatur gerbang bawah harus mirip dengan metode untuk mengatur gerbang atas. Metode yang mengubah ketinggian air memiliki pemeriksaan yang berbeda, tetapi harus mengikuti struktur yang sama. Anda mungkin merasa terbantu dengan menggunakan proses yang sama untuk metode yang mengatur tingkat air. Mulailah dengan keempat input: Status kedua gerbang, keadaan ketinggian air saat ini, dan tingkat air baru yang diminta. Ekspresi pengalihan harus dimulai dengan:

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

Anda memiliki 16 lengan sakelar yang perlu diisi. Kemudian, uji dan sederhanakan.

Apakah Anda membuat metode seperti ini?

// 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"),
    };
}

Tes Anda harus lulus, dan kunci kanal harus beroperasi dengan aman.

Ringkasan

Dalam tutorial ini, Anda belajar menggunakan pencocokan pola untuk memeriksa status internal objek sebelum menerapkan perubahan apa pun pada status tersebut. Anda dapat memeriksa kombinasi properti. Setelah Anda membuat tabel untuk salah satu transisi tersebut, Anda menguji kode Anda, lalu menyederhanakan keterbacaan dan pemeliharaan. Refaktor awal ini mungkin menyarankan refaktor lanjutan yang memvalidasi status internal serta mengelola perubahan API tambahan. Tutorial ini menggabungkan kelas dan objek dengan pendekatan berbasis pola yang lebih berorientasi data untuk mengimplementasikan kelas tersebut.