Database
 sql >> Teknologi Basis Data >  >> RDS >> Database

Indikator Kunci Desain Masalah

Konsep desain yang baik atau buruk itu relatif. Pada saat yang sama, ada beberapa standar pemrograman, yang dalam banyak kasus menjamin efektivitas, pemeliharaan, dan kemampuan pengujian. Misalnya, dalam bahasa berorientasi objek, ini adalah penggunaan enkapsulasi, pewarisan, dan polimorfisme. Ada satu set pola desain yang dalam beberapa kasus memiliki efek positif atau negatif pada desain aplikasi tergantung pada situasinya. Di sisi lain, ada kebalikannya, yang terkadang mengarah pada desain masalah.

Desain ini biasanya memiliki indikator berikut (satu atau beberapa sekaligus):

  • Kekakuan (sulit untuk mengubah kode, karena perubahan sederhana mempengaruhi banyak tempat);
  • Imobilitas (rumit untuk membagi kode menjadi modul yang dapat digunakan di program lain);
  • Viskositas (cukup sulit untuk mengembangkan atau menguji kode);
  • Kompleksitas yang tidak perlu (ada fungsi yang tidak digunakan dalam kode);
  • Pengulangan yang tidak perlu (Salin/Tempel);
  • Keterbacaan yang buruk (sulit untuk memahami untuk apa kode dirancang dan untuk memeliharanya);
  • Kerapuhan (fungsinya mudah rusak bahkan dengan perubahan kecil).

Anda harus dapat memahami dan membedakan fitur-fitur ini untuk menghindari masalah desain atau untuk memprediksi kemungkinan konsekuensi dari penggunaannya. Indikator-indikator ini dijelaskan dalam buku «Agile Principles, Patterns, And Practices in C#» oleh Robert Martin. Namun, ada deskripsi singkat dan tidak ada contoh kode di artikel ini maupun di artikel ulasan lainnya.

Kami akan menghilangkan kelemahan ini dengan memikirkan setiap fitur.

Kekakuan

Seperti yang telah disebutkan, kode yang kaku sulit untuk dimodifikasi, bahkan hal-hal terkecil. Ini mungkin tidak menjadi masalah jika kode tidak sering diubah atau tidak diubah sama sekali. Jadi, kodenya ternyata cukup bagus. Namun, jika perlu untuk mengubah kode dan sulit untuk melakukannya, itu menjadi masalah, bahkan jika berhasil.

Salah satu kasus kekakuan yang populer adalah secara eksplisit menentukan tipe kelas daripada menggunakan abstraksi (antarmuka, kelas dasar, dll.). Di bawah ini, Anda dapat menemukan contoh kode:

class A
{
  B _b;
  public A()
  {
    _b = new B();
  }

  public void Foo()
  {
    // Do some custom logic.
    _b.DoSomething();
    // Do some custom logic.
  }
}

class B
{
   public void DoSomething()
  {
    // Do something
   }
}

Di sini kelas A sangat bergantung pada kelas B. Jadi, jika di masa mendatang Anda perlu menggunakan kelas lain alih-alih kelas B, ini akan memerlukan perubahan kelas A dan akan membuatnya diuji ulang. Selain itu, jika kelas B mempengaruhi kelas lain, situasinya akan menjadi lebih rumit.

Solusinya adalah abstraksi yang memperkenalkan antarmuka IComponent melalui konstruktor kelas A. Dalam hal ini, tidak akan lagi bergantung pada kelas tertentu dan hanya akan bergantung pada antarmuka IComponent. lass pada gilirannya harus mengimplementasikan antarmuka IComponent.

interface IComponent
{
  void DoSomething();
}

class A
{
  IComponent _component;
  public A(IComponent component)
  {
    _component = component;
  }

  void Foo()
  {
     // Do some custom logic.    
     _component.DoSomething();
     // Do some custom logic.
   }
}

class B : IComponent
{
  void DoSomething()
  {
    // Do something
  }
}

