Pertanyaan Bagaimana cara menggunakan Injeksi Ketergantungan tanpa melanggar enkapsulasi?


Bagaimana saya bisa melakukan injeksi ketergantungan tanpa melanggar enkapsulasi?

Menggunakan sebuah Contoh Injeksi Ketergantungan dari Wikipedia:

public Car {
    public float getSpeed();
}

catatan: Metode dan properti lainnya (mis. PushBrake (), PushGas (),   SetWheelPosition ()) dihapus   kejelasan

Ini bekerja dengan baik; Anda tidak tahu bagaimana mengimplementasikan objek saya getSpeed - ini "dienkapsulasi".

Pada kenyataannya, objek saya mengimplementasikan getSpeed sebagai:

public Car {
    private m_speed;
    public float getSpeed( return m_speed; );
}

Dan semuanya baik-baik saja. Seseorang membangun saya Car objek, mashes pedal, klakson, kemudi, dan mobil merespon.

Sekarang katakanlah saya mengubah detail implementasi internal mobil saya:

public Car {
    private Engine m_engine;
    private float m_currentGearRatio;
    public float getSpeed( return m_engine.getRpm*m_currentGearRatio; );
}

Semua baik-baik saja. Itu Car mengikuti prinsip-prinsip OO yang tepat, menyembunyikan detail bagaimana sesuatu sudah selesai. Ini membebaskan penelepon untuk memecahkan masalahnya, daripada mencoba memahami cara kerja mobil. Ini juga memberi saya kebebasan untuk mengubah implementasi saya sesuai keinginan saya.

Tetapi suntikan ketergantungan akan memaksa saya untuk mengekspos kelas saya menjadi Engine objek yang saya tidak buat atau menginisialisasi. Lebih buruk lagi adalah bahwa saya sekarang telah mengekspos bahwa saya Car bahkan punya sebuah mesin:

public Car {
   public constructor(Engine engine);
   public float getSpeed();
}

Dan sekarang kata luarnya sadar bahwa saya menggunakan sebuah Engine. Saya tidak selalu menggunakan mesin, saya mungkin tidak ingin menggunakan Engine di masa depan, tetapi saya tidak lagi dapat mengubah implementasi internal saya:

public Car {
    private Gps m_gps;
    public float getSpeed( return m_gps.CurrentVelocity.Speed; )
}

tanpa melanggar pemanggil:

public Car {
   public constructor(Gps gps);
   public float getSpeed();
}

Tetapi suntikan ketergantungan membuka seluruh kaleng cacing: dengan membuka seluruh kaleng cacing. Ketergantungan Injeksi mensyaratkan semua benda saya pribadi rincian implementasi akan diekspos. Konsumen saya Car kelas sekarang harus memahami, dan menangani, semua kerumitan internal yang tersembunyi sebelumnya di kelas saya:

public Car {
   public constructor(
       Gps gps, 
       Engine engine, 
       Transmission transmission,
       Tire frontLeftTire, Tire frontRightTire, Tire rearLeftTire, Tire rearRightTire, 
       Seat driversSeat, Seat passengersSeat, Seat rearBenchSeat,
       SeatbeltPretensioner seatBeltPretensioner,
       Alternator alternator, 
       Distributor distributor,
       Chime chime,
       ECM computer,
       TireMonitoringSystem tireMonitor
       );
   public float getSpeed();
}

Bagaimana saya bisa menggunakan kebaikan Ketergantungan Injeksi untuk membantu pengujian unit, sementara tidak melanggar kebaikan enkapsulasi untuk membantu kegunaan?

Lihat juga


Demi kesenangan, saya bisa memangkasnya getSpeed contoh untuk apa yang dibutuhkan:

public Car {
   public constructor(
       Engine engine, 
       Transmission transmission,
       Tire frontLeftTire, Tire frontRightTire
       TireMonitoringSystem tireMonitor,
       UnitConverter unitsConverter
       );
   public float getSpeed()
   {
      float tireRpm = m_engine.CurrentRpm * 
              m_transmission.GetGearRatio( m_transmission.CurrentGear);

      float effectiveTireRadius = 
         (
            (m_frontLeftTire.RimSize + m_frontLeftTire.TireHeight / 25.4)
            +
            (m_frontRightTire.RimSize + m_frontRightTire.TireHeight / 25.4)
         ) / 2.0;

      //account for over/under inflated tires
      effectiveTireRadius = effectiveTireRadius * 
            ((m_tireMonitor.FrontLeftInflation + m_tireMontitor.FrontRightInflation) / 2.0);

      //speed in inches/minute
      float speed = tireRpm * effetiveTireRadius * 2 * Math.pi;

      //convert to mph
      return m_UnitConverter.InchesPerMinuteToMilesPerHour(speed);
   }
}

