Pola pabrik di C #: Bagaimana memastikan sebuah instance objek hanya dapat dibuat oleh kelas pabrik?


94

Baru-baru ini saya berpikir untuk mengamankan beberapa kode saya. Saya ingin tahu bagaimana orang bisa memastikan sebuah objek tidak pernah bisa dibuat secara langsung, tetapi hanya melalui beberapa metode kelas pabrik. Katakanlah saya memiliki beberapa kelas "objek bisnis" dan saya ingin memastikan setiap instance dari kelas ini akan memiliki status internal yang valid. Untuk mencapai ini, saya perlu melakukan beberapa pemeriksaan sebelum membuat objek, mungkin di konstruktornya. Ini semua baik-baik saja sampai saya memutuskan saya ingin membuat cek ini menjadi bagian dari logika bisnis. Jadi, bagaimana cara mengatur objek bisnis agar dapat dibuat hanya melalui beberapa metode di kelas logika bisnis saya tetapi tidak pernah secara langsung? Keinginan alami pertama untuk menggunakan kata kunci "teman" lama yang baik dari C ++ akan gagal dengan C #. Jadi kami membutuhkan opsi lain ...

Mari kita coba beberapa contoh:

public MyBusinessObjectClass
{
    public string MyProperty { get; private set; }

    public MyBusinessObjectClass (string myProperty)
    {
        MyProperty = myProperty;
    }
}

public MyBusinessLogicClass
{
    public MyBusinessObjectClass CreateBusinessObject (string myProperty)
    {
        // Perform some check on myProperty

        if (true /* check is okay */)
            return new MyBusinessObjectClass (myProperty);

        return null;
    }
}

Tidak apa-apa sampai Anda ingat bahwa Anda masih bisa membuat instance MyBusinessObjectClass secara langsung, tanpa memeriksa inputnya. Saya ingin mengecualikan kemungkinan teknis itu sama sekali.

Lantas, apa pendapat masyarakat tentang ini?


itu salah eja metode MyBoClass?
pradeeptp

2
Saya bingung, mengapa Anda TIDAK ingin objek bisnis Anda menjadi orang yang bertanggung jawab untuk melindungi invarian mereka sendiri? Inti dari pola pabrik (seperti yang saya pahami) adalah untuk A) menangani pembuatan kelas kompleks dengan banyak dependensi atau B) biarkan pabrik yang memutuskan jenis runtime apa yang akan dikembalikan, hanya memperlihatkan antarmuka / abstrak kelas kepada klien. Menempatkan aturan bisnis Anda di objek bisnis Anda adalah ESENSI dari OOP.
sara

@kai Benar, objek bisnis bertanggung jawab untuk melindungi invariannya, tetapi pemanggil konstruktor memastikan bahwa argumen valid sebelum meneruskannya ke konstruktor ! Tujuan CreateBusinessObjectmetode adalah untuk memvalidasi argumen DAN ke (mengembalikan objek valid baru ATAU mengembalikan kesalahan) dalam panggilan metode tunggal ketika pemanggil tidak segera mengetahui apakah argumen valid untuk diteruskan ke konstruktor.
Lightman

2
Saya tidak setuju. Konstruktor bertanggung jawab untuk melakukan pemeriksaan yang diperlukan untuk parameter yang diberikan. Jika saya memanggil konstruktor dan tidak ada pengecualian yang dilemparkan, saya percaya bahwa objek yang dibuat berada dalam status yang valid. Seharusnya secara fisik tidak mungkin untuk membuat instance kelas menggunakan argumen yang "salah". Membuat Distanceinstance dengan argumen negatif harus membuang ArgumentOutOfRangeExceptionmisalnya, Anda tidak akan mendapatkan apa-apa dari membuat DistanceFactoryyang melakukan pemeriksaan yang sama. Saya masih tidak melihat apa yang ingin Anda peroleh di sini.
sara

Jawaban:


55

Sepertinya Anda hanya ingin menjalankan beberapa logika bisnis sebelum membuat objek - jadi mengapa Anda tidak membuat metode statis di dalam "BusinessClass" yang melakukan semua pemeriksaan "myProperty" kotor, dan membuat konstruktor pribadi?