Mari berikan contoh spesifik. Asumsikan ada satu set kelas yang mencatat informasi – ProductManager dan Konsumen. Tugas mereka adalah menyimpan produk dalam database dan memesannya secara bersamaan. Kedua kelas mencatat peristiwa yang relevan. Bayangkan bahwa pada awalnya ada log ke file. Untuk melakukan ini, kelas FileLogger digunakan. Selain itu, kelas terletak di modul (rakitan) yang berbeda.

// Module 1 (Client)
static void Main()
{
  var product = new Product("milk");
  var productManager = new ProductManager();
  productManager.AddProduct(product);
  var consumer = new Consumer();
  consumer.PurchaseProduct(product.Name);
}

// Module 2 (Business logic)
public class ProductManager
{
  private readonly FileLogger _logger = new FileLogger();
  public void AddProduct(Product product)
  {
    // Add the product to the database.
    _logger.Log("The product is added.");
  }
}

public class Consumer
{
  private readonly FileLogger _logger = new FileLogger();
  public void PurchaseProduct(string product)
  {
     // Purchase the product.
    _logger.Log("The product is purchased.");
  }
}

public class Product
{
  public string Name { get; private set; }
  public Product(string name)
  {
    Name = name;
  }
}

// Module 3 (Logger implementation)
public class FileLogger
{
  const string FileName = "log.txt";
  public void Log(string message)
  {
    // Write the message to the file.
  }
}

Jika pada awalnya cukup hanya menggunakan file, dan kemudian menjadi perlu untuk masuk ke repositori lain, seperti database atau layanan pengumpulan dan penyimpanan data berbasis cloud, maka kita perlu mengubah semua kelas dalam logika bisnis modul (Modul 2) yang menggunakan FileLogger. Bagaimanapun, ini bisa menjadi sulit. Untuk mengatasi masalah ini, kami dapat memperkenalkan antarmuka abstrak untuk bekerja dengan logger, seperti yang ditunjukkan di bawah ini.

// Module 1 (Client)
static void Main()
{
  var logger = new FileLogger();
  var product = new Product("milk");
  var productManager = new ProductManager(logger);
  productManager.AddProduct(product);
  var consumer = new Consumer(logger);
  consumer.PurchaseProduct(product.Name);
}

// Module 2 (Business logic)
class ProductManager
{
  private readonly ILogger _logger;
  public ProductManager(ILogger logger)
  {
    _logger = logger;
  }
  
  public void AddProduct(Product product)
  {
    // Add the product to the database.
    _logger.Log("The product is added.");
  }
}

public class Consumer
{
  private readonly ILogger _logger;
  public Consumer(ILogger logger)
  {
    _logger = logger;
  }

  public void PurchaseProduct(string product)
  {
     // Purchase the product.
    _logger.Log("The product is purchased.");
  }
}

public class Product
{
  public string Name { get; private set; }
  public Product(string name)
  {
    Name = name;
  }
}

// Module 3 (interfaces)
public interface ILogger
{
  void Log(string message);
}

// Module 4 (Logger implementation)
public class FileLogger : ILogger
{
  const string FileName = "log.txt";
  public virtual void Log(string message)
  {
    // Write the message to the file.
  }
}

Dalam hal ini, ketika mengubah jenis logger, cukup dengan memodifikasi kode klien (Utama), yang menginisialisasi logger dan menambahkannya ke konstruktor ProductManager dan Konsumen. Jadi, kami menutup kelas logika bisnis dari modifikasi tipe logger sesuai kebutuhan.

Selain tautan langsung ke kelas yang digunakan, kami dapat memantau kekakuan pada varian lain yang dapat menyebabkan kesulitan saat memodifikasi kode. Mungkin ada satu set tak terbatas dari mereka. Namun, kami akan mencoba memberikan contoh lain. Asumsikan ada kode yang menampilkan area pola geometris di konsol.

static void Main()
{
  var rectangle = new Rectangle() { W = 3, H = 5 };
  var circle = new Circle() { R = 7 };
  var shapes = new Shape[] { rectangle, circle  };
  ShapeHelper.ReportShapesSize(shapes);
}

class ShapeHelper
{
  private static double GetShapeArea(Shape shape)
  {
    if (shape is Rectangle)
    {
      return ((Rectangle)shape).W * ((Rectangle)shape).H;
    }
    if (shape is Circle)
    {
      return 2 * Math.PI * ((Circle)shape).R * ((Circle)shape).R;
    }
    throw new InvalidOperationException("Not supported shape");
  }

