Jak rozszerzyc funkcjonalnośc singleton-a ?

0

Chciałbym używać wszędzie w kodzie Config jako singleton i pewnego rodzaju standard , jednak chciałbym moc w main() podmienić aby singleton tworzył obiekt klasy która jest potomkiem Config np. aby dane zapisywały się w SqLite albo w jakimś innym formacie

Czy można to zrobić lepiej niż poniższym przykładzie:

#include <clocale>
#include <iostream>
#include <string>
#include <map>
#include <functional>

class Config
{
private:
    std::map<std::string, int> data;    
    static inline Config* _instance{nullptr};
    static inline std::function<Config*()> createInstance{nullptr};
public:
    friend class ConfigSqLite;
    Config(Config const&) = delete;
    Config& operator=(Config const&) = delete;    

    static Config* instance()
    {
        if(!_instance)
        {
            if(createInstance)
            {
               _instance = createInstance();  
            }
            else
            {
             _instance = new Config();
            }
        }
        return _instance;
    }
    

    virtual int saveInt(const std::string &key, int value)
    {
        std::cout << "Config::saveInt\n";
        data[key] = value;
        return value;
    }

    virtual int readInt(const std::string &key)
    {
        std::cout << "Config::readInt\n";
        return data[key];
    }

    static void registerCreateInstance(std::function<Config*()> function)
    {
        createInstance = function;
    }

private:
    Config() {}    
};


class ConfigSqLite: public Config
{
    int saveInt(const std::string &key, int value) override
    {
        // TODO 
        std::cout << "ConfigSqLite::saveInt\n";
        return value;
    }

    int readInt(const std::string &key) override
    {
        // TODO
        std::cout << "ConfigSqLite::readInt\n";
        return 0;
    }
public:
    ConfigSqLite(std::string databaseName):Config()
    {
        // TODO 
    }
};

int main(int, char**){
    std::string CONFIG_WIDTH{"width"};

    // tworzac sigleton Config , naprawde utworzy sie obiekt ConfigSqLite
    Config::registerCreateInstance([](){
        return new ConfigSqLite("database.sqlite");
    });

    Config::instance()->saveInt(CONFIG_WIDTH, 100);
    std::cout <<  Config::instance()->readInt(CONFIG_WIDTH);
}
3
Marius.Maximus napisał(a):

Chciałbym używać wszędzie w kodzie Config jako singleton i pewnego rodzaju standard , jednak chciałbym moc w main() podmienić aby singleton tworzył obiekt klasy która jest potomkiem Config np. aby dane zapisywały się w SqLite albo w jakimś innym formacie

Słaby pomysł. Nie polecam.

6

Możesz po prostu stworzyć klasę config i użyć jej raz. Nikt tego nie sprawdza. Zasada DRY Cię chroni, nie potrzebujesz komplikować kodu singletonem.

6

Singleton to samo zło bo, wszyscy go źle używają.
Singleton jest wzorcem tworzenie obiektu, a nie jego używania. Czyli instance() powinno się używać przy tylko przy konstruowaniu, a nie w każdym miejscu, gdzie trzeba użyć singletona.

Używanie instance() wszędzie, gdzie potrzebny jest dostęp do obiektu powoduje to, że kod staje się nietestowany testami jednostkowymi. Przykład: jeśli testu używają różnych konfiguracji to testy te wpływają na siebie.

0

@MarekR22: ja używam instance() w sposób który Ty uważasz za błedny, to w takim razie jak zrobić to poprawnie i bez Singleton ? Obiekty potrzebują zapisać/wczytac swoja konfigurację taka globalna zmienna ukryta za konstrukcją Singleton na tą chwilę wydaje mi się dosć wygodna, jeszcze nie dotarłem do problemów które to rozwiązanie niesie ale pewnie niedługo i dojdę tam

4
Marius.Maximus napisał(a):

@MarekR22: ja używam instance() w sposób który Ty uważasz za błedny, to w takim razie jak zrobić to poprawnie i bez Singleton ? Obiekty potrzebują zapisać/wczytac swoja konfigurację taka globalna zmienna ukryta za konstrukcją Singleton na tą chwilę wydaje mi się dosć wygodna, jeszcze nie dotarłem do problemów które to rozwiązanie niesie ale pewnie niedługo i dojdę tam