public BusinessClass
{
    public string MyProperty { get; private set; }

    private BusinessClass()
    {
    }

    private BusinessClass(string myProperty)
    {
        MyProperty = myProperty;
    }

    public static BusinessClass CreateObject(string myProperty)
    {
        // Perform some check on myProperty

        if (/* all ok */)
            return new BusinessClass(myProperty);

        return null;
    }
}

Memanggilnya akan sangat mudah:

BusinessClass objBusiness = BusinessClass.CreateObject(someProperty);

6
Ah, Anda juga bisa melempar pengecualian daripada mengembalikan nol.
Ricardo Nolde

5
tidak ada gunanya memiliki konstruktor default pribadi jika Anda telah mendeklarasikan konstruktor kustom. juga, dalam kasus ini secara harfiah tidak ada yang bisa diperoleh dari menggunakan "konstruktor" statis daripada hanya melakukan validasi dalam konstruktor sebenarnya (karena itu bagian dari kelas). bahkan LEBIH BURUK, karena Anda sekarang bisa mendapatkan nilai pengembalian nol, membuka untuk NRE dan "apa artinya null ini?" pertanyaan.
sara

Adakah cara yang baik untuk meminta arsitektur ini melalui kelas dasar Antarmuka / abstrak? yaitu kelas dasar antarmuka / abstrak yang menentukan bahwa pelaksana tidak boleh mengekspos ctor.
Arash Motamedi

1
@Arash Tidak. Antarmuka tidak dapat mendefinisikan konstruktor, dan kelas abstrak yang mendefinisikan konstruktor internal atau terlindungi tidak dapat mencegah kelas yang diwariskan agar tidak mengeksposnya melalui konstruktor publiknya sendiri.
Ricardo Nolde

66

Anda dapat membuat konstruktor menjadi pribadi, dan pabrik menjadi tipe bersarang:

public class BusinessObject
{
    private BusinessObject(string property)
    {
    }

    public class Factory
    {
        public static BusinessObject CreateBusinessObject(string property)
        {
            return new BusinessObject(property);
        }
    }
}

Ini berfungsi karena tipe bertingkat memiliki akses ke anggota privat dari tipe pelingkupnya. Saya tahu ini agak membatasi, tetapi semoga ini akan membantu ...


Saya memikirkannya tetapi secara efektif memindahkan pemeriksaan ini ke dalam objek bisnis itu sendiri, yang saya coba hindari.
Pengguna

@ so-tester: Anda masih dapat melakukan pengecekan di pabrik daripada di tipe BusinessObject.
Jon Skeet

3
@JonSkeet Saya tahu pertanyaan ini benar-benar tua, tetapi saya ingin tahu apa keuntungannya menempatkan CreateBusinessObjectmetode di dalam Factorykelas bersarang daripada menjadikan metode statis itu metode langsung dari BusinessObjectkelas ... dapatkah Anda mengingat motivasi untuk melakukannya?
Kiley Naro

3
@KileyNaro: Ya, itulah pertanyaan yang diajukan :) Ini tidak selalu memberikan banyak keuntungan, tetapi menjawab pertanyaan ... ada kalanya ini berguna - pola pembangun muncul dalam pikiran. (Dalam hal ini pembangun akan menjadi kelas bersarang, dan itu akan memiliki metode instan yang dipanggil Build.)
Jon Skeet

53

Atau, jika Anda ingin benar-benar mewah, kontrol terbalik: Minta kelas mengembalikan pabrik, dan lengkapi pabrik dengan delegasi yang dapat membuat kelas.

public class BusinessObject
{
  public static BusinessObjectFactory GetFactory()
  {
    return new BusinessObjectFactory (p => new BusinessObject (p));
  }

  private BusinessObject(string property)
  {
  }
}

public class BusinessObjectFactory
{
  private Func<string, BusinessObject> _ctorCaller;

  public BusinessObjectFactory (Func<string, BusinessObject> ctorCaller)
  {
    _ctorCaller = ctorCaller;
  }

  public BusinessObject CreateBusinessObject(string myProperty)
  {
    if (...)
      return _ctorCaller (myProperty);
    else
      return null;
  }
}

:)


Sepotong kode yang sangat bagus. Logika pembuatan objek berada dalam kelas terpisah dan objek hanya dapat dibuat menggunakan pabrik.
Nikolai Samteladze

Keren. Adakah cara agar Anda bisa membatasi pembuatan BusinessObjectFactory ke BusinessObject.GetFactory statis?
Felix Keil