  public static void ReportShapesSize(Shape[] shapes)
  {
    foreach(Shape shape in shapes)
    {
       if (shape is Rectangle)
       {
         double area = GetShapeArea(shape); 
         Console.WriteLine($"Rectangle's area is {area}");
       }
       if (shape is Circle)
       {
         double area = GetShapeArea(shape); 
         Console.WriteLine($"Circle's area is {area}");
       }
    }
  }
}

public class Shape
{  }

public class Rectangle : Shape
{
  public double W { get; set; }
  public double H { get; set; }
}

public class Circle : Shape
{
  public double R { get; set; }
}

Seperti yang Anda lihat, saat menambahkan pola baru, kita harus mengubah metode kelas ShapeHelper. Salah satu opsinya adalah dengan melewatkan algoritma rendering di kelas pola geometris (Persegi Panjang dan Lingkaran), seperti yang ditunjukkan di bawah ini. Dengan cara ini, kami akan mengisolasi logika yang relevan di kelas terkait sehingga mengurangi tanggung jawab kelas ShapeHelper sebelum menampilkan informasi di konsol.

static void Main()
{
  var rectangle = new Rectangle() { W = 3, H = 5 };
  var circle = new Circle() { R = 7 };
  var shapes = new Shape[]() { rectangle, circle  };
  ShapeHelper.ReportShapesSize(shapes);
}

class ShapeHelper
{
  public static void ReportShapesSize(Shape[] shapes)
  {
    foreach(Shape shape in shapes)
    {
       shape.Report();
    }
  }
}

public abstract class Shape
{
  public abstract void Report();
}

public class Rectangle : Shape
{
  public double W { get; set; }
  public double H { get; set; }
  public override void Report()
  {
     double area = W * H;
     Console.WriteLine($"Rectangle's area is {area}");
  }
}

public class Circle : Shape
{
  public double R { get; set; }
  public override void Report()
  {
     double area = 2 * Math.PI * R * R;
     Console.WriteLine($"Circle's area is {area}");
  }
}

Akibatnya, kami sebenarnya menutup kelas ShapeHelper untuk perubahan yang menambahkan tipe pola baru dengan menggunakan pewarisan dan polimorfisme.

Imobilitas

Kami dapat memantau imobilitas saat membagi kode menjadi modul yang dapat digunakan kembali. Akibatnya, proyek dapat berhenti berkembang dan menjadi kompetitif.

Sebagai contoh, kami akan mempertimbangkan program desktop, yang seluruh kodenya diimplementasikan dalam file aplikasi yang dapat dieksekusi (.exe) dan telah dirancang sedemikian rupa sehingga logika bisnis tidak dibangun dalam modul atau kelas yang terpisah. Kemudian, pengembang menghadapi persyaratan bisnis berikut:

  • Untuk mengubah antarmuka pengguna dengan mengubahnya menjadi aplikasi Web;
  • Untuk mempublikasikan fungsionalitas program sebagai satu set layanan Web yang tersedia untuk klien pihak ketiga untuk digunakan dalam aplikasi mereka sendiri.

Dalam hal ini, persyaratan ini sulit dipenuhi, karena seluruh kode terletak di modul yang dapat dieksekusi.

Gambar di bawah ini menunjukkan contoh desain immobile berbeda dengan yang tidak memiliki indikator ini. Mereka dipisahkan oleh garis putus-putus. Seperti yang Anda lihat, alokasi kode pada modul yang dapat digunakan kembali (Logika), serta publikasi fungsionalitas di tingkat layanan Web, memungkinkan penggunaannya di berbagai aplikasi klien (Aplikasi), yang merupakan manfaat yang tidak diragukan lagi.

Imobilitas juga bisa disebut desain monolitik. Sulit untuk membaginya menjadi unit kode yang lebih kecil dan berguna. Bagaimana kita bisa menghindari masalah ini? Pada tahap desain, lebih baik untuk memikirkan seberapa besar kemungkinan menggunakan fitur ini atau itu di sistem lain. Kode yang diharapkan dapat digunakan kembali sebaiknya ditempatkan di modul dan kelas terpisah.

