Parameterisasi tidak pasti sebagai metode universal untuk membangun arsitektur aplikasi di C ++ dan Java untuk minimum. harga

C ++ adalah bahasa yang membingungkan, dan kelemahan utamanya adalah sulitnya membuat blok kode yang terisolasi. Dalam proyek tipikal, semuanya tergantung pada segalanya. Artikel ini menunjukkan cara menulis kode yang sangat terisolasi yang minimal tergantung pada perpustakaan tertentu (termasuk yang standar), implementasi, mengurangi ketergantungan setiap potongan kode ke satu set antarmuka. Selain itu, solusi arsitektur untuk parameterisasi kode akan diusulkan, yang mungkin menarik tidak hanya programmer C ++, tetapi juga programmer Java. Dan yang penting, solusi yang diusulkan sangat ekonomis dalam hal waktu pengembangan.

Penafian : Dalam artikel ini saya telah mengumpulkan ide-ide saya tentang arsitektur ideal. Beberapa ide bukan milik saya (tetapi saya tidak ingat yang mana), beberapa ide adalah hal biasa dan diketahui oleh semua orang - ini tidak penting, karena saya tidak menawarkan ide-ide saya tentang arsitektur yang baik, tetapi kode khusus yang memungkinkan arsitektur ini didekati dengan harga minimum.

Penafian N2 : Saya akan senang dengan umpan balik konstruktif yang diungkapkan dalam kata-kata. Jika Anda mengerti lebih buruk dari saya, dan memarahi saya, itu berarti bahwa di suatu tempat saya belum cukup jelas menjelaskan, dan masuk akal untuk mengerjakan ulang teks. Jika Anda mengerti lebih baik dari saya, itu berarti saya akan mendapatkan pengalaman berharga. Terima kasih sebelumnya.

Penafian N3 : Saya menulis aplikasi besar dari awal, tetapi tidak menulis aplikasi server dan klien perusahaan. Semuanya berbeda di sana dan, mungkin, pengalaman saya akan terasa aneh bagi spesialis di bidang ini. Dan artikelnya bukan tentang itu, masalah skalabilitas yang sama tidak dipertimbangkan di sini sama sekali.
Penafian N4 ( Pembaruan. Berdasarkan komentar): Beberapa komentator telah menyarankan agar saya menemukan kembali Fowler dan menawarkan pola desain yang sudah lama dikenal. Ini jelas bukan masalahnya. Saya mengusulkan alat parameterisasi yang sangat kecil yang memungkinkan Anda untuk menerapkan pola-pola ini dengan minimum coretan. Termasuk Fowler's Dependency Injection dan Service Locator, tetapi tidak hanya - menggunakan kelas TypedSet Anda juga dapat menerapkan serangkaian strategi secara ekonomis. Dalam hal ini, Fowler diakses melalui jalur, yang mahal - alat nol biaya saya, nol biaya (jika benar-benar ketat, maka log (N), bukan 2M * log (N), di mana M adalah panjang dari string parameter untuk Locator Layanan. setelah kemunculan constexpr typeid di c ++ 20, harga seharusnya menjadi nol sama sekali). Karena itu, saya meminta Anda untuk tidak memperluas arti artikel ke pola desain. Di sini Anda hanya akan menemukan metode untuk penerapan pola-pola ini secara murah.

Contohnya akan di C ++, tetapi semua hal di atas cukup dapat diterapkan di Jawa. Mungkin, seiring waktu, saya akan memberikan kode kerja untuk Java jika permintaan ini ada di komentar dari Anda.

Bagian 1. Arsitektur bulat dalam ruang hampa


Sebelum menyelesaikan semua kesulitan dengan cerdas, Anda harus membuatnya dengan benar. Dengan mahir menciptakan kesulitan untuk diri Anda di tempat yang tepat, Anda dapat sangat memfasilitasi solusi mereka. Untuk ini, kami merumuskan tujuan untuk solusi yang kami akan datang dengan metode - prinsip minimum arsitektur yang baik.