1
Apa keuntungan dari inversi ini?
yatskovsky

1
@yatskovsky Ini hanya cara untuk memastikan bahwa objek hanya dapat dibuat instance-nya dengan cara yang terkontrol sambil tetap dapat memfaktorkan logika pembuatan ke dalam kelas khusus.
Fabian Schmied

15

Anda dapat membuat konstruktor di kelas MyBusinessObjectClass internal, dan memindahkannya serta pabrik ke rakitannya sendiri. Sekarang hanya pabrik yang dapat membuat instance kelas.


7

Terlepas dari apa yang disarankan Jon, Anda juga bisa menjadikan metode pabrik (termasuk cek) menjadi metode statis BusinessObject di tempat pertama. Kemudian, buat konstruktor privat, dan semua orang akan dipaksa untuk menggunakan metode statis.

public class BusinessObject
{
  public static Create (string myProperty)
  {
    if (...)
      return new BusinessObject (myProperty);
    else
      return null;
  }
}

Tetapi pertanyaan sebenarnya adalah - mengapa Anda memiliki persyaratan ini? Apakah boleh memindahkan pabrik atau metode pabrik ke dalam kelas?


Ini bukanlah persyaratan. Saya hanya ingin pemisahan yang bersih antara objek bisnis dan logika. Sama seperti tidak pantas untuk memiliki pemeriksaan ini di belakang kode halaman, saya menganggap tidak pantas untuk memiliki pemeriksaan ini di objek itu sendiri. Yah, mungkin validasi dasar, tetapi sebenarnya bukan aturan bisnis.
Pengguna

Jika Anda ingin memisahkan logika dan struktur kelas hanya untuk meninjau kenyamanan, Anda selalu dapat menggunakan kelas parsial dan memiliki keduanya dalam file terpisah ...
Grx70

7

Setelah bertahun-tahun pertanyaan ini diajukan, dan semua jawaban yang saya lihat sayangnya memberi tahu Anda bagaimana Anda harus melakukan kode Anda alih-alih memberikan jawaban langsung. Jawaban sebenarnya yang Anda cari adalah memiliki kelas Anda dengan konstruktor pribadi tetapi instantiator publik, yang berarti bahwa Anda hanya dapat membuat contoh baru dari contoh lain yang sudah ada ... yang hanya tersedia di pabrik:

Antarmuka untuk kelas Anda:

public interface FactoryObject
{
    FactoryObject Instantiate();
}

Kelas Anda:

public class YourClass : FactoryObject
{
    static YourClass()
    {
        Factory.RegisterType(new YourClass());
    }

    private YourClass() {}

    FactoryObject FactoryObject.Instantiate()
    {
        return new YourClass();
    }
}

Dan, akhirnya, pabrik:

public static class Factory
{
    private static List<FactoryObject> knownObjects = new List<FactoryObject>();

    public static void RegisterType(FactoryObject obj)
    {
        knownObjects.Add(obj);
    }

    public static T Instantiate<T>() where T : FactoryObject
    {
        var knownObject = knownObjects.Where(x => x.GetType() == typeof(T));
        return (T)knownObject.Instantiate();
    }
}

Kemudian Anda dapat dengan mudah mengubah kode ini jika Anda memerlukan parameter tambahan untuk pembuatan instance atau untuk memproses terlebih dahulu instance yang Anda buat. Dan kode ini akan memungkinkan Anda untuk memaksa instantiation melalui pabrik karena konstruktor kelas bersifat pribadi.


4

Namun opsi lain (ringan) adalah membuat metode pabrik statis di kelas BusinessObject dan menjaga konstruktor tetap pribadi.

public class BusinessObject
{
    public static BusinessObject NewBusinessObject(string property)
    {
        return new BusinessObject();
    }

    private BusinessObject()
    {
    }
}

2

Jadi, sepertinya yang saya inginkan tidak bisa dilakukan dengan cara yang "murni". Itu selalu semacam "panggilan kembali" ke kelas logika.

Mungkin saya bisa melakukannya dengan cara yang sederhana, cukup buat metode kontraktor di kelas objek terlebih dahulu memanggil kelas logika untuk memeriksa input?