Viskositas

Ada dua jenis:

  • Viskositas pengembangan
  • Viskositas lingkungan

Viskositas pengembangan dapat kita lihat saat mencoba mengikuti desain aplikasi yang dipilih. Ini mungkin terjadi ketika seorang programmer perlu memenuhi terlalu banyak persyaratan sementara ada cara pengembangan yang lebih mudah. Selain itu, viskositas pengembangan dapat terlihat saat proses perakitan, penerapan, dan pengujian tidak efektif.

Sebagai contoh sederhana, kita dapat mempertimbangkan pekerjaan dengan konstanta yang akan ditempatkan (By Design) ke dalam modul terpisah (Modul 1) untuk digunakan oleh komponen lain (Modul 2 dan Modul 3).

// Module 1 (Constants)
static class Constants
{
  public const decimal MaxSalary = 100M;
  public const int MaxNumberOfProducts = 100;
}
 
// Finance Module
#using Module1
static class FinanceHelper
{
  public static bool ApproveSalary(decimal salary)
  {
    return salary <= Constants.MaxSalary;
  }
} 
 
// Marketing Module
#using Module1
class ProductManager
{
  public void MakeOrder()
  {
    int productsNumber = 0;
    while(productsNumber++ <= Constants.MaxNumberOfProducts)
    {
      // Purchase some product
    }
  }
}

Jika karena suatu hal proses perakitan memakan waktu lama, akan sulit bagi pengembang untuk menunggu hingga selesai. Selain itu, perlu dicatat bahwa modul konstan berisi entitas campuran yang termasuk dalam bagian logika bisnis yang berbeda (modul keuangan dan pemasaran). Jadi, modul konstan dapat sering diubah untuk alasan yang tidak tergantung satu sama lain, yang dapat menyebabkan masalah tambahan seperti sinkronisasi perubahan.

Semua ini memperlambat proses pengembangan dan dapat membuat programmer stres. Varian dari desain yang kurang kental adalah membuat modul konstanta terpisah – satu per satu untuk modul logika bisnis yang sesuai – atau meneruskan konstanta ke tempat yang tepat tanpa mengambil modul terpisah untuknya.

Contoh kekentalan lingkungan dapat berupa pengembangan dan pengujian aplikasi pada mesin virtual klien jarak jauh. Terkadang alur kerja ini menjadi tidak tertahankan karena koneksi internet yang lambat, sehingga pengembang dapat secara sistematis mengabaikan pengujian integrasi kode tertulis, yang pada akhirnya dapat menyebabkan bug di sisi klien saat menggunakan fitur ini.

Kompleksitas yang tidak perlu

Dalam hal ini, desain sebenarnya memiliki fungsionalitas yang tidak digunakan. Fakta ini dapat mempersulit dukungan dan pemeliharaan program, serta meningkatkan waktu pengembangan dan pengujian. Sebagai contoh, pertimbangkan program yang membutuhkan membaca beberapa data dari database. Untuk melakukan ini, komponen DataManager telah dibuat, yang digunakan di komponen lain.

class DataManager
{
  object[] GetData()
  {
    // Retrieve and return data
  }
}

Jika pengembang menambahkan metode baru ke DataManager untuk menulis data ke dalam database (WriteData), yang kemungkinan tidak akan digunakan di masa mendatang, maka itu juga akan menjadi kerumitan yang tidak perlu.

Contoh lain adalah antarmuka untuk semua tujuan. Misalnya, kita akan mempertimbangkan antarmuka dengan metode Proses tunggal yang menerima objek bertipe string.

interface IProcessor
{
  void Process(string message);
}

Jika tugasnya adalah memproses jenis pesan tertentu dengan struktur yang terdefinisi dengan baik, maka akan lebih mudah untuk membuat antarmuka yang diketik secara ketat, daripada membuat pengembang membatalkan serialisasi string ini menjadi jenis pesan tertentu setiap saat.

Menggunakan pola desain secara berlebihan jika hal ini tidak diperlukan sama sekali dapat menyebabkan desain viskositas juga.

