OOP uygulamasında parametre yönetimi


15

C ++ orta ölçekli bir OOP uygulaması OOP ilkeleri uygulama yolu olarak yazıyorum.

Projemde birkaç sınıf var ve bazılarının çalışma zamanı yapılandırma parametrelerine erişmesi gerekiyor. Bu parametreler, uygulamanın başlatılması sırasında çeşitli kaynaklardan okunur. Bazıları kullanıcıların ana dizinindeki bir yapılandırma dosyasından okunur, bazıları ise komut satırı bağımsız değişkenleridir (argv).

Ben de bir sınıf yarattım ConfigBlock. Bu sınıf tüm parametre kaynaklarını okur ve uygun bir veri yapısında saklar. Örnekler, yapılandırma dosyasında kullanıcı tarafından değiştirilebilen yol ve dosya adları veya --verbose CLI bayrağıdır. Daha sonra, ConfigBlock.GetVerboseLevel()bu özel parametreyi okumak için çağrılabilir.

Benim sorum: Bu tür tüm çalışma zamanı yapılandırma verilerini bir sınıfta toplamak iyi bir uygulama mudur?

Sonra, sınıflarımın tüm bu parametrelere erişmesi gerekiyor. Bunu başarmanın birkaç yolunu düşünebilirim, ama hangisini alacağından emin değilim. Bir sınıf yapıcısı gibi ConfigBlock'uma bir referans verilebilir

public:
    MyGreatClass(ConfigBlock &config);

Veya sadece benim CodingBlock tanımını içeren bir başlık "CodingBlock.h" içerir:

extern CodingBlock MyCodingBlock;

Daha sonra, yalnızca sınıflar .cpp dosyasının ConfigBlock öğelerini içermesi ve kullanması gerekir.
.H dosyası bu arabirimi sınıfın kullanıcısına tanıtmaz. Ancak, ConfigBlock arabirimi hala oradadır, ancak .h dosyasından gizlenmiştir.

Bu şekilde saklamak iyi mi?

Arayüzün olabildiğince küçük olmasını istiyorum, ama sonunda, yapılandırma parametrelerine ihtiyaç duyan her sınıfın ConfigBlock'umla bir bağlantısı olması gerektiğini düşünüyorum. Fakat bu bağlantı neye benzemeli?

Yanıtlar:


10

Oldukça pragmatistim, ama burada asıl endişem, bunun ConfigBlockarayüz tasarımlarınıza muhtemelen kötü bir şekilde hükmetmesine izin vermeniz olabilir . Böyle bir şey olduğunda:

explicit MyGreatClass(const ConfigBlock& config);

... daha uygun bir arayüz şöyle olabilir:

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

... bu foo/bar/baztarlaları devasa bir tarladan almak yerine ConfigBlock.

Tembel Arayüz Tasarımı

Artı tarafta, bu tür tasarım, kurucunuz için istikrarlı bir arayüz tasarlamayı kolaylaştırır, örneğin, yeni bir şeye ihtiyacınız varsa, bunu ConfigBlock(muhtemelen herhangi bir kod değişikliği olmadan) ve daha sonra kiraz herhangi bir arabirim değişikliği olmadan ihtiyacınız olan her şeyi seçin, sadece uygulamasında bir değişiklik yapın MyGreatClass.

Yani bu hem profesyonel hem de aleyhte bir şey. "Sadece bana bu büyük veri bloğunu verin, ondan neye ihtiyacım olduğunu seçeceğim" anlayışını uygular , "Bu kesin parametreler bu arayüzün çalışması için ihtiyaç duyduğu şeydir."

Yani burada kesinlikle bazı artılar var, ancak eksilerini ağır bastırabilirler.

Kavrama

Bu senaryoda, bir ConfigBlockörnekten oluşturulan tüm sınıflar , bağımlılıklarının şöyle görünmesini sağlar:

resim açıklamasını buraya girin

Bu bir PITA olabilir, örneğin, Class2bu diyagramda birim testi izole etmek istiyorsanız . Çeşitli koşullar altında test edebilmek için ConfigBlockilgili alanları içeren çeşitli girdileri yüzeysel olarak simüle etmeniz gerekebilir Class2.

Her türlü yeni bağlamda (ister birim test olsun isterse tamamen yeni proje), bu tür sınıflar, her zaman ConfigBlocksürüş için beraberinde getirmek ve kurmak zorunda kaldığımız için (yeniden) kullanım için daha fazla yük haline gelebilir . buna göre.

Yeniden kullanılabilirlik / konuşlandırılabilirliğini / Testedilebilirlik

Bunun yerine, bu arayüzleri uygun şekilde tasarlarsanız, bunları birbirinden ayırabilir ConfigBlockve bunun gibi bir şeyle sonuçlayabiliriz :

resim açıklamasını buraya girin

