Hermetyzacja zagregowanych obiektów

0

Zastanawiam się w jaki sposób rozwiązać problem hermetyzacji danych w obiektach agregujących inne obiekty.

public class CustomModel : ICloneable
{
	private int _age;
	public int Age
	{
		get { return this._age;}
		set { this._age=value;}
	}

	public override object Clone()
	{
		return this.MemberwiseClone();
	}
}

public class SafeClass
{
	private CustomModel _model;
	
	public SafeClass(CustomModel model)
	{
		// wersja 1
		this._model = model;
		
		// wersja 2 (klonowanie wewnatrz konstruktora)
		this._model = (CustomModel)model.Clone();
	}
}

Jeżeli napiszemy kod klasy SafeClass w wersji 1, a nastepnie utworzymy jej instację, np:

CustomModel cm = new CustomModel();
cm.Age = 20;
SafeClass sc = new SafeClass(cm);
cm.Age = 40;    //==> możliwa manipulacja zawartością obiektu sc ! (brak hermetyzacji)

mozliwe będzie modyfikowanie prywatnej zmiennej klasy SafeClass z zewnątrz tej klasy.
Problem można rozwiązać, np tak:

SafeClas sc = new SafeClass((CustomModel)cm.Clone());

Jednak należałoby założyć, że użytkownik naszej klasy w natłoku innych pilnych spraw, zapomni sklonować obiekt przed przekazaniem go do konstruktora. Wówczas nasza klasa SafeClass będzie narażona na modyfikację z zewnątrz. Jeżeli konstruktor klasy SafeClass będzie sam dokonywać klonowania (wersja 2), nasza klasa będzie bezpieczna. W takim jednak przypadku, jeżeli użtykownik naszej klasy jest świadomy zagrożenia związanego z przekazywaniem referencji do innego obiektu, a nie zna kodu naszej klasy (bo nie musi) i świadomie dokona klonowania (jak powyżej), wówczas wykonają się dwa klonowania (w tym jedno zbędne).

Czy ktoś kiedyś zastanawiał się nad takim problemem. Jakieś wasze przemyślenia w temacie ? Jak najrozsądniej podejść do sprawy ?
Pozdrawiam

0

Jest jeszcze trzecia opcja , to kasa SAFE prosi o obiekt .

0

da się zrobić tak:

var sc = new SafeClass(new CustomModel(){Age = 20});

wtedy nie ma obiektu po za klasą.

ale generalnie to chyba w żadnym języku nie da się utrzymać takiej hermetyzacji jesli masz obiekt stworzony na zewnątrz klasy i potem wrzucenie go do prywatnego pola w innej przez referencje. jedynie typy wartosciowe da sie tak przekazać bo lecą przez wartość wiec tworzy się taka nowa instancja.
bo w c# wszystko jest przekazywane przez wartość (jeślichodzi o metody i konstruktory) i przy przekazywaniu obiektów jest kopiowana referencja i jak by porównać takie obiekty metodą ReferenceEqual() to byłyby różne (będą 2 referencje na 1 obiekt).

no pozostaje jeszcze ewentualne klonowanie jak wcześniej pisałeś

generalnie tak się raczej nie pisze klas, jak już to przekazuje się parametry klasy wewnętrznej i tworzy się pole w konstruktorze, albo nawet lepiej Od razu

public class SafeClass
{
        private CustomModel _model = new CustomModel();
        
        public SafeClass(int age)
        {
                _model.Age = age;
        }
}
var sc = new SafeClass(20);

a jak już nie da się inaczej to raczej zakłada się że referencja na obiekt klasy agregowanej zniknie prędzej niż klasa która to agreguje :]

0

Dodam tylko, że nawet twoje klonowanie nie zabezpieczy tego, co chcesz osiągnąć. Gdyby CustomModel zawierał obiekt - byłby to dalej wspólny obiekt - pomimo Memberwise.Clone(). Dlatego, że jest to klonowanie płytkie - kopiuje tylko typy proste a nie typy referencyjne.

W każdym razie - twój cel jest oznaką błędnie zaprojektowanej aplikacji .. - to znaczy, skoro potrzebujesz takie rzeczy, coś w kodzie jest nie tak ;-)

0

Klonowanie rozwiąże problem pod warunkiem, że jest poprawnie zaimplementowane. Masz rację, że w przypadku bardziej złożonego obiektu MemberwiseClone() nie wystarczy, ale to zupelnie inny problem (poprawna implementacja metody Clone()). W przykładzie z CustomModel, MemberwiseClone() w zupełności wystarcza.

