Nie rozumiem sensu programowania obiektowego

1

Zacznę o zastrzeżenia, że nie zamierzam, tu wszczynać wojny ideologicznej. Jak piszę, że czegoś nie rozumiem, oznacza to, że czegoś nie rozumiem, a nie że rzucam formalne wyzwanie wyznawcom odmiennego paradygmatu ;) Poza tym wiem też, że jestem po prostu hobbystką i zdaję sobie z tego sprawę, więc nie trzeba mi ani przypominać ani udowadniać ;)

Teraz w ramach wprowadzenia: mam ze 20 lat praktyki w rzeźbieniu czegoś tam w necie, ale zawsze robiłam to sobie na własny rachunek i bardzo "po swojemu", co tłumaczy, że:

Nie kumam sensu obiektówki :/ Rozumiem sensowność takiego podejścia w sytuacji, kiedy faktycznie mam manipulować na jakichś obiektach (np. elementach strony internetowej czy symulacji floty statków różnej klasy od czego podobno zaczęła się cała idea). Nie mam natomiast pojęcia do czego mi to w sytuacji typowych zastosowań, do których używam kodu:

  • policzyć coś i wyświetlić / zapisać,
  • wczytać dane z pliku, policzyć i wyświetlić / zapisać,
  • obsłużyć formularz / cms na stronie www.

W ciągu ostatnich kilkunastu lat robiłam do sprawy kilka podejść. Oglądałam jakieś przykłady prezentujące definiowanie klas zwierzątek albo samochodów, ich właściwości, metod, dziedziczenia i wszystko to rozumiem. Nie rozumiem natomiast po ch... mi to :/ jak niby i po co mam tego użyć w wymienionych powyżej zastosowaniach. Tymczasem autorzy po dojściu do tego etapu, wychodzą z założenia, że już wszystko wytłumaczyli. Więc kończy się zawsze tak, że po prostu wzruszam ramionami i piszę sobie dalej "po swojemu".

Rozumiem celowość tworzenia programu jako hierarchii pudełek przekazujących sobie parametry, ale (w moim mniemaniu) podobny efekt daje się uzyskać programowaniem proceduralnym, tworząc odpowiednią strukturę funkcji.

W dyskusjach odnośnie pojawia się dosyć często kwestia dziedziczenia i argument, że to ułatwia utrzymanie dużych projektów. Być może, nie będę się spierać, nie mając praktyki w dodawaniu kolejnych cegiełek do cudzych, rozległych systemów.


I teraz pytania:

Czy komuś chciałoby się poświęcić czas na ukazanie kilku elementarnych, praktycznych (nie zaś zwierzątkowo-abstrakcyjnyc) przykładów zastosowania programowania obiektowego z wytłumaczeniem dlaczego akurat tak?

Czy paradygmat obiektowy jest czymś uniwersalnym? Bo mam wrażenie, że to co może faktycznie się sprawdzać przy dużych projektach zastosowane dla zrobienia zwykłego "Witaj świecie" albo czegoś podobnego, to zwykłe przeinżynierowanie?

Ew. wszelkie inne uwagi odnośnie moich lamerskich wywodów.

1

Chyba nastąpiło jakieś przesilenie, skoro napisałaś wątek na forum. :) Powiem tyle, że mam podobne wątpliwości, co Ty, i czekam na odpowiedzi. Przy okazji: zauważ, że np. w JavaScripcie funkcje są formalnie obiektami (i mogą mieć właściwości).

14

Sens programowania obiektowego bynajmniej nie wynika z dziedziczenia (jak wiemy od czasów gof kompozycja jest preferowana ponad dziedziczenie) ale właśnie z enkapsulacji, która pozwala ukrywać szczegóły w tych pudełkach.
W programowaniu proceduralnym w każdym momencie możesz wywołać dowolną procedurę, w programowaniu obiektowym tylko te które są udostępnione na zewnątrz pudełka, na dodatek musisz mieć instancję tego pudełka o określonym interfejsie (statyczne typowanie preferowane) co dodatkowo zawęża liczbę dostępnych opcji.

I właśnie to zawężanie liczby dostępnych opcji, dzielenie większego kodu na mniejsze autonomiczne fragmenty sprawia że programowanie obiektowe ma jakiś sens. A polimorfizm na dodatek pozwala nam te pudełka podmienić bez żadnych zmian w innych fragmentach kodu korzystającego z danego pudełka.

9

Przy samodzielnej walce OOP nie jest niczym niezbędnym. Czasami pewne rzeczy ułatwia, ale zgadzam się z Tobą, że nieraz może spowodować więcej komplikacji i zamieszania, niż jakby to zrobić "zwyczajnie", a pchanie czegoś na siłę tylko po to, bo "wypada" jest lekkim bezsensem. Zresztą ta uwaga dotyczy także innych "modnych" haseł - jak chociażby SOLID czy wzorce projektowe.

