Manajemen parameter dalam aplikasi OOP

15

Saya sedang menulis aplikasi OOP ukuran sedang di C ++ sebagai cara untuk menerapkan prinsip-prinsip OOP.

Saya memiliki beberapa kelas dalam proyek saya, dan beberapa dari mereka perlu mengakses parameter konfigurasi run-time. Parameter ini dibaca dari beberapa sumber selama pengaktifan aplikasi. Beberapa dibaca dari file konfigurasi di home-dir pengguna, beberapa argumen baris perintah (argv).

Jadi saya membuat kelas ConfigBlock. Kelas ini membaca semua sumber parameter dan menyimpannya dalam struktur data yang sesuai. Contohnya adalah path- dan nama file yang dapat diubah oleh pengguna dalam file konfigurasi, atau flag --verbose CLI. Kemudian, seseorang dapat menelepon ConfigBlock.GetVerboseLevel()untuk membaca parameter khusus ini.

Pertanyaan saya: Apakah ini praktik yang baik untuk mengumpulkan semua data konfigurasi runtime seperti itu dalam satu kelas?

Kemudian, kelas saya perlu akses ke semua parameter ini. Saya dapat memikirkan beberapa cara untuk mencapai ini, tetapi saya tidak yakin mana yang harus diambil. Konstruktor kelas dapat berupa referensi ke ConfigBlock saya, seperti

public:
    MyGreatClass(ConfigBlock &config);

Atau mereka hanya menyertakan header "CodingBlock.h" yang berisi definisi CodingBlock saya:

extern CodingBlock MyCodingBlock;

Kemudian, hanya file .cpp kelas yang perlu dimasukkan dan menggunakan hal-hal ConfigBlock.
File .h tidak memperkenalkan antarmuka ini kepada pengguna kelas. Namun, antarmuka ke ConfigBlock masih ada, namun, disembunyikan dari file .h.

Apakah baik menyembunyikannya dengan cara ini?

Saya ingin antarmuka menjadi sekecil mungkin, tetapi pada akhirnya, saya kira setiap kelas yang membutuhkan parameter konfigurasi harus memiliki koneksi ke ConfigBlock saya. Tapi, seperti apa hubungan ini?

lugge86
sumber

Jawaban:

10

Saya cukup pragmatis, tetapi perhatian utama saya di sini adalah bahwa Anda mungkin membiarkan ini ConfigBlockmendominasi desain antarmuka Anda dengan cara yang mungkin buruk. Ketika Anda memiliki sesuatu seperti ini:

explicit MyGreatClass(const ConfigBlock& config);

... antarmuka yang lebih tepat mungkin seperti ini:

MyGreatClass(int foo, float bar, const string& baz);

... Yang bertentangan dengan hanya memetik ceri foo/bar/bazbidang ini dari besar-besaran ConfigBlock.

Desain Antarmuka Malas

Di sisi positifnya, jenis desain ini memudahkan untuk merancang antarmuka yang stabil untuk konstruktor Anda, misalnya, karena jika Anda akhirnya membutuhkan sesuatu yang baru, Anda bisa memuatnya ke dalam ConfigBlock(mungkin tanpa perubahan kode) dan kemudian menambahkan pilih barang baru apa pun yang Anda butuhkan tanpa perubahan antarmuka apa pun, hanya perubahan pada implementasi MyGreatClass.

Jadi ini baik pro dan kontra bahwa ini membebaskan Anda merancang antarmuka yang lebih hati-hati yang hanya menerima input yang sebenarnya dibutuhkan. Ini menerapkan pola pikir, "Beri saya gumpalan data yang besar ini, saya akan memilih apa yang saya butuhkan darinya" sebagai kebalikan dari sesuatu yang lebih seperti, "Parameter yang tepat inilah yang diperlukan antarmuka ini untuk bekerja."

Jadi pasti ada beberapa pro di sini, tetapi mereka mungkin sangat kalah dengan kontra.

Kopel

Dalam skenario ini, semua kelas yang dibangun dari ConfigBlockinstance akhirnya memiliki dependensi mereka terlihat seperti ini:

masukkan deskripsi gambar di sini

Ini bisa menjadi PITA, misalnya, jika Anda ingin menguji unit Class2dalam diagram ini secara terpisah. Anda mungkin harus mensimulasikan secara dangkal berbagai ConfigBlockinput yang berisi bidang yang relevan Class2tertarik untuk dapat mengujinya di bawah berbagai kondisi.

Dalam segala jenis konteks baru (baik tes unit atau proyek baru), setiap kelas seperti itu dapat menjadi lebih berat untuk digunakan kembali, karena pada akhirnya kita harus selalu membawa ConfigBlockserta untuk perjalanan, dan mengaturnya demikian.

Dapat digunakan kembali / Deployability / Testability

Alih-alih jika Anda merancang antarmuka ini dengan tepat, kami dapat memisahkannya dari ConfigBlockdan berakhir dengan sesuatu seperti ini:

masukkan deskripsi gambar di sini

Jika Anda perhatikan dalam diagram di atas, semua kelas menjadi independen (kopling aferen / keluarnya berkurang 1).

Hal ini menyebabkan kelas yang lebih mandiri (setidaknya tidak tergantung dari ConfigBlock) yang dapat lebih mudah untuk (kembali) digunakan / diuji dalam skenario / proyek baru.

Sekarang Clientkode ini akhirnya menjadi salah satu yang harus bergantung pada segalanya dan mengumpulkan semuanya bersama-sama. Beban akhirnya ditransfer ke kode klien ini untuk membaca bidang yang sesuai dari a ConfigBlockdan meneruskannya ke kelas yang sesuai sebagai parameter. Namun kode klien seperti itu pada umumnya dirancang secara sempit untuk konteks tertentu, dan potensinya untuk digunakan kembali biasanya akan menjadi nihil atau ditutup (mungkin itu adalah mainfungsi titik masuk aplikasi Anda atau semacamnya).

Jadi dari sudut pandang usabilitas dan pengujian, ini dapat membantu untuk membuat kelas-kelas ini lebih mandiri. Dari sudut pandang antarmuka untuk mereka yang menggunakan kelas Anda, itu juga dapat membantu untuk secara eksplisit menyatakan parameter apa yang mereka butuhkan alih-alih hanya satu besar ConfigBlockyang memodelkan seluruh semesta bidang data yang diperlukan untuk semuanya.

Kesimpulan

Secara umum, jenis desain berorientasi kelas yang tergantung pada monolit yang memiliki semua yang dibutuhkan cenderung memiliki karakteristik seperti ini. Penerapannya, penerapannya, dapat digunakan kembali, dapat diuji, dll. Sebagai hasilnya dapat terdegradasi secara signifikan. Namun mereka dapat menyederhanakan desain antarmuka jika kita mencoba putaran positif di atasnya. Terserah Anda untuk mengukur pro dan kontra dan memutuskan apakah trade-off itu sepadan. Biasanya jauh lebih aman untuk melakukan kesalahan terhadap desain semacam ini di mana Anda memilih ceri dari monolit di kelas yang umumnya dimaksudkan untuk memodelkan desain yang lebih umum dan dapat diterapkan secara luas.

Terakhir tapi bukan yang akhir:

extern CodingBlock MyCodingBlock;

... ini berpotensi lebih buruk (lebih condong?) dalam hal karakteristik yang dijelaskan di atas daripada pendekatan injeksi dependensi, karena akhirnya menggabungkan kelas Anda tidak hanya untuk ConfigBlocks, tetapi langsung ke contoh spesifik itu. Itu lebih lanjut menurunkan penerapan / penyebaran / testability.

Saran umum saya adalah kesalahan dalam mendesain antarmuka yang tidak bergantung pada monolit semacam ini untuk memberikan parameternya, setidaknya untuk kelas yang paling umum berlaku yang Anda desain. Dan hindari pendekatan global tanpa injeksi ketergantungan jika Anda bisa kecuali Anda benar-benar memiliki alasan yang sangat kuat dan percaya diri untuk tidak menghindarinya.

