Ada satu hal dalam C ++ yang telah membuat saya merasa tidak nyaman untuk waktu yang cukup lama, karena saya sejujurnya tidak tahu bagaimana melakukannya, meskipun kedengarannya sederhana:
Bagaimana cara menerapkan Metode Pabrik di C ++ dengan benar?
Sasaran: untuk memungkinkan klien mengizinkan instantiate beberapa objek menggunakan metode pabrik alih-alih konstruktor objek, tanpa konsekuensi yang tidak dapat diterima dan hit kinerja.
Dengan "Pola metode pabrik", maksud saya adalah metode pabrik statis di dalam objek atau metode yang didefinisikan dalam kelas lain, atau fungsi global. Secara umum "konsep pengalihan cara normal instance dari kelas X ke tempat lain selain konstruktor".
Biarkan saya membaca beberapa jawaban yang mungkin saya pikirkan.
0) Jangan membuat pabrik, buat konstruktor.
Ini kedengarannya bagus (dan memang sering merupakan solusi terbaik), tetapi bukan obat umum. Pertama-tama, ada kasus-kasus ketika konstruksi objek adalah tugas yang cukup kompleks untuk membenarkan ekstraksi ke kelas lain. Tetapi bahkan mengesampingkan fakta itu, bahkan untuk benda-benda sederhana menggunakan konstruktor saja seringkali tidak akan berhasil.
Contoh paling sederhana yang saya tahu adalah kelas 2-D Vector. Sangat sederhana, namun rumit. Saya ingin dapat membuatnya baik dari koordinat Cartesian dan kutub. Jelas, saya tidak bisa melakukan:
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
Cara berpikir alami saya adalah:
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
Yang, alih-alih konstruktor, mengarahkan saya ke penggunaan metode pabrik statis ... yang pada dasarnya berarti bahwa saya menerapkan pola pabrik, dalam beberapa cara ("kelas menjadi pabrik sendiri"). Ini terlihat bagus (dan akan cocok dengan kasus khusus ini), tetapi gagal dalam beberapa kasus, yang akan saya uraikan pada poin 2. Baca terus.
kasus lain: mencoba untuk membebani oleh dua typedefs buram dari beberapa API (seperti GUIDs dari domain yang tidak terkait, atau GUID dan bitfield), jenis yang secara semantis berbeda (pada teori - overload yang valid) tetapi sebenarnya berubah menjadi hal yang sama - seperti int unsigned atau batal pointer.
1) Jalan Jawa
Java memilikinya sederhana, karena kita hanya punya objek dinamis yang dialokasikan. Membuat pabrik sepele seperti:
class FooFactory {
public Foo createFooInSomeWay() {
// can be a static method as well,
// if we don't need the factory to provide its own object semantics
// and just serve as a group of methods
return new Foo(some, args);
}
}
Dalam C ++, ini diterjemahkan menjadi:
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
Keren? Memang sering. Tapi lalu- ini memaksa pengguna untuk hanya menggunakan alokasi dinamis. Alokasi statis adalah apa yang membuat C ++ kompleks, tetapi juga yang sering membuatnya kuat. Juga, saya percaya bahwa ada beberapa target (kata kunci: tertanam) yang tidak memungkinkan untuk alokasi dinamis. Dan itu tidak menyiratkan bahwa pengguna platform tersebut suka menulis OOP bersih.
Pokoknya, filosofi samping: Dalam kasus umum, saya tidak ingin memaksa para pengguna pabrik untuk dibatasi ke alokasi dinamis.
2) Pengembalian berdasarkan nilai
OK, jadi kita tahu bahwa 1) keren ketika kita menginginkan alokasi dinamis. Mengapa kita tidak menambahkan alokasi statis di atasnya?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
Apa? Kami tidak dapat membebani berdasarkan jenis pengembalian? Oh, tentu saja kita tidak bisa. Jadi mari kita ubah nama metode untuk mencerminkan itu. Dan ya, saya telah menulis contoh kode tidak valid di atas hanya untuk menekankan betapa saya tidak suka perlunya mengubah nama metode, misalnya karena kita tidak dapat mengimplementasikan desain pabrik agnostik bahasa dengan benar sekarang, karena kita harus mengganti nama - dan setiap pengguna kode ini perlu mengingat perbedaan implementasi dari spesifikasi.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
OKE ... begitulah. Ini jelek, karena kita perlu mengubah nama metode. Itu tidak sempurna, karena kita perlu menulis kode yang sama dua kali. Tapi begitu selesai, itu berhasil. Baik?
Ya biasanya. Tapi terkadang tidak. Saat membuat Foo, kita sebenarnya bergantung pada kompiler untuk melakukan optimasi nilai balik untuk kita, karena standar C ++ cukup baik bagi vendor kompiler untuk tidak menentukan kapan objek akan dibuat di tempat dan kapan akan disalin ketika mengembalikan sebuah objek sementara dengan nilai dalam C ++. Jadi, jika Foo mahal untuk disalin, pendekatan ini berisiko.
Dan bagaimana jika Foo sama sekali tidak bisa disalin? Baiklah, doh. ( Perhatikan bahwa dalam C ++ 17 dengan elisi salinan yang dijamin, tidak dapat disalin tidak lagi menjadi masalah untuk kode di atas )
Kesimpulan: Membuat pabrik dengan mengembalikan objek memang merupakan solusi untuk beberapa kasus (seperti vektor 2-D yang disebutkan sebelumnya), tetapi masih bukan pengganti umum untuk konstruktor.
3) Konstruksi dua fase
Hal lain yang mungkin akan muncul adalah memisahkan masalah alokasi objek dan inisialisasi. Ini biasanya menghasilkan kode seperti ini:
class Foo {
public:
Foo() {
// empty or almost empty
}
// ...
};
class FooFactory {
public:
void createFooInSomeWay(Foo& foo, some, args);
};
void clientCode() {
Foo staticFoo;
auto_ptr<Foo> dynamicFoo = new Foo();
FooFactory factory;
factory.createFooInSomeWay(&staticFoo);
factory.createFooInSomeWay(&dynamicFoo.get());
// ...
}
Orang mungkin berpikir itu bekerja seperti pesona. Satu-satunya harga yang kami bayar dalam kode kami ...
Karena saya sudah menulis semua ini dan meninggalkan ini sebagai yang terakhir, saya juga harus tidak menyukainya. :) Kenapa?
Pertama-tama ... Saya sungguh-sungguh tidak menyukai konsep konstruksi dua fase dan saya merasa bersalah ketika menggunakannya. Jika saya mendesain objek saya dengan pernyataan bahwa "jika ada, itu dalam keadaan valid", saya merasa bahwa kode saya lebih aman dan lebih rentan kesalahan. Saya suka seperti itu.
Harus membatalkan konvensi itu DAN mengubah desain objek saya hanya untuk tujuan membuat pabrik itu .. yah, berat.
Saya tahu bahwa hal di atas tidak akan meyakinkan banyak orang, jadi mari saya berikan argumen yang lebih kuat. Menggunakan konstruksi dua fase, Anda tidak dapat:
- inisialisasi
const
atau variabel anggota referensi, - meneruskan argumen ke konstruktor kelas dasar dan konstruktor objek anggota.
Dan mungkin ada beberapa kelemahan yang tidak dapat saya pikirkan saat ini, dan saya bahkan tidak merasa wajib karena poin-poin di atas sudah meyakinkan saya.
Jadi: bahkan tidak dekat dengan solusi umum yang baik untuk mengimplementasikan pabrik.
Kesimpulan:
Kami ingin memiliki cara instantiasi objek yang akan:
- memungkinkan untuk instantiasi seragam terlepas dari alokasi,
- memberikan nama yang berbeda dan bermakna untuk metode konstruksi (sehingga tidak bergantung pada kelebihan argumen),
- tidak memperkenalkan hit kinerja yang signifikan dan, lebih disukai, hit kode signifikan, terutama di sisi klien
- bersifat umum, seperti pada: mungkin untuk diperkenalkan untuk kelas apa pun.
Saya percaya saya telah membuktikan bahwa cara yang saya sebutkan tidak memenuhi persyaratan itu.
Ada petunjuk? Tolong berikan saya solusi, saya tidak ingin berpikir bahwa bahasa ini tidak akan memungkinkan saya untuk menerapkan konsep sepele seperti itu.
delete
. Metode semacam ini baik-baik saja, asalkan "didokumentasikan" (kode sumber adalah dokumentasi ;-)) sehingga penelepon memiliki kepemilikan pointer (baca: bertanggung jawab untuk menghapusnya jika perlu).
unique_ptr<T>
bukan T*
.