I nie do końca się zgodzę z błędnym projektem. Klasa jest podstawową jednostką każdego projektu tworzonego obiektowo. I każda klasa powinna być za siebie odpowiedzialna i hermetyczna.
Np jeżeli piszesz dla kogoś bibliotekę zawierającą gotowe komponenty. Taki komponent powinien prawidłowo funkcjonować niezależnie od poziomu biegłości programisty który będzie z niego korzystał. Jeżeli korzysta nieprawidłowo i jesteś w stanie to wykryć w kodzie pisanego komponentu mozesz wyrzucić wyjątek. Jednak w takim wypadku jak przedstawiłem nie jesteś w stanie tego stwierdzić i narażasz SWÓJ komponent na niewłaściwe działanie.
Wyobraź sobie np taką sytuację:
Piszesz dwa komponenty (o takim samym interfejsie) do wyszukiwania dokumentów w dwóch różnych lokalizacjach. Zamiast CustomModel niech będzie np FilterArgs zawierający kryteria wyszukiwania.
Użytkownik najpierw tworzy obiekt args klasy FilterArgs i przekazuje go do Komponentu_1. Następnie jedynie modyfikuje dane w args i przekazuje go do Komponentu_2. I jeśli nie wykonasz klonowania to Komponent_1 wyszuka nie to o co pierwotnie prosił uzytkownik.

Pomysł Ermesa rowiązałby problem. Może być jednak uciążliwy w przypadku znacznej liczby parametrów jakie musiałby przyjmować konstruktor. Poza tym w przypadku zmiany liczby właściwości modelu CustomModel musielibyśmy za każdym razem modyfikować konstruktor SafeClass...

0

Z tego co ja widzę to Deti jedynie zauważył że NIE DA SIĘ sklonować obiektu ponieważ nigdy nie wiesz czy programista zrobił to dobrze.

A wszystko o czym piszesz wynika z prostej zasady której twój projekt nie przestrzega i przez to jest ZŁY:

  • Klasa może posiadać jako składowe tylko interfejsy obce lub,i obiekty stworzone przez nią samą.

Interfejsy są jak nazwy a obiekty jak ciało . Możesz znać osobę o imieniu Patryk ale nie wolno ci posiąść go fizycznie ;). Nazwa nie służy do używania obiektu a jedynie do wołania osoby. Ty natomiast próbujesz schować Patryka w szafie i boisz się że ci zajrzy do kieszeni.

0

I nie do końca się zgodzę z błędnym projektem. Klasa jest podstawową jednostką każdego projektu tworzonego obiektowo. I każda klasa powinna być za siebie odpowiedzialna i hermetyczna.
Np jeżeli piszesz dla kogoś bibliotekę zawierającą gotowe komponenty. Taki komponent powinien prawidłowo funkcjonować niezależnie od poziomu biegłości programisty który będzie z niego korzystał. Jeżeli korzysta nieprawidłowo i jesteś w stanie to wykryć w kodzie pisanego komponentu mozesz wyrzucić wyjątek. Jednak w takim wypadku jak przedstawiłem nie jesteś w stanie tego stwierdzić i narażasz SWÓJ komponent na niewłaściwe działanie.
Wyobraź sobie np taką sytuację:
Piszesz dwa komponenty (o takim samym interfejsie) do wyszukiwania dokumentów w dwóch różnych lokalizacjach. Zamiast CustomModel niech będzie np FilterArgs zawierający kryteria wyszukiwania.
Użytkownik najpierw tworzy obiekt args klasy FilterArgs i przekazuje go do Komponentu_1. Następnie jedynie modyfikuje dane w args i przekazuje go do Komponentu_2. I jeśli nie wykonasz klonowania to Komponent_1 wyszuka nie to o co pierwotnie prosił uzytkownik.

Tak jak napisał poprzednik - podtrzymuje zdanie - architektura jest źle zaprojektowana.

Nie sądze aby .NET w całej swej rozciągłości gdzielkolwiek musiał robić klonowanie obiektów, żeby to wszystko działało.

W twoim przypadku rozwiązaniem byłoby albo:

  1. Przenieść FilterArgs z argumentów konstruktora do argumentów metody końcowej
  2. Zrobić DeepClone w konstruktorze (bleh - ohyda)
  3. Zmienić DeepClone z klasy na strukturę.

W każdym razie - obecna implementacja jest zła..

0
XRay napisał(a)

Z tego co ja widzę to Deti jedynie zauważył że NIE DA SIĘ sklonować obiektu ponieważ nigdy nie wiesz czy programista zrobił to dobrze.

Zakładam, że ja implementuję CustomModel, a klient go używa. Wtedy ja odpowiadam za poprawną implementację metody Clone().