No najlepiej przekazać obiekt jako parametr.

Taki singleton jest koncepcyjnie tożsamy ze zmienną globalną.

4

Robisz burdel i tyle. std::map utwierdza w przekonaniu, że chcesz, żeby każdy element w twoim kodzie mógł przeczytać każdy klucz a to bez sensu.

Do tego nie wprowadzaj dynamiczności tam gdzie nie jest to potrzebne. Czytanie z SQLite możesz zrobić raz na start aplikacji. Jak chcesz więcej dynamiczności to możesz przekazywać config jako std::function<Config>

0

@slsy: to zamierzona funkcjonalność, znając key możesz wszystko 😉
jak możesz to zaproponuje rozwianie w postaci szkieletu kodu

2
Marius.Maximus napisał(a):

Obiekty potrzebują zapisać/wczytac swoja konfigurację taka globalna zmienna ukryta za konstrukcją Singleton na tą chwilę wydaje mi się dosć wygodna,

No wlasnie - wydaje ci sie. Oczywistym jest ze jesli masz 100 klas w ktorych jest potrzebny dostep do ustawien aplikacji to duzo prosciej jest do tych 100 klas przekazac jakis obiekt niz uzyc metody statycznej.

jeszcze nie dotarłem do problemów które to rozwiązanie niesie ale pewnie niedługo i dojdę tam

Watpie zebys tam doszedl. Owszem, jest to jak najbardziej mozliwe ze kiedys okaze sie ze jest wiecej niz 1 config, ale typowo jest dokladnie 1. Tylko ty wiesz czy masz, lub za 48 lat zamierzasz dodac do aplikacji wiecej niz 1 globalny config.

A zapominajac o aspektach religijnych typy "nie uzywaj singletona bo nie uzywaj singletona"(*) to masz w kodzie realny problem - kompletnie ignorujesz watki. Typowo bedziesz mial w aplikacji wiecej niz 1 watek i jesli w kilku bedziesz uzywal tego co wkleiles to masz problem.

Jest tez bardzo proste rozwiazanie problemu singletona - wystarczy ze nie bedziesz go nazywal singletonem ale service, albo jeszcze lepiej microservice (oczywiscie nie zapominajac wystawic dostep do ustawien przez siec). Niestety w C++ nie dasz rady za pomoca refleksji jakims XML 3 kilometry od zrodel zmienic zachowania aplikacji. Ale pewnie i tak magicznie z antywzorca zrobi sie podstawowe narzedzie nowoczesnego programisty.

(*)

  1. Argument "kod staje się nietestowany testami jednostkowymi" jest nieprawdziwy. Problem bedzie tylko jesli testy sa wykonywane w wielu watkach. Nie ma takiego obowiazku. A np. w Catch2 nie jest to bezposrednio osiagalne.
  2. Argument "singleton jest koncepcyjnie tożsamy ze zmienną globalną" jest oczywiscie prawdziwy, tyle ze sprowadza przykazanie "nie uzywaj singletona bo nie uzywaj singletona" do "nie uzywaj singletona bo nie uzywaj zmiennych globalnych". W takiej sytuacji nalezaloby tez rozwazyc korzystanie z sqlite, bo trzymanie danych w bazie tez jest koncepcyjnie tozsame ze zmienna globalna. A nawet uber globalna bo stan zostaje bo zamknieciu aplikacji.
2
Marius.Maximus napisał(a):

@slsy: to zamierzona funkcjonalność, znając key możesz wszystko 😉
jak możesz to zaproponuje rozwianie w postaci szkieletu kodu

robisz config per miejsce użycia. Przykładowo:


struct ServerConfig {
  int port;
  int threads_num;
};

struct NotifierConfig {
  Duration notify_duration;
  string url;
};

Configi czytasz sobie w mainie np. tak:

auto readServerConfigFromEnv {
  return ServerConfig{
    .port = to_short(std::getenv("PORT)),
    .threads_num = to_int(std::getenv("THREDS_NUM)),
  }
}

Taki obiekt po prostu przerzucasz tam gdzie trzeba. Możesz też sobie zrobić giga config np. tak

struct Config {
  ServerConfig server;
  NotifierConfig notifier;
};

dzięki czemu czytanie samego configu możesz zrobić w jednym miejscu.

Zalety:

  • czytanie configu jest całkowicie odseparowane od samego configu
  • masz sprawdzanie typów, możesz obsłużyć błąd np. wyjątkiem, gdy czegoś nie ma
  • masz modularyzację. Same definicje configów możesz umieścić tam, gdzie jest używany przez co twój kod ładnie się skaluje
1

Ja bym zrobił tak, po pierwsze rozdzielił bibliotekę od configa od faktycznego użycia w aplikacji.

Czysty config lib:

  • Nie ma żadnego singletona.
  • Operuje na prostych strukturach danych typu mapa np. wystawia funkcje getInt("db.maxConnections")
  • Jego zadaniem jest wczytanie konfiguracji do pamięci i jej późniejsze zwolnienie
  • Opiera się na interface + providers (np. FileConfiguration, InMemConfiguration, HttpConfiguration, EnvConfiguration...)

Konkretna klasa konfiguracji:

  • Publiczna metoda statyczna DatabaseConfiguration.fromConfiguration(configFile)
  • Odpowiada za wyciąganie stringów z configa i wbijanie w pola
  • Ma ładne api: int maxConnections()

Singleton, klasa ApplicationConfiguration korzysta z 2 poprzenich:

  • Laduje config do pamięci za pomocą liba
  • Tworzy specyficzne klasy konfiguracji i trzyma je jako pola
  • Jest singletonem

Teraz jeśli chcesz dodać nowe pole do DB conf'a zmienia się tylko jedna klasa.
Jeżeli konf będzie zaczytywany przez HTTP a nie z pliku zmienia się tylko jedna klasa.
Wszystko łatwo przetestować o ile zapewnisz również implementacje in-memory.

Generalnie tutaj widać jak bardzo C++ nie nadaje się do takich rzeczy w Javce pojechało by się refleksją i było by git.

1

Moim zdaniem pomysł od @0xmarcin Marcin jest słaby. Tzn idea jest nawet słuszna, ale wykonanie chyba mniej. Ja bym dążył do tego samego, ale od drugiej strony.

Należy zacząć od interfejsów (bez żadnych plików, kluczy, ładowania, ststiców). To może być nawet pojedyncza klasa value-object.

Jednym z problemów w naszej branży jest wsadzanie niepotrzebnych rzeczy przez przekonanie że "tak ma być". Np słyszymy słowo "config" i od razu robimy zagnieżdżane klucze i ładowanie z pliku, mimo że nasz use-case jescze tego nie wymaga.

PS: W odpowiedzi na prośbę o wyjaśnienie:

Czysty config lib:

  • Nie ma żadnego singletona. to jest spoko
  • Operuje na prostych strukturach danych typu mapa pomysł spoko np. wystawia funkcje getInt("db.maxConnections") zgadywanie interfejsu na tym etapie komplikuje design niepotrzebnie
  • Jego zadaniem jest wczytanie konfiguracji do pamięci i jej późniejsze zwolnienie zgadywanie co ma robić implementacja interfejsu na początku
  • Opiera się na interface + providers (np. FileConfiguration, InMemConfiguration, HttpConfiguration, EnvConfiguration...) znowu zgadywanie implementacji z jakimiś providerami

Konkretna klasa konfiguracji:

  • Publiczna metoda statyczna DatabaseConfiguration.fromConfiguration(configFile) już na samym początku dodawanie configa z pliku,mimo że nie ma usecase'u na to
  • Odpowiada za wyciąganie stringów z configa i wbijanie w pola znowu zgadywanie implementacji bez interfejsu
  • Ma ładne api: int maxConnections() To jest jedna z dobrych rzeczy - i od tego należy zacząć!

Singleton, klasa ApplicationConfiguration korzysta z 2 poprzenich:

  • Laduje config do pamięci za pomocą liba zgadywanie że config trzeba będzie ładować
  • Tworzy specyficzne klasy konfiguracji i trzyma je jako pola zgadywanie ze będą różne klasy konfiguracji
  • Jest singletonem sugerowanie finalnego designu, kiedy jeszcze basic usecase nie jest ogarnięty

tl;dr;

to co należałoby zrobić to przedstawić prosty interfejs configa, np z int maxConnections(), i przekazać to jako obiekt w miejsca które z tego korzystają, i to rozwiąże 90% przypadków. Wszystkie pozostałe rzeczy, jak pliki ,ładowanie, providery, klucze, dostęp po mapie to jest niepotrzebna komplikacja.

0

Pattern singleton może Ci się przydać do rzeczy które faktycznie są pojedyncze dla danego programu i raczej mało jest takich rzeczy właśnie ze względu na to co pisali wyżej: utrudnione możliwości testowania. Zwłaszcza jeśli chcesz odpalać testy w wielu wątkach w jednym procesie - to wtedy właściwie nie jesteś w stanie tego kontrolować.

Dlatego warto jak najbardziej ograniczać użycie tego patternu. Jest on zwłaszcza niekorzystny z tego powodu, że moduł, który ma dostęp do danego pliku .h może go zaincludeować, wywołać instance() i zrobić cokolwiek co jest publiczne w danej klasie (a z hackami nawet to co jest prywatne - public_cast).

Jeśli nie ma się doświadczenia w programowaniu, warto zobaczyć jak to robią inni, najlepiej w dużych aplikacjach.

Weźmy na przykład kod chromium. Rzeczą pojedynczą jest np. proces w którym aktualnie się znajdujemy i dane o tym procesie:
https://github.com/chromium/chromium/blob/main/chrome/browser/chrome_process_singleton.h#L56

W tej klasie jest:

ProcessSingleton process_singleton_;

czyli: https://github.com/chromium/chromium/blob/main/chrome/browser/process_singleton.h

Da się z tego wyciągnąć jakieś podstawowe dane o aktualnym procesie systemowym w którym się znajdujemy.

Pobierz sobie kod chromium lokalnie i wyszukaj w całym katalogu:
base/memory/singleton.h
Ten plik jest zawierany w 397 innych plikach. Wydaje się to całkiem spora liczba, ale kod chromium jest ogromny. Jak poprzeglądasz gdzie singleton jest faktycznie używany w takich aplikacjach to już mniej więcej będziesz czuł gdzie singleton może się przydać.

Problem, który masz z konfiguracją, w chromium jest rozwiązany przez klasę PrefService.
Obiekt tej klasy najczęściej jest wyłuskiwany z innych obiektów. Na przykład tutaj:
https://github.com/chromium/chromium/blob/main/chrome/browser/chrome_content_browser_client.cc#L1380

bool IsSubAppsPermissionGrantedByAdmins(content::WebContents* contents) {
  if (!contents) {
    return false;
  }

  Profile* profile = Profile::FromBrowserContext(contents->GetBrowserContext());
  if (!profile) {
    return false;
  }

  PrefService* prefs = profile->GetPrefs();
  if (!prefs) {
    return false;
  }

  return policy::IsOriginInAllowlist(
      contents->GetURL(), prefs,
      prefs::kSubAppsAPIsAllowedWithoutGestureAndAuthorizationForOrigins);
}

Ale bywa też, że jest wyłuskiwany z globalnego obiektu procesu:

void ChromeContentBrowserClient::AppendExtraCommandLineSwitches(
    base::CommandLine* command_line,
    int child_process_id) {
    //.......
      // The IntensiveWakeUpThrottling feature is typically managed via a
      // base::Feature, but it has a managed policy override. The override is
      // communicated to blink via a custom command-line flag. See
      // PageSchedulerImpl for the other half of related logic.
      PrefService* local_state = g_browser_process->local_state();
      const PrefService::Preference* pref = local_state->FindPreference(
          policy::policy_prefs::kIntensiveWakeUpThrottlingEnabled);
      if (pref && pref->IsManaged()) {
        command_line->AppendSwitchASCII(
            blink::switches::kIntensiveWakeUpThrottlingPolicy,
            pref->GetValue()->GetBool()
                ? blink::switches::kIntensiveWakeUpThrottlingPolicy_ForceEnable
                : blink::switches::
                      kIntensiveWakeUpThrottlingPolicy_ForceDisable);
      }

Ale raczej rzadko tak się dzieje. Zasadniczo, to wiele zależy od miejsca w którym jest dany kod. Jeśli jest możliwość pozyskania tego obiektu z kontekstu, to się to robi. Jeśli nie - z globalnego stanu.

https://github.com/chromium/chromium/blob/main/chrome/common/pref_names.h#L2554

// Holds URL patterns that specify origins that will be allowed to call
// `subApps.{add|remove|list}())` without prior user gesture and that will skip
// the user dialog authorization.
inline constexpr char
    kSubAppsAPIsAllowedWithoutGestureAndAuthorizationForOrigins[] =
        "profile.isolated_web_app.sub_apps_allowed_without_user_gesture_and_"
        "authorization";
0

Nowa koncepcja Config-a bez singleton , ale za to można zapisać dowolny typ danych za pomocą std::any , wiec kod zadziała na c++17 ,
można też zrobić thread safe

przykład użycia:
[1] bez domyślnej wartości
[2] z domyślna wartością
[3] używam alternatywnego obiektu do przechowania wartości
[4] zapis/odczyt std::vector
[5] ConfigInt jako pole obiektu

{
    std::cout << "[1]\n";
    ConfigInt maxConnections{"db.maxConnections"};
    std::cout << maxConnections.get() <<"\n";
    maxConnections.set(7);
    std::cout << maxConnections.get() <<"\n";
}

{
    std::cout << "[2]\n";
    ConfigInt maxConnections{"db.maxConnections",3};
    std::cout << maxConnections.get() <<"\n";
    maxConnections.set(7);
    std::cout << maxConnections.get() <<"\n";
}

{
    std::cout << "[3]\n";
    ConfigStorageMap configStorageMap;
    ConfigInt maxConnections{"db.maxConnections",3, &configStorageMap};
    std::cout << maxConnections.get() <<"\n";
    maxConnections.set(7);
    std::cout << maxConnections.get() <<"\n";
}

{
    using ConfigVector = Config<std::vector<int>>;
    std::cout << "[4]\n";
    std::vector v{1,2,4,5};
    ConfigVector configVector("db.ports");
    auto v2 =  configVector.get();
    std::cout << v2.size()<<"\n";
    configVector.set(v);
    auto v3 =  configVector.get();
    std::cout << v3.size()<<"\n";
}

{
    class A{
        ConfigInt pos_X{"mainWindow.x",100};    
        ConfigInt pos_Y{"mainWindow.y",200};    
    public:    
        A()
        {
            std::cout << "pos_X:" << pos_X.get() << "\n";  
            std::cout << "pos_Y:" << pos_Y.get() << "\n";  
        }
    };
    std::cout << "[5]\n";
    A a;
}

I kod

#include <string>
#include <any>
#include <map>


using ConfigAnyType =  std::any; 

class ConfigStorage
{
public:
    virtual bool find(const std::string &key, ConfigAnyType& result) { return false; }
    virtual void write(const std::string &key, const ConfigAnyType& value) {}        
};

class ConfigStorageMap : public ConfigStorage
{
    std::map<std::string, ConfigAnyType> data;

public:
    virtual bool find(const std::string &key, ConfigAnyType& result) 
    {
        auto it = data.find(key);
        if(it != data.end())
        {
            result = it->second;    
            return true;
        }

        return false;
    }

    virtual void write(const std::string &key, const ConfigAnyType& value)
    {
        data[key] =  value;   
    }        
} ;

class ConfigDefault
{
private:
    // static inline ConfigStorage _configStorageDefault;    
    static inline ConfigStorageMap _configStorageDefault;    
public:
    static inline ConfigStorage* configStorageDefault{&_configStorageDefault};
};


template <class T>
class Config
{
private:
    std::string key;  
    T defaultValue{}; 
    ConfigStorage* configStorage; 

public:
    Config(const std::string &key, T defaultValue = T{}, ConfigStorage* configStorage = ConfigDefault::configStorageDefault) : key(key), defaultValue(defaultValue), configStorage(configStorage) {};
    T get()
    {
        ConfigAnyType value;
        if(configStorage && configStorage->find(key, value)){
            return std::any_cast<T>(value);
        }
        else {
            return defaultValue; 
        }
    }   

    void set(const T& value)
    {
        if(configStorage){
           configStorage->write(key,value);
        }
    }  
};

using ConfigInt = Config<int>;
using ConfigDouble = Config<double>;

https://godbolt.org/z/xGTqvT9Po

0 użytkowników online, w tym zalogowanych: 0, gości: 0