Najlepszy sposób na załadowanie ustawień aplikacji

24

Prosty sposób na zachowanie ustawień aplikacji Java jest reprezentowany przez plik tekstowy z rozszerzeniem „.properties” zawierający identyfikator każdego ustawienia powiązanego z określoną wartością (ta wartość może być liczbą, łańcuchem, datą itp.) . C # stosuje podobne podejście, ale plik tekstowy musi mieć nazwę „App.config”. W obu przypadkach w kodzie źródłowym należy zainicjować określoną klasę do odczytu ustawień: ta klasa ma metodę, która zwraca wartość (jako ciąg) powiązaną z określonym identyfikatorem ustawienia.

// Java example
Properties config = new Properties();
config.load(...);
String valueStr = config.getProperty("listening-port");
// ...

// C# example
NameValueCollection setting = ConfigurationManager.AppSettings;
string valueStr = setting["listening-port"];
// ...

W obu przypadkach powinniśmy przeanalizować ciągi znaków załadowane z pliku konfiguracyjnego i przypisać przekonwertowane wartości do powiązanych obiektów tekstowych (podczas tej fazy mogą wystąpić błędy analizy). Po etapie analizy musimy sprawdzić, czy wartości ustawień należą do konkretnej dziedziny ważności: na przykład maksymalny rozmiar kolejki powinien być wartością dodatnią, niektóre wartości mogą być powiązane (przykład: min <max ), i tak dalej.

Załóżmy, że aplikacja powinna załadować ustawienia, jak tylko się uruchomi: innymi słowy, pierwszą operacją wykonywaną przez aplikację jest załadowanie ustawień. Wszelkie nieprawidłowe wartości ustawień muszą zostać automatycznie zastąpione wartościami domyślnymi: jeśli tak się stanie z grupą powiązanych ustawień, wszystkie te ustawienia zostaną ustawione na wartości domyślne.

Najłatwiejszym sposobem na wykonanie tych operacji jest stworzenie metody, która najpierw analizuje wszystkie ustawienia, a następnie sprawdza załadowane wartości i ostatecznie ustawia wartości domyślne. Jednak konserwacja jest trudna, jeśli zastosujesz to podejście: wraz ze wzrostem liczby ustawień podczas opracowywania aplikacji coraz trudniej jest zaktualizować kod.

Aby rozwiązać ten problem, pomyślałem o użyciu wzorca metody szablonu w następujący sposób.

public abstract class Setting
{
    protected abstract bool TryParseValues();

    protected abstract bool CheckValues();

    public abstract void SetDefaultValues();

    /// <summary>
    /// Template Method
    /// </summary>
    public bool TrySetValuesOrDefault()
    {
        if (!TryParseValues() || !CheckValues())
        {
            // parsing error or domain error
            SetDefaultValues();
            return false;
        }
        return true;
    }
}

public class RangeSetting : Setting
{
    private string minStr, maxStr;
    private byte min, max;

    public RangeSetting(string minStr, maxStr)
    {
        this.minStr = minStr;
        this.maxStr = maxStr;
    }

    protected override bool TryParseValues()
    {
        return (byte.TryParse(minStr, out min)
            && byte.TryParse(maxStr, out max));
    }

    protected override bool CheckValues()
    {
        return (0 < min && min < max);
    }

    public override void SetDefaultValues()
    {
        min = 5;
        max = 10;
    }
}

Problem polega na tym, że w ten sposób musimy stworzyć nową klasę dla każdego ustawienia, nawet dla pojedynczej wartości. Czy istnieją inne rozwiązania tego rodzaju problemu?

W podsumowaniu:

  1. Łatwa konserwacja: na przykład dodanie jednego lub więcej parametrów.
  2. Rozszerzalność: pierwsza wersja aplikacji może odczytać pojedynczy plik konfiguracyjny, ale późniejsze wersje mogą dawać możliwość konfiguracji dla wielu użytkowników (administrator konfiguruje konfigurację podstawową, użytkownicy mogą konfigurować tylko niektóre ustawienia itp.).
  3. Projektowanie obiektowe.
enzom83
źródło
Dla tych, którzy proponują użycie pliku .properties, w którym sam plik jest przechowywany podczas programowania, testowania, a następnie produkcji, ponieważ, miejmy nadzieję, nie będzie on w tej samej lokalizacji. Następnie aplikacja będzie musiała zostać ponownie skompilowana z dowolną lokalizacją (deweloper, test lub prod), chyba że można wykryć środowisko w czasie wykonywania, a następnie mieć ustalone lokalizacje w aplikacji.