marstato
sumber
1

Biasanya konfigurasi aplikasi dikonsumsi terutama oleh objek pabrik. Setiap objek yang mengandalkan konfigurasi harus dihasilkan dari salah satu objek pabrik tersebut. Anda bisa menggunakan Pola Pabrik Abstrak untuk menerapkan satu kelas yang mengambil seluruh ConfigBlockobjek. Kelas ini akan memaparkan metode publik untuk mengembalikan objek pabrik lain, dan hanya akan lulus pada bagian yang ConfigBlockrelevan dengan objek pabrik tertentu. Dengan cara itu pengaturan konfigurasi "menetes" dari ConfigBlockobjek ke anggotanya, dan dari pabrik pabrik ke pabrik.

Saya akan menggunakan C # karena saya tahu bahasa lebih baik, tetapi ini harus mudah ditransfer ke C ++.

public class ConfigBlock
{
    public ConfigBlock()
    {
        // Load config data and
        // connectionSettings = new ConnectionConfig();
        // connectionSettings...
    }

    private ConnectionConfig connectionSettings;

    public ConnectionConfig GetConnectionSettings()
    {
        return connectionSettings;
    }
}

public class FactoryProvider
{
    public FactoryProvider(ConfigBlock config)
    {
        this.config = config;
    }

    private ConfigBlock config;

    public ConnectionFactory GetConnectionFactory()
    {
        ConnectionConfig connectionSettings = config.GetConnectionSettings();

        return new ConnectionFactory(connectionSettings);
    }
}

public class ConnectionFactory
{
    public ConnectionFactory(ConnectionConfig settings)
    {
        this.settings = settings;
    }

    private ConnectionConfig settings;

    public Connection GetConnection()
    {
        return new Connection(settings.Hostname, settings.Port, settings.Username, settings.Password);
    }
}

Setelah itu Anda memerlukan semacam kelas yang bertindak sebagai "aplikasi" yang akan dipakai dalam prosedur utama Anda:

// Your main procedure (yeah I'm bending the rules of C# a tad here,
// but you get the point).
int Main(string[] args)
{
    Application app = new Application();

    app.Run();
}

public class Application
{
    public Application()
    {
        config = new ConfigBlock();
        factoryProvider = new FactoryProvider(config);
    }

    private ConfigBlock config;
    private FactoryProvider factoryProvider;

    public void Run()
    {
        ConnectionFactory connections = factoryProvider.GetConnectionFactory();
        Connection connection = connections.GetConnection();

        connection.Connect();

        // Enter into your main loop and do what this program is meant to do
    }
}

Sebagai satu catatan terakhir, ini dikenal sebagai "objek penyedia" dalam .NET berbicara. Objek penyedia di .NET sepertinya menggabungkan data konfigurasi ke objek pabrik, yang pada dasarnya adalah apa yang ingin Anda lakukan di sini.

Lihat juga Pola Penyedia untuk Pemula . Sekali lagi, ini diarahkan pada pengembangan .NET, tetapi dengan C # dan C ++ keduanya bahasa berorientasi objek, pola sebagian besar harus ditransfer antara keduanya.

Bacaan bagus lainnya yang terkait dengan pola ini: Model Penyedia .

Terakhir, kritik terhadap pola ini: Penyedia bukan sebuah pola

Greg Burghardt
sumber
Semuanya baik, kecuali tautan ke model penyedia. Refleksi tidak didukung oleh c ++, dan itu tidak akan berfungsi.
BЈовић
@ BЈовић: Benar. Refleksi kelas tidak ada, namun Anda bisa membangun solusi manual, yang pada dasarnya beralih ke switchpernyataan atau ifpengujian pernyataan terhadap nilai yang dibaca dari file konfigurasi.
Greg Burghardt
0

Pertanyaan pertama: apakah ini praktik yang baik untuk mengumpulkan semua data konfigurasi runtime seperti itu dalam satu kelas?

Iya. Lebih baik memusatkan konstanta dan nilai runtime dan kode untuk membacanya.