Memperbarui: Mungkin beberapa jawaban dapat mengikuti arahan pertanyaan, dan memberikan kode contoh?

public Car {
    public float getSpeed();
}

Contoh lain adalah ketika kelas saya bergantung pada objek lain:

public Car {
    private float m_speed;
}

Pada kasus ini float adalah kelas yang digunakan untuk merepresentasikan nilai floating-point. Dari apa yang saya baca, setiap kelas dependen harus disuntikkan - jika saya ingin mengolok-olok float kelas. Hal ini menimbulkan momok karena harus menyuntikkan setiap anggota pribadi, karena semuanya pada dasarnya adalah objek:

public Car {
    public Constructor(
        float speed,
        float weight,
        float wheelBase,
        float width,
        float length,
        float height,
        float headRoom,
        float legRoom,
        DateTime manufactureDate,
        DateTime designDate,
        DateTime carStarted,
        DateTime runningTime,
        Gps gps, 
        Engine engine, 
        Transmission transmission,
        Tire frontLeftTire, Tire frontRightTire, Tire rearLeftTire, Tire rearRightTire, 
        Seat driversSeat, Seat passengersSeat, Seat rearBenchSeat,
        SeatbeltPretensioner seatBeltPretensioner,
        Alternator alternator, 
        Distributor distributor,
        Chime chime,
        ECM computer,
        TireMonitoringSystem tireMonitor,
        ...
     }

Ini benar-benar rincian implementasi yang saya tidak ingin pelanggan harus melihat.


32
2017-09-29 15:24


asal


Jawaban:


Banyak jawaban lain yang mengisyaratkan hal itu, tetapi saya akan mengatakan dengan lebih eksplisit bahwa ya, penerapan injeksi ketergantungan yang naif bisa memecahkan enkapsulasi.

Kunci untuk menghindari ini adalah bahwa kode panggilan seharusnya tidak secara langsung memberi contoh dependensi (jika tidak peduli dengan mereka).Ini bisa dilakukan dengan berbagai cara.

Yang paling sederhana adalah hanya memiliki konstruktor default yang melakukan penyuntikan dengan nilai default. Selama kode panggilan hanya menggunakan konstruktor default, Anda dapat mengubah dependensi di belakang layar tanpa mempengaruhi kode panggilan.

Ini bisa mulai keluar dari tangan jika dependensi Anda sendiri memiliki ketergantungan dan sebagainya. Pada saat itu, pola Pabrik dapat terjadi (atau Anda dapat menggunakannya dari awal sehingga kode panggilan sudah menggunakan pabrik). Jika Anda memperkenalkan pabrik dan tidak ingin merusak pengguna kode Anda yang sudah ada, Anda dapat selalu memanggil pabrik dari konstruktor default Anda.

Di luar itu ada yang menggunakan Inversion of Control. Saya belum pernah menggunakan IoC untuk berbicara terlalu banyak tentang hal itu, tetapi ada banyak pertanyaan di sini serta artikel online yang menjelaskannya jauh lebih baik daripada yang saya bisa.

Jika harus benar-benar dienkapsulasi ke tempat kode panggilan tidak bisa tahu tentang dependensi maka ada pilihan untuk membuat suntikan (baik konstruktor dengan parameter dependensi atau setter) internal jika bahasa mendukungnya, atau membuatnya menjadi pribadi dan tes unit Anda menggunakan sesuatu seperti Refleksi jika bahasa Anda mendukungnya. Jika bahasa Anda tidak mendukung maka saya kira kemungkinan mungkin memiliki kelas yang memanggil kode instantiate kelas dummy yang hanya merangkum kelas yang melakukan pekerjaan nyata (saya percaya ini adalah pola Facade, tapi saya tidak pernah ingat nama-nama dengan benar ):

public Car {
   private RealCar _car;
   public constructor(){ _car = new RealCar(new Engine) };
   public float getSpeed() { return _car.getSpeed(); }
}

13
2017-09-29 19:37



Jika saya memahami kekhawatiran Anda dengan benar, Anda mencoba mencegah kelas apa pun yang perlu membuat instantiate objek Mobil baru karena harus menyuntikkan semua dependensi tersebut secara manual.

Saya telah menggunakan beberapa pola untuk melakukan ini. Dalam bahasa dengan constructor chaining, saya telah menetapkan konstruktor default yang menyuntikkan jenis beton ke konstruktor lain, dependensi-injected. Saya pikir ini adalah teknik DI manual yang cukup standar.

Pendekatan lain yang saya gunakan, yang memungkinkan beberapa kopling lebih longgar, adalah membuat objek pabrik yang akan mengonfigurasi objek DI'ed dengan dependensi yang sesuai. Lalu saya menyuntikkan pabrik ini ke objek apa pun yang perlu "baru" naik beberapa Mobil saat runtime; ini memungkinkan Anda untuk menyuntikkan implementasi Mobil yang benar-benar palsu selama pengujian Anda juga.

Dan selalu ada pendekatan penyetel-injeksi. Objek akan memiliki default yang wajar untuk propertinya, yang bisa diganti dengan uji-ganda yang diperlukan. Saya lebih memilih konstruktor-injeksi.


Edit untuk menampilkan contoh kode:

interface ICar { float getSpeed(); }
interface ICarFactory { ICar CreateCar(); }

class Car : ICar { 
  private Engine _engine;
  private float _currentGearRatio;

  public constructor(Engine engine, float gearRatio){
    _engine = engine;
    _currentGearRatio = gearRatio;
  }
  public float getSpeed() { return return _engine.getRpm*_currentGearRatio; }
}

class CarFactory : ICarFactory {
  public ICar CreateCar() { ...inject real dependencies... }    
}

Dan kemudian kelas konsumen hanya berinteraksi dengannya melalui antarmuka, benar-benar menyembunyikan konstruktor apa pun.

class CarUser {
  private ICarFactory _factory;

  public constructor(ICarFactory factory) { ... }

  void do_something_with_speed(){
   ICar car = _factory.CreateCar();

   float speed = car.getSpeed();

   //...do something else...
  }
}

7
2017-09-29 16:22



Saya pikir Anda melanggar enkapsulasi dengan Anda Car konstruktor. Secara khusus Anda mendikte itu Engine harus disuntikkan ke Car alih-alih beberapa jenis antarmuka yang digunakan untuk menentukan kecepatan Anda (IVelocity dalam contoh di bawah ini.)

Dengan sebuah antarmuka, Car mampu mendapatkan kecepatannya saat ini independen dari apa yang menentukan kecepatan itu. Sebagai contoh:

public Interface IVelocity {
   public float getSpeed();
}

public class Car {
   private m_velocityObject;
   public constructor(IVelocity velocityObject) { 
       m_velocityObject = velocityObject; 
   }
   public float getSpeed() { return m_velocityObject.getSpeed(); }
}

public class Engine : IVelocity {
   private float m_rpm;
   private float m_currentGearRatio;
   public float getSpeed( return m_rpm * m_currentGearRatio; );
}

public class GPS : IVelocity {
    private float m_foo;
    private float m_bar;
    public float getSpeed( return m_foo * m_bar; ); 
}

Sebuah Mesin atau GPS kemudian dapat memiliki beberapa antarmuka berdasarkan jenis pekerjaan yang dilakukannya. Antarmuka adalah kunci untuk DI, tanpa itu DI tidak memecahkan enkapsulasi.


3
2017-09-29 20:19



Di sinilah saya pikir Anda harus menggunakan kontainer injeksi ketergantungan yang memungkinkan Anda merangkum kreasi mobil Anda, tanpa membiarkan penelepon klien Anda perlu tahu cara membuatnya sama sekali. Beginilah cara symfony memecahkan masalah ini (meskipun bukan bahasa yang sama, prinsipnya tetap sama):

http://components.symfony-project.org/dependency-injection/documentation

ada bagian pada wadah injeksi ketergantungan.

Untuk membuatnya singkat dan merangkum semuanya dikutip dari halaman dokumentasi secara langsung:

Saat menggunakan wadah, kami hanya bertanya   untuk objek mailer [Ini akan menjadi mobil Anda dalam contoh Anda], dan kami tidak membutuhkannya   untuk mengetahui apa pun tentang cara membuat   itu lagi; semua pengetahuan tentang   cara membuat instance dari   mailer [car] sekarang tertanam ke dalam   wadah.

Ini harapan bahwa itu membantu Anda


2
2017-09-29 16:13



Pabrik dan antarmuka.

Anda punya beberapa pertanyaan di sini.

  1. Bagaimana saya bisa memiliki beberapa implementasi dari operasi yang sama?
  2. Bagaimana saya bisa menyembunyikan rincian konstruksi suatu objek dari konsumen suatu objek?

Jadi, yang Anda butuhkan adalah menyembunyikan kode sebenarnya di balik sebuah ICar antarmuka, buat terpisah EnginelessCar jika Anda membutuhkannya, dan gunakan ICarFactory antarmuka dan CarFactory kelas untuk menyembunyikan rincian konstruksi dari konsumen mobil.

Ini mungkin akan terlihat sangat mirip dengan kerangka kerja injeksi ketergantungan, tetapi Anda tidak harus menggunakannya.

Sesuai jawaban saya dalam pertanyaan lain, apakah ini melanggar enkapsulasi sepenuhnya tergantung pada bagaimana Anda mendefinisikan enkapsulasi. Ada dua definisi umum enkapsulasi yang pernah saya lihat:

  1. Semua operasi pada entitas logis diekspos sebagai anggota kelas, dan konsumen kelas tidak perlu menggunakan yang lain.
  2. Kelas memiliki satu tanggung jawab, dan kode untuk mengelola tanggung jawab itu ada di dalam kelas. Yaitu, ketika mengkodekan kelas, Anda dapat secara efektif mengisolasinya dari lingkungannya dan mengurangi ruang lingkup kode yang Anda kerjakan.

(Kode seperti definisi pertama bisa ada dalam basis kode yang bekerja dengan kondisi kedua - itu hanya cenderung terbatas pada fasad, dan fasad tersebut cenderung memiliki logika minimal atau tidak ada).


2
2017-09-29 16:36



Saya belum pernah menggunakan Delphi dalam waktu yang lama. Cara DI bekerja di Spring, pembuat dan konstruktor Anda bukan bagian dari antarmuka. Jadi Anda dapat memiliki beberapa implementasi antarmuka, yang mungkin menggunakan injeksi berbasis konstruktor dan yang lain mungkin menggunakan injeksi setter berbasis, kode Anda yang menggunakan antarmuka tidak peduli. Apa yang disuntikkan ada di xml konteks-aplikasi, dan itu adalah satu-satunya tempat di mana ketergantungan Anda terpapar.

EDIT: Jika Anda menggunakan kerangka atau tidak melakukan hal yang sama, Anda memiliki pabrik yang menyambungkan objek Anda. Jadi objek Anda memaparkan rincian ini di konstruktor atau di setter, tetapi kode aplikasi Anda (di luar pabrik, dan tidak termasuk tes) tidak pernah menggunakannya. Entah bagaimana Anda memilih untuk mendapatkan grafik objek dari pabrik daripada instantiate barang dengan cepat, dan Anda memilih untuk tidak melakukan hal-hal seperti menggunakan setter dalam kode yang ada disuntikkan ke dalamnya. Ini adalah pergeseran pikiran dari filosofi "kuku-segalanya-bawah" yang saya lihat dari beberapa kode orang.


1
2017-09-29 15:59



Saya tidak berpikir mobil adalah contoh yang sangat baik dari kegunaan suntikan ketergantungan dunia nyata.

Saya pikir dalam kasus contoh kode terakhir Anda, tujuan kelas Mobil tidak jelas. Apakah ini kelas yang menyimpan data / negara? Apakah ini layanan untuk menghitung hal-hal seperti kecepatan? Atau apakah itu campuran, memungkinkan Anda untuk membangun negara dan kemudian memanggil layanan di atasnya untuk membuat perhitungan berdasarkan keadaan itu?

Cara saya melihatnya, kelas Mobil itu sendiri kemungkinan akan menjadi objek stateful, yang tujuannya adalah untuk memegang rincian komposisinya, dan layanan untuk menghitung kecepatan (yang dapat disuntikkan, jika diinginkan) akan menjadi kelas yang terpisah (dengan metode seperti "getSpeed(ICar car)"). Banyak pengembang yang menggunakan DI cenderung memisahkan objek stateful dan layanan - meskipun ada kasus di mana suatu objek akan memiliki negara dan layanan, mayoritas cenderung dipisahkan. Selain itu, sebagian besar penggunaan DI cenderung di sisi layanan.

Pertanyaan selanjutnya adalah: bagaimana seharusnya kelas mobil disusun? Apakah maksud bahwa setiap mobil khusus hanyalah sebuah instance dari kelas Mobil, atau apakah ada kelas terpisah untuk setiap merek dan model yang mewarisi dari CarBase atau ICar? Jika yang pertama, maka pasti ada beberapa berarti mengatur / menyuntikkan nilai-nilai ini ke dalam mobil - tidak ada jalan lain di sekitar ini, bahkan jika Anda belum pernah mendengar inversi ketergantungan. Jika itu yang terakhir, maka nilai-nilai ini hanyalah bagian dari mobil, dan saya tidak akan melihat alasan untuk ingin membuatnya dapat diatur / disuntikkan. Itu datang ke apakah hal-hal seperti Engine dan Ban spesifik untuk implementasi (dependensi keras) atau apakah mereka composable (dependensi longgar digabungkan).

Saya memahami mobil hanyalah sebuah contoh, tetapi di dunia nyata Anda akan menjadi orang yang tahu apakah membalik ketergantungan pada kelas Anda melanggar enkapsulasi. Jika ya, pertanyaan yang seharusnya Anda tanyakan adalah "mengapa?" dan bukan "bagaimana?" (tentu saja apa yang Anda lakukan).


1
2017-09-30 01:22



Anda harus memecah kode Anda menjadi dua fase:

  1. Konstruksi grafik objek untuk masa hidup tertentu melalui pabrik atau solusi DI
  2. Menjalankan objek-objek ini (yang akan melibatkan input dan output)

Di pabrik mobil, mereka perlu tahu cara membuat mobil. Mereka tahu seperti apa mesin itu, bagaimana tanduk itu terhubung, dll. Ini adalah fase 1 di atas. Pabrik mobil dapat membangun mobil yang berbeda.

Saat Anda mengendarai mobil, Anda dapat mengendarai apa pun yang memenuhi antarmuka mobil yang Anda harapkan. misalnya pedal, roda kemudi, tanduk. Saat Anda mengemudi, Anda tidak tahu detail internal saat Anda menekan rem. Anda dapat, bagaimanapun, melihat hasilnya (perubahan kecepatan).

Enkapsulasi dipertahankan karena tidak ada yang mengendarai mobil perlu tahu bagaimana itu dibangun. Karena itu, Anda bisa menggunakan driver yang sama dengan banyak mobil yang berbeda. Ketika drive membutuhkan mobil, mereka harus diberi satu. Jika mereka membangun sendiri ketika mereka menyadari bahwa mereka membutuhkannya, maka enkapsulasi akan rusak.


1
2017-10-04 22:52



Sekarang, untuk sesuatu yang sama sekali berbeda ...

Anda ingin kebaikan injeksi ketergantungan tanpa melanggar enkapsulasi. Kerangka kerja injeksi ketergantungan akan melakukan itu untuk Anda, tetapi ada juga "suntikan ketergantungan orang miskin" yang tersedia bagi Anda melalui penggunaan kreatif konstruktor virtual, pendaftaran kelas meta dan unit inklusi selektif dalam proyek Anda.

Itu memang memiliki keterbatasan yang serius: Anda hanya dapat memiliki satu kelas Engine spesifik di setiap proyek. Tidak ada yang memilih mesin yang dipilih, meskipun kalau dipikir-pikir itu, Anda mungkin bisa mengacaukan nilai variabel kelas meta untuk mencapai hal itu. Tapi saya terlalu terburu-buru.

Keterbatasan lain adalah satu baris warisan: hanya batang, tidak ada cabang. Setidaknya berkaitan dengan unit-unit yang termasuk dalam satu proyek.

Anda tampaknya menggunakan Delphi dan karena itu metode di bawah ini akan berfungsi karena ini adalah sesuatu yang telah kami gunakan sejak D5 dalam proyek yang membutuhkan satu instance dari kelas TBaseX, tetapi proyek yang berbeda membutuhkan keturunan yang berbeda dari kelas dasar tersebut dan kami ingin menjadi mampu menukar kelas hanya dengan membuang satu unit dan menambahkan yang lain. Solusinya tidak terbatas pada Delphi sekalipun. Ini akan bekerja dengan bahasa apa pun yang mendukung konstruktor virtual dan kelas meta.

Jadi apa yang Anda butuhkan?

Nah, setiap kelas yang Anda ingin dapat tukar tergantung pada unit yang disertakan per proyek, perlu memiliki variabel di suatu tempat di mana Anda dapat menyimpan jenis kelas untuk instantiate:

var
  _EngineClass: TClass;

Setiap kelas yang mengimplementasikan suatu Mesin harus mendaftarkan dirinya dalam variabel _EngineClass menggunakan metode yang mencegah nenek moyang mengambil tempat keturunan (sehingga Anda dapat menghindari ketergantungan pada urutan inisialisasi unit):

procedure RegisterMetaClass(var aMetaClassVar: TClass; const aMetaClassToRegister: TClass);
begin
  if Assigned(aMetaClassVar) and aMetaClassVar.InheritsFrom(aMetaClassToRegister) then
    Exit;

  aMetaClassVar := aMetaClassToRegister;
end;

Pendaftaran kelas dapat dilakukan dalam kelas dasar umum:

  TBaseEngine
  protected
    class procedure RegisterClass; 

class procedure TBaseEngine.RegisterClass;
begin
  RegisterMetaClass(_EngineClass, Self);
end;

Setiap keturunan mendaftarkan dirinya dengan memanggil metode pendaftaran di bagian inisialisasi unitnya:

type
  TConcreteEngine = class(TBaseEngine)
  ...
  end;

initialization
  TConcreteEngine.RegisterClass;

Sekarang semua yang Anda butuhkan adalah sesuatu untuk instantiate "keturunan paling" kelas terdaftar bukan kelas khusus berkode keras.

  TBaseEngine
  public
    class function CreateRegisteredClass: TBaseEngine; 

class function TBaseEngine.CreateRegisteredClass: TBaseEngine;
begin
  Result := _EngineClass.Create;
end;

Tentu saja Anda sekarang harus selalu menggunakan fungsi kelas ini untuk instantiate engine dan bukan konstruktor normal.

Jika Anda melakukan itu, kode Anda sekarang akan selalu instantiate kelas mesin "paling mematikan" hadir dalam proyek Anda. Dan Anda dapat beralih antar kelas dengan memasukkan dan tidak termasuk unit tertentu. Misalnya Anda dapat memastikan proyek uji Anda menggunakan kelas tiruan dengan menjadikan kelas tiruan leluhur dari kelas yang sebenarnya dan tidak termasuk kelas yang sebenarnya dalam proyek pengujian; atau dengan membuat kelas tiruan keturunan kelas yang sebenarnya dan tidak termasuk tiruan dalam kode normal Anda; atau - lebih sederhana - dengan memasukkan antara mock atau kelas yang sebenarnya di proyek Anda.


Mock dan kelas yang sebenarnya memiliki konstruktor parameter-kurang dalam contoh implementasi ini. Tidak perlu menjadi kasus, tetapi Anda perlu menggunakan kelas meta tertentu (bukan TClass) dan beberapa casting dalam panggilan ke prosedur RegisterMetaClass karena parameter var.

type
  TBaseEngine = class; // forward
  TEngineClass = class of TBaseEngine;
var
  _EngineClass: TEngineClass

type
  TBaseEngine = class
  protected
    class procedure RegisterClass;
  public
    class function CreateRegisteredClass(...): TBaseEngine;
    constructor Create(...); virtual;

  TConcreteEngine = class(TBaseEngine)
    ...
  end;

  TMockEngine = class(TBaseEngine)
    ...
  end;

class procedure TBaseEngine.RegisterClass;
begin
  RegisterMetaClass({var}TClass(_EngineClass), Self);
end;

class function TBaseEngine.CreateRegisteredClass(...): TBaseEngine;
begin
  Result := _EngineClass.Create(...);
end;

constructor TBaseEngine.Create(...);
begin
  // use parameters in creating an instance.
end;

Selamat bersenang-senang!


0
2017-09-30 14:18