Mengapa membuang waktu Anda untuk menulis kode yang berpotensi tidak digunakan? Terkadang, QA menguji kode ini, karena kode ini sebenarnya diterbitkan dan terbuka untuk digunakan oleh klien pihak ketiga. Ini juga menunda waktu rilis. Menyertakan fitur untuk masa depan hanya bernilai jika kemungkinan manfaatnya melebihi biaya untuk pengembangan dan pengujiannya.

Pengulangan yang tidak perlu

Mungkin, sebagian besar pengembang telah menghadapi atau akan menemukan fitur ini, yang terdiri dari banyak menyalin logika atau kode yang sama. Ancaman utama adalah kerentanan kode ini saat memodifikasinya – dengan memperbaiki sesuatu di satu tempat, Anda mungkin lupa melakukannya di tempat lain. Selain itu, dibutuhkan lebih banyak waktu untuk membuat perubahan dibandingkan dengan situasi ketika kode tidak berisi fitur ini.

Pengulangan yang tidak perlu dapat disebabkan oleh kelalaian pengembang, serta karena kekakuan/kerapuhan desain ketika jauh lebih sulit dan lebih berisiko untuk tidak mengulangi kode daripada melakukannya. Namun, bagaimanapun, pengulangan bukanlah ide yang baik, dan perlu untuk terus meningkatkan kode, meneruskan bagian yang dapat digunakan kembali ke metode dan kelas umum.

Keterbacaan buruk

Anda dapat memantau fitur ini ketika sulit untuk membaca kode dan memahami untuk apa kode itu dibuat. Alasan untuk keterbacaan yang buruk dapat berupa ketidakpatuhan terhadap persyaratan untuk eksekusi kode (sintaks, variabel, kelas), logika implementasi yang rumit, dll.

Di bawah ini Anda dapat menemukan contoh kode yang sulit dibaca, yang mengimplementasikan metode dengan variabel Boolean.

void Process_true_false(string trueorfalsevalue)
{
  if (trueorfalsevalue.ToString().Length == 4)
  {
    // That means trueorfalsevalue is probably "true". Do something here.
  }
  else if (trueorfalsevalue.ToString().Length == 5)
  {
    // That means trueorfalsevalue is probably "false". Do something here.
  }
  else
  {
    throw new Exception("not true of false. that's not nice. return.")
  }
}

Di sini, kami dapat menguraikan beberapa masalah. Pertama, nama metode dan variabel tidak sesuai dengan konvensi yang diterima secara umum. Kedua, penerapan metode ini bukan yang terbaik.

Mungkin, ada baiknya mengambil nilai Boolean, daripada string. Namun, lebih baik mengubahnya menjadi nilai Boolean di awal metode, daripada menggunakan metode penentuan panjang string.

Ketiga, teks pengecualian tidak sesuai dengan gaya resmi. Membaca teks semacam itu, mungkin ada perasaan bahwa kode tersebut dibuat oleh seorang amatir (masih, mungkin ada poin yang dipermasalahkan). Metode ini dapat ditulis ulang sebagai berikut jika membutuhkan nilai Boolean:

public void Process(bool value)
{
  if (value)
  {
    // Do something.
  }
  else
  {
    // Do something.
  }
}

Berikut adalah contoh refactoring lain jika Anda masih perlu mengambil string:

public void Process(string value)
{
  bool bValue = false;
  if (!bool.TryParse(value, out bValue))
  {
    throw new ArgumentException($"The {value} is not boolean");
  }  
  if (bValue)
  {
    // Do something.
  }
  else
  {
    // Do something.
  }
}

Direkomendasikan untuk melakukan pemfaktoran ulang dengan kode yang sulit dibaca, misalnya, ketika pemeliharaan dan kloningnya menyebabkan banyak bug.

Kerapuhan

Kerapuhan suatu program berarti dapat dengan mudah crash ketika dimodifikasi. Ada dua jenis crash:kesalahan kompilasi dan kesalahan runtime. Yang pertama bisa menjadi sisi belakang kekakuan. Yang terakhir adalah yang paling berbahaya karena terjadi di sisi klien. Jadi, mereka adalah indikator kerapuhan.