public MyBusinessObjectClass
{
    public string MyProperty { get; private set; }

    private MyBusinessObjectClass (string myProperty)
    {
        MyProperty  = myProperty;
    }

    pubilc static MyBusinessObjectClass CreateInstance (string myProperty)
    {
        if (MyBusinessLogicClass.ValidateBusinessObject (myProperty)) return new MyBusinessObjectClass (myProperty);

        return null;
    }
}

public MyBusinessLogicClass
{
    public static bool ValidateBusinessObject (string myProperty)
    {
        // Perform some check on myProperty

        return CheckResult;
    }
}

Dengan cara ini, objek bisnis tidak dapat dibuat secara langsung dan metode pemeriksaan publik dalam logika bisnis juga tidak akan merugikan.


2

Dalam kasus pemisahan yang baik antara antarmuka dan implementasi, pola
protected-constructor-public-initializer memungkinkan solusi yang sangat rapi.

Diberikan objek bisnis:

public interface IBusinessObject { }

class BusinessObject : IBusinessObject
{
    public static IBusinessObject New() 
    {
        return new BusinessObject();
    }

    protected BusinessObject() 
    { ... }
}

dan pabrik bisnis:

public interface IBusinessFactory { }

class BusinessFactory : IBusinessFactory
{
    public static IBusinessFactory New() 
    {
        return new BusinessFactory();
    }

    protected BusinessFactory() 
    { ... }
}

perubahan berikut ke BusinessObject.New()penginisialisasi memberikan solusi:

class BusinessObject : IBusinessObject
{
    public static IBusinessObject New(BusinessFactory factory) 
    { ... }

    ...
}

Di sini diperlukan referensi ke pabrik bisnis beton untuk memanggil BusinessObject.New()penginisialisasi. Tetapi satu-satunya yang memiliki referensi yang dibutuhkan adalah pabrik bisnis itu sendiri.

Kami mendapatkan apa yang kami inginkan: satu-satunya yang bisa berkreasi BusinessObjectadalah BusinessFactory.


Jadi, Anda memastikan bahwa satu-satunya konstruktor publik yang berfungsi dari objek tersebut memerlukan Pabrik, dan parameter Pabrik yang diperlukan hanya dapat dibuat dengan memanggil metode statis Pabrik? Sepertinya solusi yang berfungsi, tetapi saya merasa bahwa cuplikan seperti public static IBusinessObject New(BusinessFactory factory) akan menaikkan banyak alis jika orang lain mempertahankan kodenya.
Flater

@Flater Setuju. Saya akan mengubah nama parameter factorymenjadi asFriend. Dalam basis kode saya, ini akan ditampilkan sebagaipublic static IBusinessObject New(BusinessFactory asFriend)
Reuven Bass

Saya tidak mengerti sama sekali. Bagaimana cara kerjanya? Pabrik tidak dapat membuat instance baru IBusinessObject juga tidak berniat (metode) untuk melakukannya ...?
lapsus

2
    public class HandlerFactory: Handler
    {
        public IHandler GetHandler()
        {
            return base.CreateMe();
        }
    }

    public interface IHandler
    {
        void DoWork();
    }

    public class Handler : IHandler
    {
        public void DoWork()
        {
            Console.WriteLine("hander doing work");
        }

        protected IHandler CreateMe()
        {
            return new Handler();
        }

        protected Handler(){}
    }

    public static void Main(string[] args)
    {
        // Handler handler = new Handler();         - this will error out!
        var factory = new HandlerFactory();
        var handler = factory.GetHandler();

        handler.DoWork();           // this works!
    }

Silakan coba pendekatan saya, 'Pabrik' adalah wadah penangan, dan hanya itu yang dapat membuat contoh untuk dirinya sendiri. dengan beberapa modifikasi, mungkin sesuai dengan kebutuhan Anda.
lin

1
Dalam contoh Anda, bukankah hal berikut ini mungkin (yang tidak masuk akal) ?: factory.DoWork ();
Whyser

1

Saya akan meletakkan pabrik di rakitan yang sama dengan kelas domain, dan menandai internal konstruktor kelas domain. Dengan cara ini, semua kelas di domain Anda mungkin dapat membuat instance, tetapi Anda tidak yakin untuk melakukannya, bukan? Siapa pun yang menulis kode di luar lapisan domain harus menggunakan pabrik Anda.

public class Person
{
  internal Person()
  {
  }
}