Faktanya, keajaiban arsitektur yang baik hanyalah dua prinsip, dan apa yang tertulis di bawah ini hanyalah sebuah decoding. Prinsip pertama adalah uji kode. Testabilitas seperti benang Ariadne yang mengarahkan Anda ke arsitektur yang baik. Jika Anda tidak tahu cara menulis tes untuk fungsionalitas, maka Anda telah merusak arsitekturnya. Jika Anda tidak tahu cara membuat arsitektur yang baik, pikirkan tes apa yang akan dilakukan untuk fungsionalitas yang Anda rencanakan - dan Anda akan secara otomatis membuat batang kualitas arsitektur untuk diri sendiri, dan cukup tinggi. Pikiran pada tes secara otomatis meningkatkan modularitas, konektivitas yang lebih rendah, dan membuat arsitektur lebih logis.

Dan maksud saya bukan TDD. Penyakit khas banyak pemrogram adalah penyembahan teknologi keagamaan yang dibaca di suatu tempat tanpa memahami batas efektivitasnya. TDD bagus ketika beberapa programmer mengerjakan kode, ketika ada departemen pengujian dan pihak berwenang memiliki pemahaman tentang mengapa praktik pengkodean yang baik diperlukan dan bersedia membayar tidak hanya untuk beberapa kode yang memecahkan masalah, tetapi juga untuk keandalannya. Jika atasan Anda tidak siap membayar, Anda harus bekerja lebih ekonomis. Meskipun demikian, Anda masih harus menguji kode tersebut - kecuali, tentu saja, Anda memiliki rasa percaya diri.

Prinsip kedua adalah modularitas. Lebih tepatnya, modularitas yang sangat terisolasi tanpa menggunakan perpustakaan / hardcode yang tidak terkait dengan modul itu sendiri. Sekarang ketika mendesain arsitektur server, adalah modis untuk membagi monolith menjadi layanan microser. Saya akan memberi tahu Anda rahasia yang mengerikan - setiap modul di monolith harus seperti layanan mikro. Dalam arti bahwa itu harus mudah menonjol dari kode umum dengan minimal header yang terhubung di lingkungan pengujian. Belum jelas, tetapi saya akan menjelaskan dengan sebuah contoh: Apakah Anda pernah mencoba mengalokasikan shared_ptr dari peningkatan? Jika pada saat yang sama Anda berhasil menyeret tidak hanya seluruh dorongan, tetapi hanya setengah dari bahan bakunya, maka ini berarti Anda membunuh tiga hingga lima hari untuk memotong kecanduan yang tidak perlu !!! Pada saat yang sama, Anda menyeret sepanjang fakta bahwa shared_ptr pasti tidak ada hubungannya !!!

Dan ini lebih buruk daripada kesalahan - ini adalah kejahatan arsitektur.

Dengan arsitektur yang baik, Anda harus dapat menghapus shared_ptr, tanpa rasa sakit dan cepat mengganti segala sesuatu yang tidak terkait dengan shared_ptr dengan versi uji. Misalnya, versi uji pengalokasi. Atau lupakan dorongannya. Katakanlah Anda menulis parser xml / html. Anda perlu bekerja dengan string dan bekerja dengan file untuk parser. Dan jika kita berbicara tentang arsitektur ideal yang tidak terikat dengan kebutuhan perusahaan produksi / perangkat lunak tertentu, maka untuk parser dengan arsitektur ideal kita tidak memiliki hak untuk menggunakan std :: istream, std :: file_system, std :: string dan operasi pencarian hardcode dengan string di parser. Kita harus menyediakan antarmuka aliran, antarmuka untuk operasi file (mungkin dibagi menjadi sub-antarmuka, tetapi akses ke sub-antarmuka masih harus dilakukan melalui antarmuka modul operasi file), antarmuka untuk bekerja dengan string, antarmuka pengalokasi, dan idealnya juga antarmuka untuk baris itu sendiri. Sebagai hasilnya, kami tanpa kesulitan dapat mengganti segala sesuatu yang tidak terkait dengan penguraian dengan test blank, atau menyisipkan versi uji pengalokasi / bekerja dengan pencarian file / string dengan pemeriksaan tambahan. Dan fleksibilitas solusi akan meningkat - besok, di bawah antarmuka aliran, tidak akan ada file, tetapi sebuah situs di suatu tempat di Internet, dan tidak ada yang akan melihatnya. Anda dapat mengganti perpustakaan standar dengan Qt, dan kemudian beralih ke visual c ++, dan kemudian mulai menggunakan hanya hal-hal Linux - dan perubahannya akan minimal. Sebagai spoiler, saya akan mengatakan bahwa dengan pendekatan ini, pertanyaan harga muncul dalam pertumbuhan penuh - untuk menutupi segala sesuatu dengan antarmuka, termasuk elemen dari perpustakaan standar, mahal, tetapi ini bukan tujuan, tetapi solusi.