Odpowiedzi:

8

Zasadniczo zewnętrzny plik konfiguracyjny jest zakodowany jako dokument YAML. Jest to następnie analizowane podczas uruchamiania aplikacji i mapowane na obiekt konfiguracyjny.

Ostateczny wynik jest solidny i przede wszystkim prosty w zarządzaniu.

Gary Rowe
źródło
7

Rozważmy to z dwóch punktów widzenia: API, aby uzyskać wartości konfiguracyjne, i formatu przechowywania. Często są ze sobą powiązane, ale warto rozważyć je oddzielnie.

Interfejs API konfiguracji

Wzorzec metody szablonów jest bardzo ogólny, ale wątpię, czy naprawdę potrzebujesz tej ogólności. Potrzebujesz klasy dla każdego typu wartości konfiguracji. Czy naprawdę masz tyle rodzajów? Sądzę, że można sobie z tym poradzić za pomocą tylko garstki: ciągów, liczb całkowitych, liczb zmiennoprzecinkowych, boolean i wyliczeń. Biorąc to pod uwagę, możesz mieć Configklasę, która ma garść metod:

int getInt(name, default, min, max)
float getFloat(name, default, min, max)
boolean getBoolean(name, default)
String getString(name, default)
<T extends Enum<T>> T getEnum(name, Class<T> enumClass, T default)

(Myślę, że mam generyki w tym ostatnim).)

Zasadniczo każda metoda wie, jak obsługiwać parsowanie wartości ciągu z pliku konfiguracyjnego, obsługiwać błędy i w razie potrzeby zwracać wartość domyślną. Prawdopodobnie wystarcza sprawdzenie zakresu wartości liczbowych. Możesz chcieć mieć przeciążenia, które pomijają wartości zakresu, co byłoby równoważne z podaniem zakresu Integer.MIN_VALUE, Integer.MAX_VALUE. Enum to bezpieczny sposób sprawdzania poprawności łańcucha znaków względem ustalonego zestawu łańcuchów.

Są pewne rzeczy, których to nie obsługuje, takie jak wiele wartości, wartości, które są ze sobą powiązane, dynamiczne wyszukiwanie tabel itp. Możesz napisać dla nich specjalne procedury analizowania i sprawdzania poprawności, ale jeśli będzie to zbyt skomplikowane, zacznę zadawać pytania czy próbujesz zrobić zbyt wiele z plikiem konfiguracyjnym.

Format przechowywania

Pliki właściwości Java wydają się dobrze do przechowywania pojedynczych par klucz-wartość i całkiem dobrze obsługują typy wartości, które opisałem powyżej. Możesz również wziąć pod uwagę inne formaty, takie jak XML lub JSON, ale prawdopodobnie są one nadmierne, chyba że zagnieżdżono lub powtórzono dane. W tym momencie wydaje się, że jest to coś więcej niż plik konfiguracyjny ....

Telastyn wspomniał o serializowanych obiektach. Jest to możliwe, chociaż serializacja ma pewne trudności. Jest binarny, a nie tekstowy, więc trudno jest zobaczyć i edytować wartości. Musisz poradzić sobie ze zgodnością serializacji. Jeśli brakuje wartości w serializowanym wejściu (np. Dodałeś pole do klasy Config i czytasz starą postać zserializowaną), nowe pola są inicjowane na zero / zero. Musisz napisać logikę, aby ustalić, czy wypełnić jakąś inną wartość domyślną. Ale czy zero wskazuje na brak wartości konfiguracji, czy też określono ją jako zero? Teraz musisz debugować tę logikę. Wreszcie (nie jestem pewien, czy to dotyczy) nadal może być konieczne sprawdzenie poprawności wartości w serializowanym strumieniu obiektów. Złośliwy użytkownik może (choć jest niewygodny) modyfikować nieoczekiwanie zmodyfikowany szereg obiektów.

Powiedziałbym, żeby trzymać się właściwości, jeśli to w ogóle możliwe.