Natomiast obiektówka ma sens przy większych projektach, kiedy pracuje więcej osób, każdy dostaje swój kawałek. Ty skupiasz się na swoim obiekcie, chowasz przed kolegami jego całą wewnętrzną logikę i wystawiasz jedynie kilka metod do manipulacji stanem obiektu. Oni wiedzą, w jaki sposób mogą się z nim komunikować, ale jednocześnie Ty jesteś zabezpieczona przed jakimś grzebaniem i przypadkowym zmienieniem czegoś przez innych członków ekipy.

Oczywiście - zaraz pewnie ludzie zaczną podawać inne plusy OOP, ale moim zdaniem to, co powyżej jest główną zaletą.

A to co piszesz - czyli wczytać dane z pliku, policzyć i wyświetlić / zapisać to jest ewidentne pomieszanie odpowiedzialności. W teorii powinny być one rozdzielone - jeden fragment kodu (czy, zgodnie z OOP - obiekt) powinien się zajmować wczytywaniem danych, inny ich obrabianiem, a jeszcze inny zapisem czy wyświetlaniem. Kod, w którym masz to pomieszane jest o wiele mnie czytelny, a także trudniej go przebudowywać/rozszerzać. Ale ponownie - jeśli sobie ktoś grzebie samodzielnie, to te wskazówki może sobie wsunąć pod swój smoczy ogonek. Póki sam się w tym łapie, niech robi jak mu wygodnie ;)

4

policzyć coś i wyświetlić / zapisać,
wczytać dane z pliku, policzyć i wyświetlić / zapisać,

Do takich rzeczy faktycznie OOP może być nienajlepszym paradygmatem, bo takie rzeczy lepiej się robi w kategoriach myślenia o przepływie danych

  • Excel
  • programowanie funkcyjne
  • strumienie uniksowe
  • programowanie reaktywne (np. Rx)
  • data driven programming
    itp.

Wtedy masz sobie dane, które po prostu skądś płyną, są jakoś transformowane, i gdzieś się pojawiają (czy to będzie przepływ danych z komórki do komórki w Excelu, czy wynik działania funkcji w programowaniu funkcyjnym, czy strumienie wejścia i wyjścia czy obserwable w Rx - zasada podobna).

Aczkolwiek obiektówkę również można postrzegać jako przepływ danych z jednego obiektu do drugiego (zresztą jeden z pionierów OOP - Alan Kay - nawet porównywał OOP do komórek biologicznych, które się nawzajem ze sobą komunikują za pomocą jakichś tam komunikatów).

obsłużyć formularz / cms na stronie www.

Zauważ, że przy pojedynczej kalkulacji to może nie mieć znaczenia, ale jak chcesz, żeby aplikacja z automatu ci wykonywała pewne kalkulacje i odpalała pewną logikę "sama z siebie" to przydaje się mieć jakieś gdzieś tam siedzące sobie obiekty, które będą nasłuchiwać komunikatów i wysyłać komunikaty do innych (piszę "komunikat" od strony koncepcyjnej, technicznie to może być np. wywołanie metody obiektu, albo np. odpalenie funkcji która odpowiada za zdarzenie onClick przycisku na stronie). Obsługiwanie formularzy czy innego GUI akurat dość naturalnie się robi na OOP.

Czy komuś chciałoby się poświęcić czas na ukazanie kilku elementarnych, praktycznych (nie zaś zwierzątkowo-abstrakcyjnyc) przykładów zastosowania programowania obiektowego z wytłumaczeniem dlaczego akurat tak?

Praktyczne przykłady są takie, że większość osób robi obiektówkę całkowicie źle. Skupia się na złych elementach, więc większość kodu OOP pisanego przez programistów to nieporozumienie.

  • Dziedziczenie? Cóż to jest dziedziczenie? To tylko design pattern, który można użyć w jakichś ograniczonych zastosowaniach (i który można zastąpić np. kompozycją), a nie żaden fundament OOP
  • Klasy - tak samo. To tylko design pattern, żeby móc reużywać kod sprawniej i żeby kompilator miał wygodniej, a nie żaden fundament OOP.
  • Obiekty - to tylko pojemniki na dane, samo użycie obiektów nie świadczy, że kod jest obiektowy (np. w JavaScript często się używa obiektów w sytuacjach kiedy w C++ by się użyło "struktur", w Pythonie "słownika", a w PHP "tablicy asocjacyjnej"). Przy czym nie mówię, że to źle akurat. Pojemniki na dane też są potrzebne (gorzej jeśli ktoś robi pojemniki na dane, a udaje wielką obiektówkę - taki kod można rozpoznać po tym, że do wszystkich właściwości są gettery i settery - czyli efektywnie wszystkie właściwości są publiczne, nie ma enkapsulacji. Czyli programowanie proceduralne pod płaszczykiem OOP).
  • nie wychodzi poza swój język, a to błąd, bo zamiast myśleć w kategoriach OOP to myśli kategoriami swojego języka (szczególnie jeśli ktoś ma wizję OOP jak w Javie, to już w ogóle porażka. A potem ludzie piszą proceduralnie używając klas, bo myślą, że jak coś jest na klasach to już jest OOP. Java w ogóle namieszała dużo, jeśli chodzi o opis obiektówki. Jak widzę opisy wzorców projektowych czy diagramy UML - wszystko jest pod Javę pisane i w bardziej dynamicznych językach nie musi mieć to wiele sensu).