Secara umum, prinsip radikal module-as-microservice yang dicanangkan dalam artikel ini adalah titik lemah dalam C ++ dan umumnya ditambah kode. Jika Anda membuat file deklarasi dan memisahkan antarmuka secara terpisah dari implementasinya, Anda masih dapat membuat independensi / isolasi file cpp dari satu sama lain, dan kemudian, relatif, bukan 100%, maka tajuk biasanya ditenun menjadi monolit padat, yang darinya tidak ada yang dapat dihancurkan tanpa daging. Dan meskipun ini memiliki efek buruk pada waktu kompilasi, itu benar. Selain itu, bahkan jika kemandirian judul tercapai, ini secara otomatis berarti ketidakmampuan untuk menggabungkan kelas. Sebenarnya, satu-satunya cara untuk mencapai independensi baik file .cpp dan header di c ++ adalah dengan mendeklarasikan kelas yang sudah digunakan (tanpa mendefinisikannya), dan kemudian hanya menggunakan pointer ke mereka. segera setelah Anda menggunakan kelas itu sendiri dan bukan penunjuk kelas di file header (yaitu, agregat), Anda akan membuat banyak semua .cpp-shnik yang menyertakan tajuk ini, dan .cpp-shnik yang berisi definisi kelas. Masih ada fastpimpl, tetapi hanya dijamin untuk membuat dependensi pada level cpp.

Jadi, untuk arsitektur yang baik, isolasi modul adalah penting - kemampuan untuk menarik satu modul dengan makro penghubung pertama dan jenis perpustakaan utama, dengan judul kedua untuk deklarasi dan beberapa inklusi yang menghubungkan satu set antarmuka. Dan hanya apa yang berhubungan dengan fungsi ini, dan segala sesuatu yang lain harus disimpan dalam modul lain dan hanya dapat diakses melalui antarmuka.

Kami menyatakan fitur utama arsitektur yang baik, termasuk poin yang ditunjukkan di atas, poin demi poin.

Mari kita mendefinisikan istilah "Modul". Modul adalah jumlah fungsi yang berhubungan secara logis. Misalnya, bekerja dengan aliran atau kerja file, atau parser html.

Modul "File Work" dapat menggabungkan banyak fungsi - buka file, tutup, posisi, baca properti, baca ukuran file. Pada saat yang sama, pemindai folder dapat dirancang sebagai bagian dari antarmuka "File Work", atau sebagai modul terpisah, dan bekerja dengan stream dapat ditempatkan dalam modul terpisah tentunya. Namun, yang tidak mengganggu pengorganisasian akses ke semua modul lain ke stream dan pemindai folder secara tidak langsung, melalui "File Work". Ini tidak perlu, tetapi cukup logis.

  1. Modularitas. Imperative "Module-as-microservice".
  2. Alokasi 20% dari kode dieksekusi 80% dari waktu di perpustakaan terpisah - inti dari program
  3. Testabilitas masing-masing fungsionalitas dari setiap modul
  4. Antarmuka, tidak ada hardcode. Anda hanya dapat memanggil hardcode yang secara langsung terkait dengan fungsionalitas modul, dan Anda harus membuat panggilan perpustakaan langsung lainnya ke modul terpisah dan mengaksesnya melalui antarmuka.
  5. Isolasi lengkap modul oleh antarmuka dari lingkungan eksternal. Larangan implementasi "memaku" yang tidak terkait dengan fungsionalitas kelas. Dan yang lebih radikal, mengisolasi perpustakaan (termasuk yang standar) dengan antarmuka / adaptor / dekorator
  6. Agregasi kelas atau membuat variabel kelas atau fastpimpl hanya digunakan ketika sangat penting untuk kinerja.