XRay napisał(a)
  • Klasa może posiadać jako składowe tylko interfejsy obce lub,i obiekty stworzone przez nią samą.

Jeżeli chodzi o składowe tworzone przez samą klasę to OK. Ale jaka jest różnica czy klasa zawiera referencję do obiektu konkretnej klasy czy obiektu implementującego określony interfejs ? Przecież jeżeli do takiego obiektu będzie istniała jakakolwiek inna referencja poza naszą klasą to w dalszym ciągu będzie możliwa jego modyfikacja spoza naszej klasy...
Mógłbyś podać przykład ?

0

Moze taka male wtracenie:

/// <summary> Public Constructor </summary>
/// <param name="model">Reference to existing object of CustomModel, whose values will be copied to new instance</param>
public SafeClass(CustomModel model)
{...

I to by chyba rozwialo wszystkie dylematy tego programisty. Wiec tak za bardzo nie rozumiem problemu :) Programista uzytkujacy Twoja klase ma sie nie zastanawiac za bardzo (nie mowiac o wykonywaniu jakiejkolwiek dodatkowej akcji), a co najwyzej przeczytac dokumentacje (chociazby z intelisensa), a osobiscie nic zlego w klonowaniu w konstruktorze nie widze - o ile wykona sie je poprawnie oczywiscie.

0
Artist napisał(a)

Zakładam, że ja implementuję CustomModel, a klient go używa. Wtedy ja odpowiadam za poprawną implementację metody Clone().

Ale to niewiele zmienia , zawsze zakładasz że program pisze wiele osób. To tak jak z komentarzami , oczywiście możesz je pominąć w końcu sam piszesz aplikacje ale to nie fachowe. Rób jak chcesz to Twoja decyzja - sam ustalasz czy dasz radę.

Artist napisał(a)

Jeżeli chodzi o składowe tworzone przez samą klasę to OK. Ale jaka jest różnica czy klasa zawiera referencję do obiektu konkretnej klasy czy obiektu implementującego określony interfejs ? Przecież jeżeli do takiego obiektu będzie istniała jakakolwiek inna referencja poza naszą klasą to w dalszym ciągu będzie możliwa jego modyfikacja spoza naszej klasy...
Mógłbyś podać przykład ?

Jest różnica pomiędzy posiadaniem danych i ich hermetyzacją a posiadaniem funkcji

  • FUNKCJI SIĘ NIE HERMETYZUJE!!
    Oczywiście zamiast interfejsu możesz użyć klasy z metodami nie ma to większego znaczenia.

Odróżnij sweter , od zasilacza - sweter nie pełni funkcji. Jak działa zasilacz cie nie interesuje on ma po to obudowę abyś nie wiedział jak działa.

Oczywiście nie istnieje taki przedmiot który nie pełni funkcji np. litera to zbiór bitów , jednak tobie nie wolno samemu interpretować jak ten zbiór bitów interpretować, naruszył byś hermetyzacje. Wywołujesz funkcje więc.

0
Artist napisał(a)

Piszesz dwa komponenty (o takim samym interfejsie) do wyszukiwania dokumentów w dwóch różnych lokalizacjach. Zamiast CustomModel niech będzie np FilterArgs zawierający kryteria wyszukiwania.
Użytkownik najpierw tworzy obiekt args klasy FilterArgs i przekazuje go do Komponentu_1. Następnie jedynie modyfikuje dane w args i przekazuje go do Komponentu_2. I jeśli nie wykonasz klonowania to Komponent_1 wyszuka nie to o co pierwotnie prosił uzytkownik.
To wtedy robi się strukturę albo klasę z polami read-only tak aby nie dało się zmodyfikować obiektu a jedynie utworzyć nowy. Tak działa String, Date, Point, Color ...

Jeśli jednak musi to być bardziej rozbudowana klasa i wypada aby miała jakieś pola read/write to podpina się ją jako własność. Użytkownik wiedząc że jeden obiekt jest podpięty w dwóch miejscach będzie miał świadomość, że jego modyfikacja będzie miała wpływ na obydwa. Nie widzę tu problemu. Innym wspomniany rozwiązaniem jest wspomniane przeniesienie do metody końcowej tak aby obiekt nie był już używany po wykonaniu wyrażenia które go zawiera.

Biblioteka dostarczana wraz z kompilatorem (tutaj .NET) jest najlepszym wzorcem do naśladowania, nikt nie będzie miał problemu ze zrozumieniem/użyciem twojego kodu jeśli będzie podobny do tego, co zna bardzo dobrze.

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