3

Dla mnie idę obiektowości jest dzielenie kodu na odpowiedzialności i abstrakcje. Czyli korzystasz tylko z tego co możesz (na co pozwalają Ci inne obiekty). Trochę jak w firmie, gdy prosisz kogoś by zrobił coś dla Ciebie to mu to mówisz, a nie siadasz razem z nim i klikasz razem w klawiaturę 

2

Praktyczny przykład z życia:

Istniał system do generowania plików XML na podstawie danych z bazy, teoretycznie w OOP (bo osoby które to pisały używały klas), w praktyce był to kod strukturalny. Każdy generowany plik posiadał własną klasę np. OdbiorcaPlikuWFormacieA, OdbiorcaPlikuWFormacieB itp. istniało takich klas około 15 (po 1000+ linijek).

Wszystko działało (w programowaniu wszystko można napisać strukturalnie i będzie działać), problem był tylko wtedy gdy trzeba było nanieść zmiany np.

  • Dla 3 Odbiorców chcemy żeby zamiast 1 wielkiego XML powstały "chunki" plików po max. 100 MB.

Jak to zrobić w tym kodzie "strukturalnym"? Trzeba nanieść do każdego z tych plików zmianę dot. zapisu plików, finalnie musimy zmienić 3 klasy. Co więcej każda z tych klas po swojemu otwierała pliki, zapisywała, po swojemu też pobierała dane z bazy.

Przepisałem cały kod na OOP. Teraz nie istniało 15 klas dla każdego generowanego pliku, tylko 1 fabryka + system który obsługuje te pliki.

Jak wcześniej wyglądało generowanie plików:

$odbiorcaPlikuWFormacieA = new OdbiorcaPlikuWFormacieA();
$odbiorcaPlikuWFormacieA->generuj();

$odbiorcaPlikuWFormacieB = new OdbiorcaPlikuWFormacieB();
$odbiorcaPlikuWFormacieB->wygeneruj(); // specjalnie dałem inną nazwę, bo każdy pisał to po swojemu

Jak wyglądał system po przepisaniu:

$writer = XMLWriter();
$generator = new Generator([
   new Odbiorca('A', $writer),
   new Odbiorca('B', $writer);
]);

$generator->generuj();

Teraz nanosimy zmianę, chcemy żeby generowało chunki tylko dla B:

$writer = XMLWriter(); // implementuje interfejs Writer, dzięki czemu Generator wie, ze posiada metode np. write() a co Writer robi w środku, to już nie ma znaczenia dla Generatora
$chunkWriter = new ChunkXMLWriter(); // też implementuje interfejs Writer
$generator = new Generator([
   new Odbiorca('A', $writer),
   new Odbiorca('B', $chunkWriter);
]);

$generator->generuj();

To kod bardzo uproszczony, finalnie te fabryki miały po kilkaset linijek, a konstruktory po kilka argumentów.

Ile zmieniliśmy plików? Tylko fabrykę i dopisaliśmy nowy writer.