Tentu saja, kami akan mencari cara untuk dengan cepat mencapai semua ini dengan harga yang lebih rendah, tetapi saya ingin menarik perhatian ke masalah lain, solusi yang akan menjadi bonus bagi kami - transfer parameter yang tergantung pada platform. Misalnya, jika Anda perlu membuat kode yang berfungsi sama di Android dan Windows, maka logis untuk mengalokasikan algoritma yang tergantung platform ke dalam modul terpisah. Dalam hal ini, mungkin, implementasi untuk android mungkin memerlukan referensi ke lingkungan Java (jni), JNIEnv *, dan mungkin beberapa objek Java. Dan implementasi pada Windows mungkin memerlukan folder kerja program (yang pada android dapat diminta dari sistem, memiliki JNIEnv *). Kuncinya adalah bahwa JNIEnv * yang sama tidak ada dalam konteks Windows, sehingga bahkan serikat yang diketik atau alternatif c ++ untuk std :: varian tidak mungkin. Anda dapat, tentu saja, menggunakan vektor * void atau std :: any vector sebagai parameter, tetapi jujur, ini adalah kruk atipikal. Atypical - karena menolak keunggulan utama c ++, pengetikan yang kuat. Dan ini lebih berbahaya daripada SARS.

Lebih lanjut kami akan menganalisis bagaimana menyelesaikan masalah ini dengan cara yang sangat spesifik.

Bagian 2. Peluru ajaib dan label harganya


Jadi, katakanlah kita memiliki sejumlah besar kode yang perlu ditulis dari awal, dan hasilnya akan menjadi proyek yang sangat besar.

Bagaimana bisa dirakit sesuai dengan prinsip yang telah kita tentukan?

Cara klasik, yang disetujui oleh semua manual, adalah untuk membagi semuanya menjadi antarmuka dan strategi. Dengan bantuan antarmuka dan strategi, jika ada banyak dari mereka, subproblem proyek kami dapat diisolasi sedemikian rupa sehingga prinsip "module-as-microservice" akan mulai bekerja di sana. Tetapi pengalaman pribadi saya adalah bahwa jika Anda membagi proyek menjadi 20-30 bagian, yang akan diisolasi ke tingkat "modul-as-microservice", maka Anda akan berhasil. Tetapi fitur utama dari arsitektur yang baik adalah kemampuan untuk menguji setiap kelas di luar konteks proyek. Dan jika Anda sudah mengisolasi masing-masing kelas, maka sudah ada lebih dari 500 modul, dan dalam pengalaman saya, ini meningkatkan waktu pengembangan sebanyak 3-5 kali, yang berarti bahwa dalam "kondisi pertempuran" Anda tidak akan melakukan ini dan akan berkompromi antara harga dan kualitas.

Seseorang mungkin ragu, dan akan menjadi haknya sendiri. Mari kita membuat perkiraan kasar. Biarkan kelas menengah memiliki 3-5 anggota dan 20 fungsi dan 3 konstruktor. Ditambah 6-10 getter dan setter (mutator) untuk akses ke anggota kami. Total sekitar 40 unit di kelas. Dalam proyek tipikal, setiap kelas "pusat" membutuhkan akses ke rata-rata lima fungsi, bukan pusat ke 3. Sebagai contoh, sangat banyak kelas membutuhkan pengalokasi, sistem file, bekerja dengan string, bekerja dengan stream, dan akses ke database.

Setiap strategi / antarmuka akan memerlukan satu anggota tipe std::shared_ptr<CreateStreamStrategy> m_create_stream; . Dua mutator, plus inisialisasi di masing-masing dari tiga konstruktor. plus di suatu tempat dalam inisialisasi kelas kami, Anda perlu memanggil sesuatu seperti myclass->SetCreateStreamStrategy( my_create_stream_strategy ) beberapa kali, dengan total 8 unit per antarmuka / strategi, dan karena kami memiliki sekitar lima unit, akan ada 40 unit. Artinya, kami membuat kelas sumber dua kali lebih rumit. Dan hilangnya kesederhanaan pasti akan mempengaruhi keterbacaan, dan di tempat lain dalam proses debugging, dan setengah kali, terlepas dari kenyataan bahwa tidak ada yang tampaknya pada dasarnya berubah.

Jadi pertanyaannya adalah. Bagaimana melakukan hal yang sama, tetapi dengan harga minimum? Hal pertama yang terlintas dalam pikiran adalah parameterisasi statis pada templat, dengan gaya Alexandrescu dan perpustakaan Loki.

Kami menulis kelas dengan gaya

 template < struct Traits > class MyClass { public: void DoMainTaskFunction() { ... MyStream stream = Traits::streamwork::Open( stream_name ); ... } }; 

Keputusan ini memiliki semua keunggulan arsitektur yang kami identifikasi pada bagian pertama. Namun ada juga banyak kerugiannya.