Konstruktor kelas dapat diberi referensi ke ConfigBlock saya

Ini buruk: sebagian besar konstruktor Anda tidak akan membutuhkan sebagian besar nilai. Sebagai gantinya, buat antarmuka untuk segala hal yang tidak sepele untuk dikonstruksi:

kode lama (proposal Anda):

MyGreatClass(ConfigBlock &config);

kode baru:

struct GreatClassData {/*...*/}; // initialization data for MyGreatClass
GreatClassData ConfigBlock::great_class_values();

instantiate MyGreatClass:

auto x = MyGreatClass{ current_config_block.great_class_values() };

Di sini, current_config_blockadalah instance dari ConfigBlockkelas Anda (yang berisi semua nilai Anda) dan MyGreatClasskelas menerima GreatClassDatainstance. Dengan kata lain, hanya berikan kepada konstruktor data yang mereka butuhkan, dan tambahkan fasilitas kepada Anda ConfigBlockuntuk membuat data itu.

Atau mereka hanya menyertakan header "CodingBlock.h" yang berisi definisi CodingBlock saya:

 extern CodingBlock MyCodingBlock;

Kemudian, hanya file .cpp kelas yang perlu dimasukkan dan menggunakan hal-hal ConfigBlock. File .h tidak memperkenalkan antarmuka ini kepada pengguna kelas. Namun, antarmuka ke ConfigBlock masih ada, namun, disembunyikan dari file .h. Apakah baik menyembunyikannya dengan cara ini?

Kode ini menunjukkan bahwa Anda akan memiliki instance CodingBlock global. Jangan lakukan itu: biasanya Anda harus memiliki instance yang dideklarasikan secara global, di titik masuk apa pun yang digunakan aplikasi Anda (fungsi utama, DllMain, dll) dan meneruskannya sebagai argumen di mana pun Anda butuhkan (tetapi seperti yang dijelaskan di atas, Anda tidak boleh lulus seluruh kelas di sekitar, cukup tampilkan antarmuka di sekitar data, dan lewati itu).

Juga, jangan ikat kelas klien Anda (Anda MyGreatClass) dengan jenis CodingBlock; Ini berarti bahwa, jika Anda MyGreatClassmengambil string dan lima bilangan bulat, Anda akan lebih baik melewati string dan bilangan bulat itu, daripada Anda akan meneruskan dalam a CodingBlock.

utnapistim
sumber
Saya pikir itu ide yang baik untuk memisahkan pabrik dari konfigurasi. Tidak memuaskan bahwa implementasi konfigurasi harus tahu cara membuat instantiate komponen, karena ini pasti menghasilkan ketergantungan 2 arah di mana sebelumnya, hanya ketergantungan 1 arah. Ini memiliki implikasi besar ketika memperluas kode Anda, terutama ketika menggunakan perpustakaan bersama di mana antarmuka sangat penting
Joel Cornett
0

Jawaban singkat:

Anda tidak memerlukan semua pengaturan untuk setiap modul / kelas dalam kode Anda. Jika Anda melakukannya, maka ada sesuatu yang salah dengan desain berorientasi objek Anda. Terutama dalam kasus pengujian unit pengaturan semua variabel yang tidak Anda butuhkan dan melewati objek itu tidak akan membantu dengan membaca atau memelihara.

Dawid Pura
sumber
Dengan cara ini saya dapat mengumpulkan kode parser (parse command line dan file config) di satu lokasi pusat. Kemudian, setiap kelas dapat memilih parameter yang relevan dari sana. Apa desain yang bagus menurut Anda?
lugge86
Mungkin saya salah menulis - maksud saya Anda (dan ini merupakan praktik yang baik) untuk memiliki abstraksi umum dengan semua pengaturan yang didapat dari file konfigurasi / variabel lingkungan - yang mungkin ConfigBlockkelas Anda . Intinya di sini adalah untuk tidak menyediakan semua, dalam hal ini, konteks keadaan sistem, hanya khusus, nilai yang diperlukan untuk melakukannya.
Dawid Pura