Poczytaj wujka bob`a, on potrafi wytłumaczyć na czym to polega. Dodatkowo trzeba czytać dużo kodu który jest ładnie napisany.

2
neves napisał(a):

W programowaniu proceduralnym w każdym momencie możesz wywołać dowolną procedurę, w programowaniu obiektowym tylko te które są udostępnione na zewnątrz pudełka

W C możesz mieć funkcje statyczne, których nie da się wprost wywołać poza plikiem w którym się znajdują.

W C masz również "mętne wskaźniki", które pozwalają na deklaracje struktur do których możesz się dobrać tylko przez funkcje udostępnione z modułu.

@Freja Draco: w sprzedaży pojawiła się książka Object Design Style Guide która z tego co czytałem jest w miarę praktyczna, ma przykłady m.in. w C++ i PHP.
Do każdego z języków znajdziesz też pozycje polskie odnośnie OOP (patrz Helion).

W uproszczeniu najważniejsza różnica m. programowaniem proceduralnym opartym o struktury a programowaniem OOP jest taka, że w OOP masz dostępne mechanizmy kapsułkowania danych wprost w języku i dzięki temu dużo łatwiej znajduje się kod odpowiedzialny za operacje na danych elementach struktur.

2
vpiotr napisał(a):

W C masz również "mętne wskaźniki", które pozwalają na deklaracje struktur do których możesz się dobrać tylko przez funkcje udostępnione z modułu.
[...]
W uproszczeniu najważniejsza różnica m. programowaniem proceduralnym opartym o struktury a programowaniem OOP jest taka, że w OOP masz dostępne mechanizmy kapsułkowania danych wprost w języku i dzięki temu dużo łatwiej znajduje się kod odpowiedzialny za operacje na danych elementach struktur.

A czy znasz jakiś przykład z życia, gdzie mechanizm typu "FILE *" z C był niewystarczający, a mechanizm obiektowy coś dopomógł?

3
Markuz napisał(a):

Istniał system do generowania plików XML na podstawie danych z bazy, teoretycznie w OOP (bo osoby które to pisały używały klas), w praktyce był to kod strukturalny. Każdy generowany plik posiadał własną klasę np. OdbiorcaPlikuWFormacieA, OdbiorcaPlikuWFormacieB itp. istniało takich klas około 15 (po 1000+ linijek).

Ale ja nawet pisząc proceduralnie pewnie bym tego tak nie zrobiła, tylko byłaby jedna funkcja do obsługi tych 15 rodzajów plików, funkcja odwoływałaby się do podfunkcji odpowiedzialnych za poszczególne etapy przetwarzania a zmiana obsługi trzech z nich polegałaby na zmianie parametrów wywołania i ew. zmianie / dodaniu, jakichś podfunkcji.

Tzn. zakładając, że obsługa tych 15 plików posiadała jakieś wspólne, nadające się do współdzielenia etapy i że pisałabym to z głową z myślą o późniejszej rozbudowie. Bo na szybko i "na jeden raz" to się czasem różne bzdety pisuje.

1
neves napisał(a):

W programowaniu proceduralnym w każdym momencie możesz wywołać dowolną procedurę, w programowaniu obiektowym tylko te które są udostępnione na zewnątrz pudełka, na dodatek musisz mieć instancję tego pudełka o określonym interfejsie (statyczne typowanie preferowane) co dodatkowo zawęża liczbę dostępnych opcji.

No i tego też trochę nie rozumiem. Jak pisałam, jeżeli wiem, że będę operować na zbiorze obiektów (np. statków), które mają wchodzić ze sobą w interakcje, przechowywać swoje stany, reagować na polecenia, to budowa systemu klas jest dla mnie najzupełniej logiczna.

Jeżeli jednak mam program typu: policz i wyświetl, to trochę nie ogarniam filozofii:

  • zdefiniuj klasę,
  • stwórz (pojedynczy) obiekt danej klasy,
  • wywołaj jego (być może też pojedynczą) metodę,
  • i - brawo - już mamy wynik.
    :/
0

Dlatego języki takie jak Kotlin pozwalają po prostu wywołać kilka metod po sobie, beż tworzenia obiektów.

1

@danek: ale nadal to nie rozwiązuje problemu, jakim jest zabezpieczenie się przed grzebaniem w logice mojego kodu. OK, mogę (Kotlina nie znam, ale wierzę Ci na słowo) zrobić jakiś łańcuszek wywołań, coś w stylu a(x) -> b() -> c(), na zasadzie analogicznej do shellowego cat >> grep X, albo nawet c(b(a(X))) czy if a(x) {b(x)}, ale nie zabezpiecza mnie to przed tym, że ktoś sobie poczuje potrzebę wywołania nagle z czapy b(x). Tak więc ten łańcuszek wiele nie zmienia.

Z drugiej strony, jeśli sobie zrobię obiekt z metodą a(x), we wnętrzu której zostanie wywołane b() oraz c() to po pierwsze mam pewność, że wszystkie potrzebne funkcje zostaną wywołane, do tego w pożądanej przeze mnie kolejności (w wypadku "łańcuszków wywołań" muszę liczyć na to, że inny programista zastosuje się do moich wytycznych i wywoła funkcje w określonym porządku, a ponadto że nie będzie próbować robić czegoś inaczej, np. wywołać wprost jakąś funkcję, która powinna być jedynie częścią łańcuszka), a także wiem, że nie ma opcji, aby ktoś wywołała bezpośrednio b() czy c(), dobrał się do pól obiektu itp.

0

@cerrato: masz rację. Chodzi mi tylko o sytuację, że nawet mając język obiektowy, nie trzeba z tej obiektowości korzystać, jeśli okoliczności temu sprzyjają (np. robisz coś na szybko dla siebie)

1

A moim zdaniem @cerrato nie ma (do końca) racji. :) Możesz skorzystać z OOP i udostępnić obiekt z publiczną metodą a i prywatnymi b oraz c, a możesz też skorzystać z FP i udostępnić funkcję a, która woła funkcje b i c które są prywatne (co dokładnie oznacza 'prywatne' zależy od języka). Moim zdaniem do problemów jakie opisuje @Freja Draco FP nadaje się całkiem dobrze. Ja osobiście lubie pisać 'hybrydowo', głównie jest to OOP z elementami FP. :)

1

Pomijając wymienioną wcześniej enkapsulację, OOP pozwala w prostszy czy też bardziej naturalny sposób zamodelować domenę. Łatwiej jest operować na obiekcie Customer który sam "pilnuje" swoich danych i zasad rządzących nimi, niż na kilku wartościach wyciągniętych z bazy (czyli kolumnach) i przekazywaniu tychże między funkcjami.

3

@Aventus: no własnie z tym modelowaniem domeny za pomocą OOP to jest lekki bait, bo pojęcia domenowe nie zawsze 1:1 da się przemapować na obiekty, a często właśnie taka próba na siłę modelowania kończy się jakimiś dzikimi strukturami

0

Ale to już wina tego kto źle to modeluje. To trochę tak jak z nożami- to że je masz w kuchni nie znaczy że są narzędziem zbrodni, dopóki Ty z nich tego nie zrobisz. Jeśli nie da się zmapować 1:1 to tego nie robisz. Swoją drogą nigdy nie spotkałem się z czymś takim jak mapowanie 1:1 modelu domenowego do kodu. W sensie że jakby to miało wyglądać? Jeśli w kartotece klient ma "takie i takie" pola to w kodzie muszą się zgadzać 1:1? Brzmi bez sensu. Kto w ogóle tak próbuje robić i jaki miałby być tego cel?

3

Fundamentalną ideą w OOPie jest wrzucenie zachowania (metod) i stanu (pól) do jednego miejsca zwanego zwykle klasą. Dziedziczenie, typowanie, etc są cechami drugorzędnymi, aczkolwiek w praktyce nierozerwalnie związane z OOPem. Podobnie w FP - tutaj podstawową ideą jest unikanie efektów ubocznych, ale w praktyce by uniknąć nadmiaru kodu wręcz musi się stosować masę domknięć i przez to sam zwięzły zapis dla domknięć jest brany za podstawę FP (a to jest bullshit, bo można mieć mocno imperatywny kod naszpikowany lambdami).

Wszystko rozbija się o polimorfizm. Dla przykładu w OOP możemy mieć:

interface Source {
  String readFully();
}
class FileSource implements Source { ... }
class NetworkSource implements Source { ... }
class RandomSource implements Source { ... }
class FixedSource implements Source { ... }

Bez OOP i bez FP możemy mieć np:

enum SourceType { ... }
final class Source {
  SourceType type;
  String sourcePath;
}
// gdzieś zupełnie indziej
switch (source.type} {
  SourceType.File:
    // działamy na pliku
  SourceType.Network:
    // działamy po sieci
}

Przy FP mamy np typeclassy.

Jeżeli będziesz pakować do struktury z danymi także wskaźniki do funkcji to będziesz robić swojego (raczej kulawego) OOPa.

Przy klasycznym OOPie jeśli dostanę obiekt to przeglądając jego metody wiem mniej więcej co można z nim zrobić. Przy FP i typeclassach mogę sprawdzić które typeclassy zostały zaimplementowane dla danego typu. Bez klas i typeclass zostaje mi przeglądanie wszystkich funkcji które biorą dany typ jako parametr - tylko część tych funkcji implementuje funkcjonalność ogólnego przeznaczenia, taką która byłaby wrzucona do klasy czy typeclassy. Przez to muszę przeglądać znacznie więcej funkcji.

1
Wibowit napisał(a):

Wszystko rozbija się o polimorfizm. Dla przykładu w OOP możemy mieć:

interface Source {
  String readFully();
}
class FileSource implements Source { ... }
class NetworkSource implements Source { ... }
class RandomSource implements Source { ... }
class FixedSource implements Source { ... }

Bez OOP i bez FP możemy mieć np:

enum SourceType { ... }
final class Source {
  SourceType type;
  String sourcePath;
}
// gdzieś zupełnie indziej
switch (source.type} {
  SourceType.File:
    // działamy na pliku
  SourceType.Network:
    // działamy po sieci
}

No i w tym akurat przykładzie wariant bez OOP i bez FP wygląda na znacznie lepszy. Źródło z pliku i źródło z sieci, która może sprawić, że przekazywanie danych zawiesi się na kilka minut, to źródła o bardzo różnej charakterystyce i lepiej jest tego faktu nie ukrywać. Może gdyby interfejs zawierał inne funkcje niż readFully, miałoby to jakikolwiek sens, ale i w takim przypadku raczej dałoby się napisać prosty proceduralny odpowiednik. W tym przypadku co tutaj podany, nie widzę żadnego pożytku z istnienia interfejsu. Bo po co byłoby gdziekolwiek przekazywać interfejs, zamiast po prostu na początku programu wczytać dane ze źródła (być może zupełnie odmiennie traktując źródła blokujące i nieblokujące), a potem jawnie przekazać te dane dalej?

0

@Troll anty OOP: Pomyśl sobie, że twój program to nie jedyny program na świecie. Ludzie tworzą biblioteki pełne klas i są to zarówno biblioteki ogólnie dostępne jak i biblioteki wewnętrzne w danym projekcie. Taki Source mógłby być wykorzystywany przez N projektów z czego każdy projekt wykorzystywałby ten Source w inny sposób.

Źródło z pliku i źródło z sieci, która może sprawić, że przekazywanie danych zawiesi się na kilka minut, to źródła o bardzo różnej charakterystyce i lepiej jest tego faktu nie ukrywać.

A coś ukrywam? OOP to po prostu sposób organizacji danych i kodu. Ukrywanie to dodatek. Jeżeli tego switcha opakuję w metodę to to będzie to takim samym ukrywaniem logiki jak polimorfizm na typowo OOP-owych podklasach czy też typowo FP-owych typeclassach.

No i w tym akurat przykładzie wariant bez OOP i bez FP wygląda na znacznie lepszy.

Wariantu FP nie pokazałem.

1

@Freja Draco:
wyobraź sobie że masz aplikację internetową np. bankową i masz możliwośc tworzenia raportów.
Programowanie obiektowe ułatwia takie coś dzięki polimorfizmowi, masz interfejs:

public interface ReportGenerator {
    void generate(Data data, Path dstFile);

   ReportFormat  supportedReportFormat();
}

Teraz możesz zaimplementować np.


public final class PdfReportGenerator {
  
  public void generate(Data data, Path dstFile) {
    void generate(Data data, Path dstFile) {
     //skomplikowana logika
   }

   public  RepertFormat supportedReportFormat {
      return ReportFormat.PDF
   }
 
}

Teraz możesz dodawać nowe formaty bez mieszania w kodzie, jesli chcesz dodać Excel to dodajesz Excel, nie zmieniając tworzenia PDFów

1
Wibowit napisał(a):

@Troll anty OOP: Pomyśl sobie, że twój program to nie jedyny program na świecie. Ludzie tworzą biblioteki pełne klas i są to zarówno biblioteki ogólnie dostępne jak i biblioteki wewnętrzne w danym projekcie. Taki Source mógłby być wykorzystywany przez N projektów z czego każdy projekt wykorzystywałby ten Source w inny sposób.

Teoretycznie mógłby, tylko dlaczego miałby preferować interfejs, zamiast po prostu wczytania całości na początku i przekazania treści dalej?
Ludzie robią bardzo wiele różnych rzeczy, ale ten wątek został rozpoczęty prośbą o uzasadnienie na przykładzie, kiedy OOP będzie lepszy, a nie tylko o stwierdzenie faktu, że jest używany.

@scibi92: Teraz możesz dodawać nowe formaty bez mieszania w kodzie, jesli chcesz dodać Excel to dodajesz Excel, nie zmieniając tworzenia PDFów

Jakbym miał generowanie raportów zwyczajnie w funkcjach, też bym miał taką możliwość.
generatePDFreport, generateXMLreport,... chcę dodać Excel-owe, to dodaję funkcję generateExcelReport i mam wtedy dwie alternatywy: albo w miejscu gdzie chcę wywołać tworzenie raportu jawnie wywołuję jedną z tych funkcji, albo mam funkcję zbiorczą, wywołującą jedną z tych podstawowych w switch-case.

0

@scibi92:
W programowaniu strukturalnym też możesz dodać obsługę nowego formatu. Różnica jest jednak taka iż w OOPie zmieniasz kod w innym miejscu. Dla przykładu jeśli w OOP mam:

interface BaseType {
  A method1(B b);
  C method2(D d);
  E method3(F f);
}
class Variant1 { ... } implements BaseType
class Variant2 { ... } implements BaseType
class Variant3 { ... } implements BaseType

To mogę dodać class Variant4 { ... } implements BaseType w dowolnym miejscu bez konieczności modyfikacji pozostałych implementacji metod czy interfejsu BaseType.

W przypadku programowania strukturalnego miałbym np:

enum StructType { ... }
class Struct {
  StructType type;
  X param1;
  Y param2;
  Z param3;
  A method1(B b) {
    switch(type) {
      // obsługa wszystkich przypadków
    }
  }
  C method2(D d) {
    // analogicznie do method1
  }
  // method3 analogicznie do method2
}

W przypadku imperatywnym widać jak na dłoni, że by dorobić kolejny podtyp trzeba mieć możliwość edycji oryginalnego kodu i podorzucać kolejny szczebelek w każdym switchu. W przypadku OOPa takiego jak np w Javie mogę rozszerzyć klasę z biblioteki i jej instancję wrzucić z powrotem do funkcji bibliotecznych i wszystko ładnie zadziała bez konieczności prucia tejże biblioteki i bez konieczności robienia kulawej imitacji OOPa w postaci np wrzucania wskaźników do funkcji do środka struktur.

0
Troll anty OOP napisał(a):
Wibowit napisał(a):

@Troll anty OOP: Pomyśl sobie, że twój program to nie jedyny program na świecie. Ludzie tworzą biblioteki pełne klas i są to zarówno biblioteki ogólnie dostępne jak i biblioteki wewnętrzne w danym projekcie. Taki Source mógłby być wykorzystywany przez N projektów z czego każdy projekt wykorzystywałby ten Source w inny sposób.

Teoretycznie mógłby, tylko dlaczego miałby preferować interfejs, zamiast po prostu wczytania całości na początku i przekazania treści dalej?
Ludzie robią bardzo wiele różnych rzeczy, ale ten wątek został rozpoczęty prośbą o uzasadnienie na przykładzie, kiedy OOP będzie lepszy, a nie tylko o stwierdzenie faktu, że jest używany.

Dlatego, że mając instancję obiektu z metodą readFully masz kontrolę nad tym kiedy tą metodę wykonujesz. Możesz np wziąć dwa Source i jeśli jeden (np szybszy) się wysypie to korzystasz z drugiego (np wolniejszego, ale bardziej stabilnego).

3

Moja pierwsza praca na etat wiązała się z wielkim optymizm do wykorzystania programowanie zgodnego z OOP, a kilka lat później moja ostatnia praca na etat właśnie skończyła się przekonaniem, że OOP to samo zuo :-) Poniżej przedstawiam skąd u mnie jest taka diametralna zmiana:

Ogólnie OPP traktuje jako sposób na uzyskanie rozszerzalnego kodu w statycznie typowanych językach.

W przypadku języków dynamicznych masz jeszcze do wyboru sposób pisania rozszerzonego kodu z użyciem funkcji wyższego rzędu, ale do tego wrócę za moment.

W każdym razie chodzi o uzyskanie sposobu na rozszerzalność, docelowo chcemy uzyskać takie warunki w projekcie by zaplanowane zmiany można było wprowadzać bez konieczności modyfikowania istniejącego kodu. Możesz o tym myśleć tak jakby Twoja klasa była pluginem, który można podpiąć lub odpiąć. Oczywiście, by to wszystko działało musisz wcześniej zaprojektować w swoim kodzie odpowiednie sloty, by właśnie tam dało się podpiąć nową klasę.

Rozszerzalność niestety nie jest za darmo, każda rozszerzalność kosztuje i sprawia, że Twój projekt staje się bardziej magiczny i być może z czasem coraz trudniejszy do pojęcia całościowego. Ta sprawa komplikuje się (moim zdaniem do kwadratu) jeśli definiowane sloty nie są pomyślane, jeśli są tworzone pod konkretny task, a nie pod cały system, jeśli ogólnie brakuje w tym zachowanego umiaru i spójności (co widać po tym jak rozjeżdża się wszystko w czasie). W projektach w jakich pracowałem często widziałem ten problem i ogólnie bardzo ciężko jest uzmysłowić innym ludziom, że stawianie na mniejsze interefejsy dają większe korzyści.

Gdzieś tak w połowie mojej drogi zrozumiałem, że to OOP nie działa tak jak powinno, ale do końca nie widziałem z czego to wynikało.

Dodatkowo było trudniej, ponieważ generalnie działałem w pythonie, a w tu nie ma wbudowanych interfejsow (są różne biblioteki, mają charkter inwazyjny i na ogół ich się nie stosuje). Znacznie gorzej ludziom idzie wyczuć balans w tworzeniu klas, bo niby co miałoby ich ograniczyć? Często widzę przerośnięte klasy, które są po prostu workiem na metody niż pomyślaną abstrakcją do rozwiązywania konkretnego problemu. Najbardziej patologiczne były klasy, które dziedziczyły po klasach z frameworku.

Problem tworzyłem nie tylko ja, ale ogólnie cały zespół, który próbował improwizować z OOP.

Z biegiem lat nauczyłem się tak tworzyć klasy, by redukować ilość nieporozumień. Wtedy moje klasy wystawiały po 1-2 metody. Ostatecznie bardziej złożone zachowania uzyskiwałem łącząc ze sobą nie klasy, a obiekty. W java pewnie musiałbym posiłkować się wzorcami projektowymi, ale w Pythonie służyła mi jego dynamiczna natura, która pozwala mi używać różnych obiektów bez definiowania odgórnych interfejsów.

Dla przykładu gdybym dla każdej klasy chciał wywołać funkcję render to wystarczy dać:

def render_all(renderable):
   for obj in renderable:
      obj.render()

i sekwencja renderable może mieć obiekty różnych klas i te klasy nie muszą dziedziczyć po tym samym, ani mieć odgórnie zaimplementowany interfejs - wystarczy, że da się wywołać render i tyle.

Pod koniec drogi widziałem, że skoro w klasie mam 1 metodę to może zamiast klas po prostu lepiej byłoby pisać funkcje. Jeśli będę chciał coś rozszerzyć to mogę dodać kolejne funkcje, mogę je składać itp nieświadomie coraz bardziej rozumiałem jakie możliwości wiążą się z kodem funkcyjnym.

No i teraz podsumowując:

OPP w teorii jest super, bo daje Ci formalne konstrukcje do pisania rozszarzalnego kodu, ale w praktyce (gdy lekko zmieniają się wymagania) to stworzone abstrakcje raz dwa stają się rozmyte. Dla mnie jedyna rzecz jaka w takich okolicznościach działa to tylko kompilator i taka praca jest trochę do bani, bo w tym wszystkim nie powinno chodzić o to by kod był zrozumiały tylko dla komputera.

Mam wrażenie, że do podobnych wniosków doszedł Paul Graham, gdy pisał dialekt lispa pozbawiony elmentów obiektowości: http://www.paulgraham.com/noop.html

0

Oop dobrze działa i nie tylko w dużych projektach, choćby z uwagi na kontrolę typów i polimorfizm. Już w ogóle najlepiej gra z paradygmatem funkcyjnym.
Jeszcze jest kwestia języka. Oop w Javie to nie to samo co oop w scali, nie mówiąc o common lispie I smalltalku. W lispie już w ogóle jest to coś zupełnie innego (na korzyść - petarda), smalltalka nie znam, ale o ile wiem to podobne. W c++ I Javie oop jest dość toporne.
Jednak nie popieram oop jako religię, nie do wszystkiego jest potrzebna

0
nohtyp napisał(a):

Mam wrażenie, że do podobnych wniosków doszedł Paul Graham, gdy pisał dialekt lispa pozbawiony elmentów obiektowości: http://www.paulgraham.com/noop.html

Trawa u sąsiada jest zawsze bardziej zielona. Myślę, że gdyby karierowicze po bootcampach pisali w tym cudownym nie-OOP-owym dialekcie LISPa to i tak doszłoby do szeregu patologii. Niech mi ktoś pokaże chociaż jeden język w którym wszyscy piszą elegancki kod, taki jak w najlepszych książkach czy kodach demonstracyjnych.

2

wczytać dane z pliku, policzyć i wyświetlić / zapisać

@Freja Draco bardzo dobry przykład.

Czy jak czytasz z pliku, to przejmujesz się tym:

  • Na jakim systemie plików jest ten dokument zapisany?
  • Czy jest symlinkiem czy nie? A może w ogóle jest to kolejka (nazwana lub nie)…
  • Może jest zmmapowany?

Chodzi o to, że na Uniksach FILE* to jest interfejs, który jest implementowany przez ileś różnych podsystemów, które tylko wystawiają API, które pozwala Tobie tego używać. Ciebie jako użytkownika nie obchodzi jak to jest zaimplementowane "pod spodem". Dokładnie tak samo w swoich założeniach działa programowanie obiektowe, gdzie wystawiasz tylko w miarę mały interfejs i użytkownik nie musi wiedzieć jak to działa pod spodem.

1
hauleth napisał(a):

Chodzi o to, że na Uniksach FILE* to jest interfejs, który jest implementowany przez ileś różnych podsystemów, które tylko wystawiają API, które pozwala Tobie tego używać. Ciebie jako użytkownika nie obchodzi jak to jest zaimplementowane "pod spodem". Dokładnie tak samo w swoich założeniach działa programowanie obiektowe, gdzie wystawiasz tylko w miarę mały interfejs i użytkownik nie musi wiedzieć jak to działa pod spodem.

Oczywiście podział na kernel i program w przestrzeni użytkownika sprawdził się w praktyce. Tego nie planuję kwestionować. Jednak fakt, że idea interfejsu sprawdziła się na granicy program-kernel, lub kernel-sprzęt, nie oznacza że tak sensowne abstrakcje da się odnaleźć w większości innych zadań. Dyskusje OOP kontra programowanie proceduralne nie są dyskusjami o tym, czy używać systemu operacyjnego, czy z niego zrezygnować - korzystanie z systemu operacyjnego jest opcją zawierającą się zarówno w programowaniu proceduralnym jak i obiektowym.

Pytanie jest, czy mając już kernel i korzystając z niego, rzeczywiście skorzystamy na tworzeniu dużej liczby kolejnych interfejsów. Dużej liczby, bo jeśli mowa o liczbie bardzo małej, to bez sensu jest w ogóle używanie specjalnego określenia do takiego modelu programowania, jeżeli ten element jest aż tak małym wycinkiem całości. Nie ma też wtedy sensu przygotowywanie języków programowania, by to ułatwiały, skoro jest to tak rzadko potrzebne a i w językach nie-obiektowych wykonalne może nieco trudniej, ale też bez jakiejś nadzwyczajnej gimnastyki.

Te interfejsy, które są między kernelem a programami, albo nawet interfejsy w bibliotece standardowej, dopracowywane były przez całe lata. Na pewno nie było proste wymyślenie ich od razu dobrze. Z kolei jeśli interfejs był nieudany, to po jego zmianie na lepszy, poprawienie używających go programów to bardzo dużo pracy - przeniesienie programu z jednego systemu operacyjnego na drugi, albo od razu przygotowanie aby działał na więcej niż jednym, jest bardzo trudne, jeśli tylko korzysta się z abstrakcji które się między systemami różnią.

Błędnie podzielony na części składowe program proceduralny, łatwiej zmodyfikować niż błędnie podzielony na części program obiektowy.

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