Saya sendiri suka shabble, tapi saya menyesal untuk diri sendiri saya akui: templat dalam kode biasa hanya dicintai oleh penyihir templat. Sejumlah besar programmer dengan kata "templat" sedikit mengernyit. Selain itu, di industri, sebagian besar plus sebenarnya tidak plus, tetapi sedikit dilatih kembali dalam c ++ syshnik yang tidak memiliki pengetahuan yang mendalam tentang plus, tetapi jatuh di bawah kata "templat" dan berpura-pura mati.

Jika kita menerjemahkan ini ke dalam bahasa produksi, maka mempertahankan kode pada parameterisasi statis lebih mahal dan lebih rumit.

Pada saat yang sama, jika kita ingin, untuk tujuan keterbacaan yang lebih besar, untuk dengan hati-hati menghapus tubuh fungsi di luar kelas, maka kita mendapatkan banyak coretan dengan nama templat dan parameter templat. Dan jika terjadi kesalahan kompilasi, kami mendapatkan rak panjang yang dapat dibaca oleh manusia dan area bermasalah dengan sekelompok templat bersarang yang kompleks.

Tapi, ada jalan keluar sederhana. Sebagai penyihir templat, saya menyatakan bahwa hampir semua yang dapat dilakukan menggunakan parameterisasi statis / polimorfisme statis dapat ditransfer ke polimorfisme dinamis. Tidak, tentu saja, kami tidak akan membasmi kejahatan templat sampai akhir - tetapi kami tidak akan menyebarkannya dengan tangan yang murah hati untuk parameterisasi di setiap kelas, tetapi akan membatasinya pada beberapa kelas instrumental.

Bagian tiga. Solusi yang diusulkan dan kode yang dikodekan untuk solusi ini


Jadi disana !!! Memenuhi kelas templat TypedSet. Ia mengaitkan satu penunjuk pintar tipe ini dengan satu tipe tunggal. Selain itu, untuk tipe yang ditentukan mungkin memiliki objek, tetapi mungkin tidak. Saya tidak suka namanya - jadi saya akan berterima kasih jika di komentar memberi tahu saya opsi yang lebih sukses.

Satu tipe - satu objek. Tetapi jumlah jenisnya tidak terbatas! Oleh karena itu, Anda dapat melewati kelas seperti itu sebagai parameterizer.

Saya ingin menarik perhatian Anda ke satu titik. Tampaknya pada titik tertentu Anda mungkin memerlukan dua objek di bawah satu antarmuka. Bahkan, jika kebutuhan seperti itu muncul, maka (menurut saya) ini berarti kesalahan arsitektur. Artinya, jika Anda memiliki dua objek di bawah satu antarmuka, maka mereka bukan lagi antarmuka akses fungsional: ini adalah variabel input untuk fungsi tersebut, atau Anda tidak memiliki satu tetapi dua fungsionalitas yang perlu Anda akses, maka lebih baik untuk membagi antarmuka menjadi dua .

Kami akan membuat tiga fungsi dasar: Buat, Dapatkan dan Miliki. Dengan demikian, pembuatan, penerimaan, dan verifikasi keberadaan elemen.

 /// @brief    .      ,    ///           /// class TypedSet { public: template <class TypedElement> void Create( const std::shared_ptr<TypedElement> & value ); template <class TypedElement> std::shared_ptr<TypedElement> Get() const; template <class TypedElement> bool Has() const; size_t GetSize() const { return storage_.size(); } protected: typedef std::map< size_t, std::shared_ptr<void> > Storage; Storage const & storage() const { return storage_; } Storage & get_storage() { return storage_; } private: Storage storage_; }; template <class TypedElement> void TypedSet::Create( const std::shared_ptr<TypedElement> & value ) { size_t hash = typeid(TypedElement).hash_code(); if ( storage().count( hash ) > 0 ) { LogError( "Access Violation" ); return; } std::shared_ptr<void> to_add ( value ); get_storage().insert( std::pair( typeid(TypedElement).hash_code(), to_add ) ); } template <class TypedElement> bool TypedSet::Has() const { size_t hash = typeid(TypedElement).hash_code(); return storage().count( hash ) > 0; } template <class TypedElement> std::shared_ptr<TypedElement> TypedSet::Get() const { size_t hash = typeid(TypedElement).hash_code(); if ( storage().count( hash ) > 0 ) { std::shared_ptr<void> ret( storage().at(hash) ); return std::static_pointer_cast<TypedElement>( ret ); } else { LogError( "Access Violation" ); return std::shared_ptr<TypedElement> (); } } 

