Wątek przeniesiony 2019-07-29 15:33 z przez cerrato.

Nie rozumiem sensu programowania obiektowego

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, botów: 0