Znaki Stuarta
źródło
2
Hej Stuart, miło cię tu widzieć :-). Dodam do odpowiedzi Stuarta, że ​​myślę, że Twój pomysł na tempalte będzie działał w Javie, jeśli użyjesz Generics do silnego pisania, więc możesz mieć również ustawienie <T>.
Martijn Verburg,
@StuartMarks: Cóż, moja pierwsza myśl była po prostu napisać Configklasę i stosowanie metody zaproponowanej przez Ciebie: getInt(), getByte(), getBoolean(), etc .. Kontynuując tę ideę, po raz pierwszy przeczytałem wszystkie wartości i mogłem skojarzyć każdą wartość z flagą (ta flaga jest fałszywa, jeśli wystąpił problem podczas deserializacji, na przykład błędy parsowania). Następnie mogłem rozpocząć fazę sprawdzania poprawności wszystkich załadowanych wartości i ustawić dowolne wartości domyślne.
enzom83,
2
Wolę jakieś podejście JAXB lub YAML, aby uprościć wszystkie szczegóły.
Gary Rowe,
4

Jak to zrobiłem:

Zainicjuj wszystko do wartości domyślnych.

Analizuj plik, przechowując wartości w miarę upływu czasu. Ustawiane miejsca są odpowiedzialne za zapewnienie akceptowalności wartości, złe wartości są ignorowane (a zatem zachowują wartość domyślną).

Loren Pechtel
źródło
Może to być również dobry pomysł: klasa ładująca wartości ustawień może mieć do czynienia tylko z ładowaniem wartości z pliku konfiguracyjnego, to znaczy, że jej obowiązkiem może być tylko ładowanie wartości Z pliku konfiguracyjnego; zamiast tego każdy moduł (który korzysta z niektórych ustawień) będzie odpowiedzialny za sprawdzenie wartości.
enzom83,
2

Czy istnieją inne rozwiązania tego rodzaju problemu?

Jeśli wszystko, czego potrzebujesz, to prosta konfiguracja, lubię tworzyć dla niej zwykłą starą klasę. Inicjuje wartości domyślne i może zostać załadowany z pliku przez aplikację za pośrednictwem wbudowanych klas serializacji. Następnie aplikacja przekazuje je do potrzebnych rzeczy. Żadnych rozmyślań podczas analizowania lub konwersji, bez przekręcania ciągów konfiguracji, bez wyrzucania śmieci. I to sprawia, że konfiguracja sposób łatwiejszy w obsłudze dla w-kodu scenariuszach, w których musi być zapisywane / odczytywane z serwerem lub jako zaprogramowane i sposób łatwiejszy do wykorzystania w testach jednostkowych.

Telastyn
źródło
1
Żadnych rozmyślań podczas analizowania lub konwersji, bez przekręcania ciągów konfiguracji, bez wyrzucania śmieci. Co masz na myśli?
enzom83,
1
Mam na myśli, że: 1. Nie musisz brać wyniku AppConfig (łańcucha) i analizować go w żądany sposób. 2. Nie musisz określać żadnego łańcucha, aby wybrać parametr konfiguracji, który chcesz; jest to jedna z tych rzeczy, które są podatne na błędy ludzkie i trudne do refaktoryzacji, i 3. nie trzeba wtedy wykonywać konwersji innego typu, aby programowo ustawić wartość.
Telastyn
2

Przynajmniej w .NET można dość łatwo tworzyć własne, mocno wpisane obiekty konfiguracyjne - szybki przykład można znaleźć w tym artykule MSDN .

Protip: umieść klasę config w interfejsie i pozwól swojej aplikacji na to porozmawiać. Ułatwia wstrzykiwanie fałszywej konfiguracji do testów lub dla zysku.

Wyatt Barnett
źródło
Przeczytałem artykuł MSDN: jest interesujący, zasadniczo każda podklasa ConfigurationElementklasy może reprezentować grupę wartości, a dla każdej wartości można określić walidator. Ale jeśli na przykład chciałbym przedstawić element konfiguracji, który składa się z czterech prawdopodobieństw, cztery wartości prawdopodobieństwa są skorelowane, ponieważ ich suma musi być równa 1. Jak zweryfikować ten element konfiguracji?
enzom83,
1
Ogólnie twierdzę, że nie jest to coś do sprawdzania poprawności konfiguracji niskiego poziomu - dodałbym metodę AssertConfigrationIsValid do mojej klasy konfiguracyjnej, aby uwzględnić to w kodzie. Jeśli to nie zadziała, myślę, że możesz utworzyć własne weryfikatory konfiguracji, rozszerzając klasę podstawową atrybutu. Mają walidator porównania, więc oczywiście mogą mówić o różnych własnościach.
Wyatt Barnett,