Wpis z mikrobloga

Hej miraski. Mam problem projektowy i zastanawiam się jak go rozwiązać, żeby nie pogwałcić zasady Open-Closed Principle.

Przypuśćmy, że mamy klasę abstrakcyjną

Shape
. Mamy też kilka klas konkretnych, tj.

Circle
,

Triangle
, wszystkie dziedziczą po

Shape
i implementują metodę

draw
. Do tej pory wszystko jest zajebiście, OCP jest zachowana, super. Potrzebuję jednak metody, powiedzmy o takiej sygnaturze:

Shape getProperShape(Whatever someArgument)
która na podstawie odpowiedniej wartości argumentu zwróci mi obiekt odpowiedniej klasy dziedziczącej po Shape. A więc przykładowo jeśli dostanie argument o takiej wartości to zwróci mi

Circle
, jak o innej to

Triangle
itd.

Teraz tak, dodanie takiej metody statycznej do klasy

Shape
już będzie pogwałceniem tej reguły - bo jak dojdzie nowy kształt, powiedzmy

Square
, to ta metoda będzie wymagać zmiany, aby obsługiwała tę nową klasę.

Jak to rozwiązać? Stworzyć osobną klasę np.

ShapeFactory
? Ale to w sumie dalej będzie gwałcenie tej zasady, tylko że na innej klasie.

#programowanie #cleancode #oop
  • 14
@Marmite: Tak czy tak gdzieś musisz mieć logikę "mapującą" wartości argumentu someArgument na konkretne klasy. Normalnie robi się to wzorcem metody fabrycznej (FactoryMethod, bardzo podobne do tego co sam wymyśliłeś z metodą statyczną), ale jak sam zauważyłeś nie jest to idealne rozwiązanie, bo również łamie OCP
@MacDada: (#) Czyli co, każdorazowe utworzenie klasy dziedziczącej po

Shape
, powiedzmy tenże właśnie

Square
, to będzie:

- kod klasy

Square
- kod klasy

SquareProvider
-

Shape.providers.add(new SquareProvider())
coś takiego? I wtedy metoda

getProperShape
byłaby

for (ShapeProvider provider: Shape.providers) {

Shape result = provider.create(someArgument);

if(result != null) {

return result;

}

}

zgadza się?
@korri: (#) Jeśli dobrze mi się wydaje, to chyba takie rozwiązanie jak dał @MacDada: (#) pozwala nie łamać OCP no chyba że źle patrzę. Ale generalnie to chyba jest kompromis, albo OCP, albo klasa typu Factory, prawda?
@Marmite: Tak. To jest kompromis. Wynika to z tego, że ta logika mapowania o której wyżej pisałem, gdzieś fizycznie musi "leżeć". Jeśli leży w jednej oddzielnej klasie, to mamy klasyczne Factory - jeśli natomiast jest "rozsmarowana" po innych klasach typu providery/bezpośredno shape'y - to mamy rozwiązanie zaproponowane przez @MacDada i zachowane OCP.
zgadza się?


@Marmite: Tak, druga opcja to nie sprawdzać czy null, tylko dać dwie metody:

ShapeProvider.supports(args)
i

ShapeProvider.create(args)
– wtedy możesz iterować i użyć tego co zwróci

true
przy

supports()
. Oczywiście każdy

SquareShapeProvider
, etc implementuje

ShapeProviderInterface
.
@Marmite: Przykładowo tak działa (#php) Symfony Serializer.

Zanim dane/obiekty zostaną zserializowane np do JSONa, muszą zostać „spłaszczone” („znormalizowane”) do prostych danych. Każdy normalizator implementuje NormalizerInterface z dwiema metodami:

normalize()
i

supportsNormalization()
.

Serializer przyjmuje normalizatorów w konstruktorze, a kiedy ma coś zserializować, to iteruje po normalizatorach i korzysta z tego, który twierdzi, że potrafi obsłużyć przekazane dane.

Tym samym Serializer nie ma ścisłych powiązań z danymi, na których ma działać
@Hauleth: (#) Z tego co widzę to właściwie sama ta funkcja jest złamaniem zasady Liskov - szkoda, że nigdzie nie ma informacji czym ją w dobry sposób zastąpić :< nawet Wujek Bob tutaj wspomina o analogicznej funkcji, ale potem już rozważa trochę inne rzeczy...