Yukarıdaki şemada fark ederseniz, tüm sınıflar bağımsız hale gelir (afferent / giden kaplinleri 1 azalır).

Bu ConfigBlock, yeni senaryolarda / projelerde (yeniden) kullanılması / test edilmesi çok daha kolay olabilen çok daha bağımsız sınıflara (en azından bağımsız ) yol açar .

Şimdi bu Clientkod, her şeye bağlı ve hepsini bir araya getirmek zorunda olan kod haline geliyor. Yük, uygun alanları a'dan okumak ve ConfigBlockparametre olarak uygun sınıflara aktarmak için bu istemci koduna aktarılır . Yine de, bu tür istemci kodu genellikle belirli bir bağlam için dar olarak tasarlanmıştır ve yeniden kullanım potansiyeli genellikle zilch veya yine de kapanır (uygulamanızın maingiriş noktası işlevi veya bunun gibi bir şey olabilir).

Bu nedenle, yeniden kullanılabilirlik ve test bakış açısından, bu sınıfları daha bağımsız hale getirmeye yardımcı olabilir. Sınıflarınızı kullananlar için arayüz açısından, ConfigBlockher şey için gerekli olan tüm veri alanları evrenini modelleyen tek bir kitle yerine, hangi parametrelere ihtiyaç duyduklarını açıkça belirtmeye yardımcı olabilir .

Sonuç

Genel olarak, gereken her şeye sahip bir monolite dayanan bu tür bir sınıf odaklı tasarım, bu tür özelliklere sahip olma eğilimindedir. Uygulanabilirlikleri, konuşlandırılabilirlikleri, tekrar kullanılabilirlikleri, test edilebilirlikleri vb. Sonuç olarak önemli ölçüde bozulabilir. Yine de, üzerinde olumlu bir dönüş yapmaya çalışırsak, arayüz tasarımını basitleştirebilirler. Bu artıları ve eksileri ölçmek ve değiş tokuşların buna değip değmeyeceğine karar vermek size kalmış. Genellikle daha genel ve yaygın olarak uygulanabilir bir tasarımı modellemek için tasarlanan sınıflardaki bir monolitten kiraz seçtiğiniz bu tür tasarıma karşı hata yapmak daha güvenlidir.

Sonuncu ama bir o kadar önemli:

extern CodingBlock MyCodingBlock;

... bu, yukarıda açıklanan özellikler açısından bağımlılık enjeksiyon yaklaşımından daha kötüdür (daha fazla çarpık mı?), çünkü sınıflarınızı sadece belirli bir örneğeConfigBlocks değil, doğrudan belirli bir örneğine de bağlar. Bu, uygulanabilirliği / konuşlandırılabilirliği / test edilebilirliği daha da bozmaktadır.

Genel tavsiyem, en azından tasarladığınız en genel olarak uygulanabilir sınıflar için parametrelerini sağlamak için bu tür monolitlere bağlı olmayan arayüzler tasarlama tarafında hata yapmaktır. Ve gerçekten kaçınmak için çok güçlü ve kendinden emin bir nedeniniz yoksa, bağımlılık enjeksiyonu olmadan küresel yaklaşımdan kaçının.


1

Genellikle bir uygulamanın yapılandırması temel olarak fabrika nesneleri tarafından kullanılır. Yapılandırmaya dayanan herhangi bir nesne, bu fabrika nesnelerinden birinden oluşturulmalıdır. Tüm nesneyi alan bir sınıfı uygulamak için Soyut Fabrika Deseni'ni kullanabilirsiniz ConfigBlock. Bu sınıf, diğer fabrika nesnelerini döndürmek için genel yöntemleri ortaya koyar ve yalnızca ConfigBlockilgili fabrika nesnesiyle ilgili kısmından geçer . Bu şekilde yapılandırma ayarları ConfigBlocknesneden üyelerine ve Fabrika fabrikasından fabrikalara "damlar" .

Dili daha iyi bildiğim için C # kullanacağım, ancak bu C ++ 'a kolayca aktarılabilir olmalıdır.

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);
    }
}

Bundan sonra, ana prosedürünüzde somutlaşan "uygulama" görevi gören bir çeşit sınıfa ihtiyacınız vardır:

// 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
    }
}

Son bir not olarak, bu .NET speak "sağlayıcı nesnesi" olarak bilinir. .NET'teki sağlayıcı nesneleri, yapılandırma verilerini fabrika nesnelerine evlendiriyor gibi görünüyor, bu da aslında burada yapmak istediğiniz şey.

Ayrıca bkz . Yeni Başlayanlar için Sağlayıcı Modeli . Yine, bu .NET geliştirmeye yöneliktir, ancak C # ve C ++ her ikisi de nesne yönelimli diller olduğundan, model çoğunlukla ikisi arasında aktarılabilir olmalıdır.

