Klasa z elementami, które można modyfikować podczas tworzenia, ale potem są niezmienne

22

Mam algorytm, który tworzy kolekcję obiektów. Obiekty te można modyfikować podczas tworzenia, ponieważ zaczynają od bardzo małej liczby, ale następnie zapełniane są danymi w różnych miejscach algorytmu.

Po zakończeniu algorytmu obiekty nie powinny być nigdy zmieniane - jednak są one zużywane przez inne części oprogramowania.

Czy w tych scenariuszach dobrą praktyką jest posiadanie dwóch wersji klasy, jak opisano poniżej?

  • Zmienny jest zatem tworzony przez algorytm
  • Po zakończeniu algorytmu dane są kopiowane do niezmiennych obiektów, które są zwracane.
Paul Richards
źródło
3
Czy możesz edytować swoje pytanie, aby wyjaśnić, na czym polega Twój problem?
Simon Bergot
Możesz być także zainteresowany tym pytaniem: Jak zamrozić popsicle w .NET (uczynić klasę niezmienną)
R0MANARMY

Odpowiedzi:

46

Być może możesz użyć wzorca konstruktora . Wykorzystuje oddzielny obiekt „konstruktora” w celu zebrania niezbędnych danych, a po zebraniu wszystkich danych tworzy rzeczywisty obiekt. Utworzony obiekt może być niezmienny.

JacquesB
źródło
Twoje rozwiązanie było właściwe dla moich wymagań (nie wszystkie zostały podane). Po zbadaniu wszystkie zwrócone obiekty nie mają tych samych cech. Klasa konstruktora ma wiele zerowalnych pól, a gdy dane zostały zbudowane, wybiera jedną z 3 różnych klas do wygenerowania: są one powiązane ze sobą przez dziedziczenie.
Paul Richards
2
@Paul W takim przypadku, jeśli ta odpowiedź rozwiązała problem, oznacz go jako zaakceptowany.
Riking
24

Prostym sposobem na osiągnięcie tego byłoby posiadanie interfejsu, który pozwala czytać właściwości i wywoływać metody tylko do odczytu oraz klasę, która implementuje ten interfejs, co pozwala również na pisanie tej klasy.

Twoja metoda, która go tworzy, zajmuje się tym pierwszym, a następnie zwraca ten drugi, zapewniając tylko interfejs tylko do odczytu do interakcji. Nie wymagałoby to kopiowania i pozwala łatwo dostroić zachowania, które mają być dostępne dla dzwoniącego, a nie dla twórcy.

Weź ten przykład:

public interface IPerson 
{
    public String FirstName 
    {
        get;
    }

    public String LastName 
    {
        get;
    }
} 

public class PersonImpl : IPerson 
{
    private String firstName, lastName;

    public String FirstName 
    {
        get { return firstName; }
        set { firstName = value; }
    }

    public String LastName 
    {
        get { return lastName; }
        set { lastName = value; }
    }
}

class Factory 
{
    public IPerson MakePerson() 
    {
        PersonImpl person = new PersonImpl();
        person.FirstName = 'Joe';
        person.LastName = 'Schmoe';
        return person;
    }
}

Jedyną wadą tego podejścia jest to, że można go po prostu rzucić na klasę wdrażającą. Gdyby to była kwestia bezpieczeństwa, samo zastosowanie tego podejścia jest niewystarczające. Obejściem tego problemu jest to, że można utworzyć klasę elewacji, aby owinąć klasę zmienną, która po prostu przedstawia interfejs, z którym program wywołujący współpracuje i nie może mieć dostępu do obiektu wewnętrznego.

W ten sposób nawet casting nie pomoże. Oba mogą wywodzić się z tego samego interfejsu tylko do odczytu, ale rzutowanie zwróconego obiektu da ci tylko klasę Fasada, która jest niezmienna, ponieważ nie zmienia stanu podstawowego opakowanej klasy zmiennej.

Warto wspomnieć, że nie jest to zgodne z typowym trendem, w którym niezmienny obiekt jest budowany raz na zawsze za pośrednictwem jego konstruktora. Zrozumiałe, że możesz mieć do czynienia z wieloma parametrami, ale powinieneś zadać sobie pytanie, czy wszystkie te parametry muszą zostać zdefiniowane z góry, czy też niektóre mogą zostać wprowadzone później. W takim przypadku należy zastosować prosty konstruktor z tylko wymaganymi parametrami. Innymi słowy, nie używaj tego wzorca, jeśli ukrywa on inny problem w twoim programie.

Neil
źródło
1
Bezpieczeństwo nie jest lepsze, zwracając obiekt „tylko do odczytu”, ponieważ kod, który go otrzymuje, może nadal modyfikować obiekt za pomocą odbicia. Nawet ciąg znaków można modyfikować (nie kopiować, modyfikować na miejscu) za pomocą odbicia.
MTilsted
Problem bezpieczeństwa można starannie rozwiązać z prywatną klasą
Esben Skov Pedersen
@Esben: Nadal musisz walczyć z MS07-052: Wykonanie kodu powoduje wykonanie kodu . Twój kod działa w tym samym kontekście bezpieczeństwa co jego kod, więc mogą po prostu dołączyć debugger i zrobić, co tylko zechcą.
Kevin
Kevin1 można to powiedzieć o wszystkich enkapsulacjach. Nie próbuję chronić przed odbiciem.
Esben Skov Pedersen
1
Problem z używaniem słowa „bezpieczeństwo” polega na tym, że natychmiast ktoś zakłada, że ​​to, co uważam za bezpieczniejszą opcję, jest równoważne maksymalnemu bezpieczeństwu i najlepszej praktyce. Nigdy też nie powiedziałem. Jeśli oddajesz bibliotekę do użytku, o ile nie jest zaciemniana (a czasem nawet zaciemniona), możesz zapomnieć o zagwarantowaniu bezpieczeństwa. Myślę jednak, że wszyscy możemy się zgodzić co do faktu, że jeśli manipulujesz zwróconym obiektem fasady za pomocą odbicia w celu odzyskania zawartego w nim obiektu wewnętrznego, nie używasz biblioteki tak, jak powinna.
Neil
8

Możesz użyć wzorca Konstruktora, jak mówi @JacquesB, lub alternatywnie zastanów się, dlaczego tak naprawdę twoje obiekty muszą być modyfikowalne podczas tworzenia?

Innymi słowy, dlaczego proces ich tworzenia musi być rozłożony w czasie, w przeciwieństwie do przekazywania wszystkich wymaganych wartości do konstruktora i tworzenia instancji za jednym razem?

Ponieważ Konstruktor może być dobrym rozwiązaniem niewłaściwego problemu.

Jeśli problem polega na tym, że otrzymujesz konstruktor o długości około 10 parametrów i chcesz go złagodzić, budując obiekt stopniowo, może to oznaczać, że projekt jest pomieszany, a te 10 wartości powinno mieć wartość „ spakowane "/ pogrupowane w kilka obiektów ... lub główny obiekt podzielony na kilka mniejszych ...

W takim przypadku - trzymaj się niezmienności przez cały czas, po prostu popraw projekt.

Konrad Morawski
źródło
Utworzenie tego wszystkiego byłoby trudne, ponieważ kod wykonuje wiele zapytań do bazy danych w celu uzyskania danych; porównuje dane w różnych tabelach i różnych bazach danych.
Paul Richards