Ngomong-ngomong, saya melihat solusi alternatif dari rekan-rekan menulis di Qt. Di sana, akses ke antarmuka yang diinginkan dilakukan melalui singleton, yang "memetakan" antarmuka yang diinginkan, dikemas ke Varaint, melalui baris teks (!!!), dan setelah casting opsi ini, hasilnya dapat digunakan.

 GlobalConfigurator()["FileSystem"].Get().As<FileSystem>() 

Tentu saja berhasil, tetapi overhead menghitung panjang dan lebih lanjut hashing string agak menakutkan bagi jiwa optimis saya. Di sini, biaya overhead adalah nol, karena pilihan antarmuka yang diinginkan dilakukan pada waktu kompilasi.

Berdasarkan pada TypedSet, kita dapat membuat kelas StrategiesSet, yang sudah lebih maju. Di dalamnya kita akan menyimpan tidak hanya satu objek per antarmuka akses untuk setiap fungsional, tetapi juga untuk setiap antarmuka (selanjutnya disebut sebagai strategi) sebuah TypedSet tambahan dengan parameter untuk strategi ini. Saya mengklarifikasi: parameter, tidak seperti variabel fungsi, adalah apa yang ditetapkan sekali selama inisialisasi program atau sekali untuk menjalankan program besar. Parameter memungkinkan Anda untuk membuat kode benar-benar lintas platform. Di sanalah kita menggerakkan seluruh dapur yang bergantung pada platform.