Tidak diragukan lagi, indikatornya relatif. Seseorang memperbaiki kode dengan sangat hati-hati dan kemungkinan kerusakannya cukup rendah, sementara yang lain melakukannya dengan tergesa-gesa dan ceroboh. Namun, kode yang berbeda dengan pengguna yang sama dapat menyebabkan jumlah kesalahan yang berbeda. Mungkin, kita dapat mengatakan bahwa semakin sulit untuk memahami kode dan bergantung pada waktu eksekusi program, daripada pada tahap kompilasi, semakin rapuh kode tersebut.

Selain itu, fungsionalitas yang tidak akan dimodifikasi sering kali macet. Ini mungkin menderita dari kopling logika yang tinggi dari komponen yang berbeda.

Pertimbangkan contoh khusus. Di sini logika otorisasi pengguna dengan peran tertentu (didefinisikan sebagai parameter yang digulung) untuk mengakses sumber daya tertentu (didefinisikan sebagai resourceUri) terletak di metode statis.

static void Main()
{
  if (Helper.Authorize(1, "/pictures"))
  {
    Console.WriteLine("Authorized");
  }
}

class Helper
{
  public static bool Authorize(int roleId, string resourceUri)
  {
    if (roleId == 1 || roleId == 10)
    {
      if (resourceUri == "/pictures")
      {
        return true;
      }
    }

    if (roleId == 1 || roleId == 2 && resourceUri == "/admin")
    {
      return true;
    }

    return false;
  }
}

Seperti yang Anda lihat, logikanya rumit. Jelas bahwa menambahkan peran dan sumber daya baru akan dengan mudah merusaknya. Akibatnya, peran tertentu mungkin mendapatkan atau kehilangan akses ke sumber daya. Membuat kelas Resource yang secara internal menyimpan pengenal resource dan daftar peran yang didukung, seperti yang ditunjukkan di bawah, akan mengurangi kerapuhan.

static void Main()
{
  var picturesResource = new Resource() { Uri = "/pictures" };
  picturesResource.AddRole(1);
  if (picturesResource.IsAvailable(1))
  {
    Console.WriteLine("Authorized");
  }
}

class Resource
{
  private List<int> _roles = new List<int>();
  public string Uri { get; set; }
  public void AddRole(int roleId)
  {
    _roles.Add(roleId);
  }
  public void RemoveRole(int roleId)
  {
    _roles.Remove(roleId);
  }
  public bool IsAvailable(int roleId)
  {
    return _roles.Contains(roleId);
  }
}

Dalam hal ini, untuk menambahkan sumber daya dan peran baru, kode logika otorisasi tidak perlu diubah sama sekali, artinya, sebenarnya tidak ada yang rusak.

Apa yang dapat membantu menangkap kesalahan runtime? Jawabannya adalah manual, otomatis dan pengujian unit. Semakin baik proses pengujian diatur, semakin besar kemungkinan kode rapuh akan muncul di sisi klien.

Seringkali, kerapuhan adalah sisi belakang dari pengidentifikasi lain dari desain yang buruk seperti kekakuan, keterbacaan yang buruk, dan pengulangan yang tidak perlu.

Kesimpulan

Kami telah mencoba untuk menguraikan dan menjelaskan pengidentifikasi utama dari desain yang buruk. Beberapa di antaranya saling bergantung. Anda perlu memahami bahwa masalah desain tidak selalu pasti mengarah pada kesulitan. Ini hanya menunjukkan bahwa mereka mungkin terjadi. Semakin sedikit pengenal ini dipantau, semakin rendah kemungkinannya.


  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Cara Mengelompokkan dengan Dua Kolom di SQL

  2. Labu dengan Contoh – Menyiapkan Postgres, SQLAlchemy, dan Alembic

  3. Menggunakan Jenkins dengan Kubernetes AWS, Bagian 1

  4. Dasar-dasar ekspresi tabel, Bagian 5 – CTE, pertimbangan logis

  5. Perubahan pada Partisi yang Dapat Ditulis Mungkin Gagal Secara Tak Terduga