Bu kalıpla ilgili bir başka iyi okuma: Sağlayıcı Modeli .

Son olarak, bu modelin bir eleştirisi: Sağlayıcı bir model değildir


Sağlayıcı modellerine bağlantılar dışında her şey iyidir. Yansıma c ++ tarafından desteklenmez ve bu çalışmaz.
BЈовић

@ BЈовић: Doğru. Sınıf yansıması mevcut değildir, ancak temel olarak bir yapılandırma ya da yapılandırma dosyalarından okunan bir değere karşı switchbir ififade testine dönüşen manuel bir geçici çözüm oluşturabilirsiniz .
Greg Burghardt

0

İlk soru: bu tür tüm çalışma zamanı yapılandırma verilerini bir sınıfta toplamak iyi bir uygulama mudur?

Evet. Çalışma zamanı sabitlerini ve değerlerini ve bunları okumak için kodu merkezileştirmek daha iyidir.

Bir sınıfın yapıcısına ConfigBlock'uma bir başvuru verilebilir

Bu kötü: inşaatçılarınızın çoğu değerlerin çoğuna ihtiyaç duymayacak. Bunun yerine, inşa edilmesi önemsiz olmayan her şey için arayüzler oluşturun:

eski kod (teklifiniz):

MyGreatClass(ConfigBlock &config);

yeni kod:

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

bir MyGreatClass örneğini başlatın:

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

Burada, sınıfınızın current_config_blockbir örneğidir ConfigBlock(tüm değerlerinizi içeren örnek ) ve MyGreatClasssınıf bir GreatClassDataörnek alır . Diğer bir deyişle, kuruculara yalnızca ihtiyaç duydukları verileri iletin ve ConfigBlockbu verileri oluşturmak için tesislerinizi ekleyin .

Veya sadece benim CodingBlock tanımını içeren bir başlık "CodingBlock.h" içerir:

 extern CodingBlock MyCodingBlock;

Ardından, yalnızca sınıflar .cpp dosyasının ConfigBlock öğelerini içermesi ve kullanması gerekir. .H dosyası bu arabirimi sınıfın kullanıcısına tanıtmaz. Ancak, ConfigBlock arabirimi hala oradadır, ancak .h dosyasından gizlenmiştir. Bu şekilde saklamak iyi mi?

Bu kod, global bir CodingBlock örneğine sahip olacağınızı gösterir. Bunu yapmayın: normalde, uygulamanızın kullandığı giriş noktasında (ana işlev, DllMain, vb.) Global olarak bildirilen bir örneğiniz olmalı ve bunu ihtiyacınız olan her yerde bir argüman olarak iletmelisiniz (ancak yukarıda açıklandığı gibi, geçmemelisiniz) tüm sınıf etrafında, sadece veri etrafında arayüzleri ortaya koymak ve bunları geçmek).

Ayrıca, istemci sınıflarınızı (sizin MyGreatClass) türüne bağlamayın CodingBlock; Bu MyGreatClass, bir dize ve beş tamsayı alırsa, o dize ve tamsayıları geçmekten daha iyi olacağınız anlamına gelir CodingBlock.


Fabrikaları yapılandırmadan ayırmanın iyi bir fikir olduğunu düşünüyorum. Yapılandırma uygulamasının bileşenlerin nasıl somutlaştırılacağını bilmesi tatmin edici değildir, çünkü bu daha önce sadece 1 yönlü bir bağımlılığın olduğu 2 yönlü bir bağımlılıkla sonuçlanır. Kodunuzu genişletirken, özellikle de arayüzlerin gerçekten önemli olduğu paylaşılan kütüphaneleri kullanırken bunun büyük etkileri vardır
Joel Cornett

0

Kısa cevap:

Sen yok Kodunuzdaki modüller / sınıfların her biri için tüm ayarları gerekir. Bunu yaparsanız, nesne yönelimli tasarımınızda bir sorun var. Özellikle birim test durumunda, ihtiyacınız olmayan tüm değişkenlerin ayarlanması ve bu nesnenin iletilmesi, okuma veya bakımda yardımcı olmaz.


Bu şekilde ayrıştırıcı kodunu (ayrıştırma komut satırı ve yapılandırma dosyaları) tek bir merkezi konumda toplayabilirim. Daha sonra, her sınıf ilgili parametrelerini oradan seçebilir. Sizce iyi bir tasarım nedir?
lugge86

Belki de sadece yanlış yazdım - yani, ConfigBlocksınıfınız olabilecek yapılandırma dosyası / ortam değişkenlerinden alınan tüm ayarlarla genel bir soyutlamaya sahip olmalısınız (ve iyi bir uygulama) . Buradaki nokta, bu durumda, sistem durumu bağlamını, sadece belirli, gerekli değerleri vermemektir.
Dawid Pura
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.