Di sini kita akan memiliki lebih banyak fungsi dasar: Buat, Dapatkan, CreateParamsSet dan GetParamsSet. Belum diletakkan, karena secara arsitektur berlebihan: jika kode Anda merujuk pada fungsionalitas bekerja dengan sistem file, tetapi kode panggilan tidak menyediakannya, Anda hanya dapat melempar pengecualian atau menegaskan, atau membuat program sebukka memanggil fungsi abort () berfungsi.

 class StrategiesSet { public: template <class Strategy> void Create( const std::shared_ptr<Strategy> & value ); template <class Strategy> std::shared_ptr<Strategy> Get(); template <class Strategy> void CreateParamsSet(); template <class Strategy> std::shared_ptr<TypedSet> GetParamsSet(); template <class Strategy, class ParamType> void CreateParam( const std::shared_ptr<ParamType> & value ); template <class Strategy, class ParamType> std::shared_ptr<ParamType> GetParam(); protected: TypedSet const & strategies() const { return strategies_; } TypedSet & get_strategies() { return strategies_; } TypedSet const & params() const { return params_; } TypedSet & get_params() { return params_; } template <class Type> struct ParamHolder { ParamHolder( ) : param_ptr( std::make_shared<TypedSet>() ) {} std::shared_ptr<TypedSet> param_ptr; }; private: TypedSet strategies_; TypedSet params_; }; template <class Strategy> void StrategiesSet::Create( const std::shared_ptr<Strategy> & value ) { get_strategies().Create<Strategy>( value ); } template <class Strategy> std::shared_ptr<Strategy> StrategiesSet::Get() { return get_strategies().Get<Strategy>(); } template <class Strategy> void StrategiesSet::CreateParamsSet( ) { typedef ParamHolder<Strategy> Holder; std::shared_ptr< Holder > ptr = std::make_shared< Holder >( ); ptr->param_ptr = std::make_shared< TypedSet >(); get_params().Create< Holder >( ptr ); } template <class Strategy> std::shared_ptr<TypedSet> StrategiesSet::GetParamsSet() { typedef ParamHolder<Strategy> Holder; if ( get_params().Has< Holder >() ) { return get_params().Get< Holder >()->param_ptr; } else { LogError("StrategiesSet::GetParamsSet : get unexisting!!!"); return std::shared_ptr<TypedSet>(); } } template <class Strategy, class ParamType> void StrategiesSet::CreateParam( const std::shared_ptr<ParamType> & value ) { typedef ParamHolder<Strategy> Holder; if ( !params().Has<Holder>() ) CreateParamsSet<Strategy>(); if ( params().Has<Holder>() ) { std::shared_ptr<TypedSet> params_set = GetParamsSet<Strategy>(); params_set->Create<ParamType>( value ); } else { LogError( "Param creating error: Access Violation" ); } } template <class Strategy, class ParamType> std::shared_ptr<ParamType> StrategiesSet::GetParam() { typedef ParamHolder<Strategy> Holder; if ( params().Has<Holder>() ) { return GetParamsSet<Strategy>()->template Get<ParamType>(); //   template          .    . } else { LogError( "Access Violation" ); return std::shared_ptr<ParamType> (); } } 

Nilai tambah tambahan adalah bahwa pada tahap prototyping Anda dapat membuat satu kelas pengetikan super besar, menjejalkan akses ke semua modul ke dalamnya, dan meneruskannya ke semua modul sebagai parameter, dengan cepat menjadi kecil, dan kemudian diam-diam memecahnya menjadi potongan-potongan yang minimal diperlukan untuk setiap modul.

Yah, dan kasus penggunaan yang kecil dan (belum) terlalu disederhanakan. Saya harap Anda di komentar menyarankan saya apa yang ingin Anda lihat sebagai contoh sederhana, dan saya akan membuat artikel ini menjadi upgrade kecil. Seperti yang dikatakan oleh kebijaksanaan pemrograman populer, "Rilis sedini mungkin dan tingkatkan penggunaan umpan balik setelah rilis."

 class Interface1 { public: virtual void Fun() { printf("\niface1\n");} virtual ~Interface1() {} }; class Interface2 { public: virtual void Fun() { printf("\niface2\n");} virtual ~Interface2() {} }; class Interface3 { public: virtual void Fun() { printf("\niface3\n");} virtual ~Interface3() {} }; class Implementation1 : public Interface1 { public: virtual void Fun() override { printf("\nimpl1\n");} }; class Implementation2 : public Interface2 { public: virtual void Fun() override { printf("\nimpl2\n");} }; class PrintParams { public: virtual ~PrintParams() {} virtual std::string GetOs() = 0; }; class PrintParamsUbuntu : public PrintParams { public: virtual std::string GetOs() override { return "Ubuntu"; } }; class PrintParamsWindows : public PrintParams { public: virtual std::string GetOs() override { return "Windows"; } }; class PrintStrategy { public: virtual ~PrintStrategy() {} virtual void operator() ( const TypedSet& params, const std::string & str ) = 0; }; class PrintWithOsStrategy : public PrintStrategy { public: virtual void operator()( const TypedSet& params, const std::string & str ) override { auto os = params.Get< PrintParams >()->GetOs(); printf(" Printing: %s (OS=%s)", str.c_str(), os.c_str() ); } }; void TestTypedSet() { using namespace std; TypedSet a; a.Create<Interface1>( make_shared<Implementation1>() ); a.Create<Interface2>( make_shared<Implementation2>() ); a.Get<Interface1>()->Fun(); a.Get<Interface2>()->Fun(); Log("Double creation:"); a.Create<Interface1>( make_shared<Implementation1>() ); Log("Get unexisting:"); a.Get<Interface3>(); } void TestStrategiesSet() { using namespace std; StrategiesSet printing; printing.Create< PrintStrategy >( make_shared<PrintWithOsStrategy>() ); printing.CreateParam< PrintStrategy, PrintParams >( make_shared<PrintParamsWindows>() ); auto print_strategy_ptr = printing.Get< PrintStrategy >(); auto & print_strategy = *print_strategy_ptr; auto & print_params = *printing.GetParamsSet< PrintStrategy >(); print_strategy( print_params, "Done!" ); } int main() { TestTypedSet(); TestStrategiesSet(); return 0; } 

Ringkasan


Jadi, kami memecahkan masalah penting: kami hanya meninggalkan antarmuka yang berhubungan langsung dengan fungsi kelas. Sisanya "didorong" ke dalam StrategiesSet, sambil menghindari keduanya mengacaukan kelas dengan elemen yang tidak perlu, dan "memaku" fungsi tertentu yang kami butuhkan untuk algoritme. Ini akan memungkinkan kita tidak hanya untuk menulis kode yang sangat terisolasi, dengan nol ketergantungan pada implementasi dan perpustakaan, tetapi juga untuk menghemat banyak waktu.

Kode untuk kelas contoh dan alat dapat ditemukan di sini.

Pembaruan. dari 11/13/2019
Sebenarnya, kode yang ditampilkan di sini hanyalah contoh sederhana untuk keterbacaan. Faktanya adalah typeid (). Hash_code diimplementasikan dalam kompiler modern secara perlahan dan tidak efisien. Penggunaannya membunuh banyak artinya. Selain itu, seperti yang disarankan 0xd34df00d dihormati, standar tidak menjamin kemampuan untuk membedakan jenis dengan kode hash (dalam prakteknya, pendekatan ini berfungsi). Tetapi contohnya dibaca dengan baik. Saya menulis ulang TypedSet tanpa typeid (). Hash_code (), apalagi, diganti peta dengan array (tetapi dengan kemampuan untuk dengan cepat beralih dari peta ke array dan sebaliknya dengan mengubah satu digit di #jika). Ternyata lebih sulit, tetapi lebih menarik untuk penggunaan praktis.
di coliru
 namespace metatype { struct Counter { size_t GetAndIncrease() { return counter_++; } private: size_t static inline counter_ = 1; }; template <typename Type> struct HashGetterBody { HashGetterBody() : hash_( counter_.GetAndIncrease() ) { } size_t GetHash() { return hash_; } private: Counter counter_; size_t hash_; }; template <typename Type> struct HashGetter { size_t GetHash() {return hasher_.GetHash(); } private: static inline HashGetterBody<Type> hasher_; }; } // namespace metatype template <typename Type> size_t GetTypeHash() { return metatype::HashGetter<Type>().GetHash(); } namespace details { #if 1 //   ,        () class TypedSetStorage { public: static inline const constexpr size_t kMaxTypes = 100; typedef std::array< std::shared_ptr<void>, kMaxTypes > Storage; void Set( size_t hash_index, const std::shared_ptr<void> & value ) { ++size_; assert( hash_index < kMaxTypes ); // too many types data_[hash_index] = value; } std::shared_ptr<void> & Get( size_t hash_index ) { assert( hash_index < kMaxTypes ); return data_[hash_index]; } const std::shared_ptr<void> & Get( size_t hash_index ) const { if ( hash_index >= kMaxTypes ) return empty_ptr_; return data_[hash_index]; } bool Has( size_t hash_index ) const { if ( hash_index >= kMaxTypes ) return 0; return (bool)data_[hash_index]; } size_t GetSize() const { return size_; } private: Storage data_; size_t size_ = 0; static const inline std::shared_ptr<void> empty_ptr_; }; #else //    ,        (std::map) class TypedSetStorage { public: typedef std::map< size_t, std::shared_ptr<void> > Storage; void Set( size_t hash_index, const std::shared_ptr<void> & value ) { data_[hash_index] = value; } std::shared_ptr<void> & Get( size_t hash_index ) { return data_[hash_index]; } const std::shared_ptr<void> & Get( size_t hash_index ) const { return data_.at(hash_index); } bool Has( size_t hash_index ) const { return data_.count(hash_index) > 0; } size_t GetSize() const { return data_.size(); } private: Storage data_; }; #endif } // namespace details /// @brief    .      ,    ///           /// class TypedSet { public: template <class TypedElement> void Create( const std::shared_ptr<TypedElement> & value ); template <class TypedElement> std::shared_ptr<TypedElement> Get() const; template <class TypedElement> bool Has() const; size_t GetSize() const { return storage_.GetSize(); } protected: typedef details::TypedSetStorage Storage; Storage const & storage() const { return storage_; } Storage & get_storage() { return storage_; } private: Storage storage_; }; template <class TypedElement> void TypedSet::Create( const std::shared_ptr<TypedElement> & value ) { size_t hash = GetTypeHash<TypedElement>(); if ( storage().Has( hash ) ) { LogError( "Access Violation" ); return; } std::shared_ptr<void> to_add ( value ); get_storage().Set( hash, to_add ); } template <class TypedElement> bool TypedSet::Has() const { size_t hash = GetTypeHash<TypedElement>(); return storage().Has( hash ); } template <class TypedElement> std::shared_ptr<TypedElement> TypedSet::Get() const { size_t hash = GetTypeHash<TypedElement>(); if ( storage().Has( hash ) ) { std::shared_ptr<void> ret( storage().Get( hash ) ); return std::static_pointer_cast<TypedElement>( ret ); } else { LogError( "Access Violation" ); return std::shared_ptr<TypedElement> (); } } 

Di sini akses dilakukan dalam waktu linier, jenis hash dihitung sebelum main () diluncurkan, kerugian hanya untuk pemeriksaan validasi, yang dapat dibuang jika diinginkan.

Source: https://habr.com/ru/post/id475268/


All Articles