public class PersonFactory
{
  public Person Create()
  {
    return new Person();
  }  
}

Namun, saya harus mempertanyakan pendekatan Anda :-)

Saya pikir jika Anda ingin kelas Person Anda menjadi valid saat dibuat, Anda harus meletakkan kode di konstruktor.

public class Person
{
  public Person(string firstName, string lastName)
  {
    FirstName = firstName;
    LastName = lastName;
    Validate();
  }
}

1

Solusi ini didasarkan pada ide munificents menggunakan token di konstruktor. Selesai dalam jawaban ini pastikan objek hanya dibuat oleh pabrik (C #)

  public class BusinessObject
    {
        public BusinessObject(object instantiator)
        {
            if (instantiator.GetType() != typeof(Factory))
                throw new ArgumentException("Instantiator class must be Factory");
        }

    }

    public class Factory
    {
        public BusinessObject CreateBusinessObject()
        {
            return new BusinessObject(this);
        }
    }

1

Berbagai pendekatan dengan pengorbanan yang berbeda telah disebutkan.

  • Menumpuk kelas pabrik dalam kelas yang dibangun secara pribadi hanya memungkinkan pabrik untuk membangun 1 kelas. Pada titik itu Anda lebih baik dengan aCreate metode dan ctor pribadi.
  • Menggunakan warisan dan ctor yang dilindungi memiliki masalah yang sama.

Saya ingin mengusulkan pabrik sebagai kelas parsial yang berisi kelas bertingkat privat dengan konstruktor publik. Anda 100% menyembunyikan objek yang sedang dibangun oleh pabrik Anda dan hanya mengekspos apa yang Anda pilih melalui satu atau beberapa antarmuka.

Kasus penggunaan yang saya dengar untuk ini adalah saat Anda ingin melacak 100% instans di pabrik. Desain ini menjamin tidak seorang pun kecuali pabrik yang memiliki akses untuk membuat contoh "bahan kimia" yang ditentukan di "pabrik" dan menghilangkan kebutuhan akan perakitan terpisah untuk mencapainya.

== ChemicalFactory.cs ==
partial class ChemicalFactory {
    private  ChemicalFactory() {}

    public interface IChemical {
        int AtomicNumber { get; }
    }

    public static IChemical CreateOxygen() {
        return new Oxygen();
    }
}


== Oxygen.cs ==
partial class ChemicalFactory {
    private class Oxygen : IChemical {
        public Oxygen() {
            AtomicNumber = 8;
        }
        public int AtomicNumber { get; }
    }
}



== Program.cs ==
class Program {
    static void Main(string[] args) {
        var ox = ChemicalFactory.CreateOxygen();
        Console.WriteLine(ox.AtomicNumber);
    }
}

0

Saya tidak mengerti mengapa Anda ingin memisahkan "logika bisnis" dari "objek bisnis". Ini terdengar seperti distorsi orientasi objek, dan Anda akhirnya akan mengikat diri sendiri dengan mengambil pendekatan itu.


Saya juga tidak mengerti perbedaan ini. Adakah yang bisa menjelaskan mengapa ini dilakukan, dan kode apa yang ada di objek bisnis?
pipTheGeek

Itu hanya satu pendekatan yang bisa digunakan. Saya ingin objek bisnis menjadi struktur data utama tetapi tidak dengan semua bidang terbuka untuk baca / tulis. Tetapi pertanyaannya sebenarnya adalah tentang dapat membuat instance objek hanya dengan bantuan metode pabrik dan tidak secara langsung.
Pengguna

@Jim: Saya pribadi setuju dengan maksud Anda, tetapi secara profesional, ini dilakukan terus-menerus. Proyek saya saat ini adalah layanan web yang mengambil string HTML, membersihkannya sesuai dengan beberapa aturan yang telah ditetapkan, lalu mengembalikan string yang telah dibersihkan. Saya diharuskan untuk melewati 7 lapis kode saya sendiri "karena semua proyek kita harus dibangun dari template proyek yang sama". Masuk akal bahwa kebanyakan orang yang menanyakan pertanyaan ini di SO tidak mendapatkan kebebasan untuk mengubah seluruh aliran kode mereka.
Flater

0

Saya tidak berpikir ada solusi yang tidak lebih buruk dari masalahnya, semua yang dia di atas memerlukan pabrik statis publik yang IMHO merupakan masalah yang lebih buruk dan tidak akan menghentikan orang yang hanya menelepon pabrik untuk menggunakan objek Anda - itu tidak menyembunyikan apa pun. Paling baik untuk mengekspos antarmuka dan / atau menjaga konstruktor sebagai internal jika Anda bisa, itu adalah perlindungan terbaik karena rakitan adalah kode tepercaya.

Salah satu opsinya adalah memiliki konstruktor statis yang mendaftarkan pabrik di suatu tempat dengan sesuatu seperti kontainer IOC.


0

Berikut adalah solusi lain dalam nada "hanya karena Anda bisa tidak berarti Anda harus" ...

Itu memenuhi persyaratan untuk menjaga kerahasiaan konstruktor objek bisnis dan meletakkan logika pabrik di kelas lain. Setelah itu menjadi sedikit samar.

Kelas pabrik memiliki metode statis untuk membuat objek bisnis. Ini berasal dari kelas objek bisnis untuk mengakses metode konstruksi terlindung statis yang memanggil konstruktor pribadi.

Pabrik itu abstrak sehingga Anda tidak dapat benar-benar membuat instance darinya (karena itu juga akan menjadi objek bisnis, jadi itu akan aneh), dan memiliki konstruktor pribadi sehingga kode klien tidak dapat diturunkan darinya.

Apa yang tidak dicegah adalah kode klien juga berasal dari kelas objek bisnis dan memanggil metode konstruksi statis yang dilindungi (tetapi tidak divalidasi). Atau lebih buruk lagi, memanggil konstruktor default terlindungi yang harus kami tambahkan agar kelas pabrik dapat dikompilasi sejak awal. (Yang kebetulan mungkin menjadi masalah dengan pola apa pun yang memisahkan kelas pabrik dari kelas objek bisnis.)

Saya tidak mencoba menyarankan siapa pun yang waras harus melakukan sesuatu seperti ini, tetapi ini adalah latihan yang menarik. FWIW, solusi pilihan saya adalah menggunakan konstruktor internal dan batas perakitan sebagai penjaga.

using System;

public class MyBusinessObjectClass
{
    public string MyProperty { get; private set; }

    private MyBusinessObjectClass(string myProperty)
    {
        MyProperty = myProperty;
    }

    // Need accesible default constructor, or else MyBusinessObjectFactory declaration will generate:
    // error CS0122: 'MyBusinessObjectClass.MyBusinessObjectClass(string)' is inaccessible due to its protection level
    protected MyBusinessObjectClass()
    {
    }

    protected static MyBusinessObjectClass Construct(string myProperty)
    {
        return new MyBusinessObjectClass(myProperty);
    }
}

public abstract class MyBusinessObjectFactory : MyBusinessObjectClass
{
    public static MyBusinessObjectClass CreateBusinessObject(string myProperty)
    {
        // Perform some check on myProperty

        if (true /* check is okay */)
            return Construct(myProperty);

        return null;
    }

    private MyBusinessObjectFactory()
    {
    }
}

0

Akan sangat menghargai mendengar beberapa pemikiran tentang solusi ini. Satu-satunya yang dapat membuat 'MyClassPrivilegeKey' adalah pabrik. dan 'MyClass' membutuhkannya di konstruktor. Sehingga menghindari refleksi kontraktor swasta / "registrasi" ke pabrik.

public static class Runnable
{
    public static void Run()
    {
        MyClass myClass = MyClassPrivilegeKey.MyClassFactory.GetInstance();
    }
}

public abstract class MyClass
{
    public MyClass(MyClassPrivilegeKey key) { }
}

public class MyClassA : MyClass
{
    public MyClassA(MyClassPrivilegeKey key) : base(key) { }
}

public class MyClassB : MyClass
{
    public MyClassB(MyClassPrivilegeKey key) : base(key) { }
}


public class MyClassPrivilegeKey
{
    private MyClassPrivilegeKey()
    {
    }

    public static class MyClassFactory
    {
        private static MyClassPrivilegeKey key = new MyClassPrivilegeKey();

        public static MyClass GetInstance()
        {
            if (/* some things == */true)
            {
                return new MyClassA(key);
            }
            else
            {
                return new MyClassB(key);
            }
        }
    }
}
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.