Programowanie obiektowe, co i jak?

0

Witam! Nie wiedziałem gdzie zapytać no to stawiam na Newbie. Mam kilka pytać co do programowania obiektowego? Czy naprawdę jest ono takie dobre skoro w ostatnich 2 dekadach tak ogarnęło świat programowania? Pisze jakieś tam aplikacje w C++ z wykorzystaniem klas ale nie nazwałbym tego podejścia obiektowego. Moje pytania to:

  1. Jak PORZĄDNIE zaprojektować aplikacje napisaną z podejściem projektowym? Zazwyczaj starałem się wypisać na kartce potrzebne mi funkcje i poukładać je w jakieś sensowne klasy ale czy to dobre podejście?
  2. Czy jeżeli pozostało mi kilka funkcji, które nie pasują do żadnej sensownej klasy warto z nich zrobić dodatkową klasę, a funkcje zadeklarować jako static ?
  3. Czy na moim poziomie nauki obiektowości warto uczyć się jakiegoś języka modelowania? np UML, jeśli tak to jakie polecacie narzędzia/kursy co do tego jezyku ?

Pozdrawiam

0
  1. Jeżeli jest to złożona aplikacja to warto właśnie użyć wzorców typu UML. Jeżeli nie taka bardzo trudna i piszesz ją sam, to nawet bez kartki się obejdzie. Najłatwiej jednak na komputerze sobie to rozrysować, nawet jako obrazek, ale pewnie są gotowe programy do wzorców. VS w tych lepszych wersjach ma wizualne budowanie klas, to też pomaga.
  2. Tak. No bo co z nimi zrobisz?
  3. A dlaczego by nie? Na pewno przyda się w pracy i nie tylko. Ale warto dopiero wtedy, gdy już ogarniesz dobrze programowanie, na pewno nie od razu.
0

ad.2
To jest dość normalna praktyka, taką klasę oznacza się jako utils albo jakoś tak.

2

Czy jeżeli pozostało mi kilka funkcji, które nie pasują do żadnej sensownej klasy warto z nich zrobić dodatkową klasę, a funkcje zadeklarować jako static ?

Jeśli ci jakieś funkcje nigdzie nie pasują, to znaczy że coś jest nie tak z projektem, albo są niepotrzebne.

0
TenCoNieJestWDomu napisał(a)
  1. Jak PORZĄDNIE zaprojektować aplikacje napisaną z podejściem projektowym? Zazwyczaj starałem się wypisać na kartce potrzebne mi funkcje i poukładać je w jakieś sensowne klasy ale czy to dobre podejście?

To dobry sposób. Ja go kiedyś używałem częściej. Teraz, na co dzień, bardzo rzadko. Może dlatego, że doświadczenie mi wzrosło bardziej niż stopień skomplikowania projektów, które robię.

Stosuję za to rutynowo inne narzędzie, którego i Ty się musisz w końcu nauczyć, jeśli chcesz być dobrym programistą. Refaktoryzacja. To po prostu zmiana struktury istniejącego kodu. Brzmi prosto, ale trzeba trochę nauki i doświadczenia, by robić to sprawnie. Tj. małymi kroczkami, nie psując niczego.

Poprzez refaktoryzację możemy rozumieć np. głupią zmianę nazwy metody lub zmiennej. Albo wydzielenie metody -- gdy masz metodę, która jest za długa, i chcesz ją podzielić na kilka mniejszych. Równie często korzystam z wydzielenia klasy (raczej obiektu, bo na co dzień piszę w języku bez dziedziczenia klasycznego).

Na co dzień pracuję więc tak:

  1. Zastanawiam się nad rozwiązaniem. Myślę, jakie obiekty mogę wydzielić.
  2. Tworzę sobie zalążki tych obiektów (w Twoim przypadku: definicje klas).
  3. Piszę kolejne metody.
    Czasami zaczynam od definicji kilku pustych metod naraz, ale często nie muszę tego robić, bo mam to w głowie. Mam we łbie zestaw narzędzi: wzorców projektowych, standardowych zachowań i typów obiektów. Np. "ten obiekt to tak naprawdę będzie mapa" albo "tamten obiekt będzie zarządzał flagami" lub "ten obiekt będzie koordynatorem", "proxy" czy jakimś kontenerem. W głowie od razu widzę część metod, jakich będę potrzebował.
  4. Załóżmy, że podczas pisania kodu widzę, że coś jest nie tak. Np. (pisząc w terminach klasycznego dziedziczenia) że połowa metod używa połowy własności obiektu i zarówno te metody, jak i własności, mają pewien człon w nazwie lub są logicznie mocno powiązane. Zapala mi się lampka mówiąca, że klasa cierpi na za niską kohezję, tj. nie jest wystarczająco integralna. I wydzielam klasę z tej połowy związanych ze sobą pól i metod. Wydzielona klasa charakteryzuje się wysoką kohezją, tj. wszystko jest w niej ze sobą ściśle związane. Jeśli dodatkowo jest mała i zajmuje się jedną rzeczą na jednym poziomie abstrakcji, to dla mnie informacja, że dokonałem dobrego podziału.

Nie próbuję więc od razu rozpisać pełnej struktury klas i trzymać się jej superdokładnie. Refaktoryzacja pozwala mi na dokonanie zmian gdy już dojdę do rozwiązania jakiegoś problemu i zobaczę, że pierwotny wybór nie był optymalny. Teraz mam lepsze zrozumienie problemu i mogę dokonać lepszego wyboru.

Ważne jest jednak, by te rzeczy, o których piszę, nie były dla nas pretekstem i usprawiedliwieniem do lenistwa. W porównaniu do mniej doświadczonych programistów, ja jestem naprawdę skrupulatny i konsekwentny jeśli chodzi o refaktoryzację i utrzymywanie jakości kodu. I trochę już się naprojektowałem, więc łatwiej mi ad hoc wymyślać przyzwoite struktury w miarę prostych obiektów.

Wiem też na ogół, kiedy powinienem się zatrzymać i przypisać planowaniu znacznie większy priorytet, niż standardowo.

Np. gdy planuję globalną architekturę aplikacji, których się ma trzymać N programistów w X różnych projektach. Wtedy trzeba przemyśleć mnóstwo rzeczy, wiele konsekwencji. Na firmowym wiki powstają wtedy ścisłe dokumenty -- najpierw propozycje, potem standardy. Na forum zakładane są wątki, a na odpowiednim etapie organizowane są też spotkania i brainstormingi. Architekturę sprawdza się też, w miarę możliwości, za robiąc jakiś fragment projektu od A do Z, wszystkie warstwy (tzw. metoda pocisków smugowych, wspomniana w książce "Pragmatyczny programista") lub za pomocą projektu pilotażowego.

Zdarza się, że pół godziny czy więcej spędzam na wymyślaniu nazwy dosłownie paru modułów na krzyż.

Niedawno miałem taką sytuację. Moduł zawierał:

  1. Obiekt/przestrzeń nazw, która miała bardzo ogólną odpowiedzialność. Ciężko było nadać jej wystarczająco konkretną nazwę. W tamtym momencie nazwę miała akurat konkretną ale, jak się okazało, zupełnie mylącą.
  2. Kilka obiektów o związanych z pewną funkcjonalnością. Wszystkie miały ten sam przedrostek i -- w sumie -- długie, niewygodne nazwy.
  3. Jeszcze trochę rzeczy, które wydawały się nazwane OK.

Nad samą nazwą z punktu 1. myślałem z pół godziny i jeszcze skonsultowałem się z dwoma innymi programistami. Jednym, który nie odnalazł się w tym module i zgłosił mi, że go nie ogarnia (bo to ja stworzyłem tę złą hierarchię -- taaak, wpadki zdarzają mi się tak jak każdemu) i jeszcze z drugim b. dobrym programistą. Ostatecznie nazwę wymyśliliśmy wspólnie z tym drugim, każdy z nas jedno słowo (lol). W punkcie 2 przeprowadziłem odrobinkę żmudną, ale prostą refaktoryzację, zmieniając nazwy obiektów i plików i przemieszczając pliki do osobnego folderu, przez co mogłem usunąć z nazw przedrostek.

Również niedawno zdarzyło mi się, że kumpel poprosił mnie o radę i godzinę rozkminialiśmy wyjątkowo upierdliwy pod względem IO problem. Rysowaliśmy na kartce diagramy, próbowaliśmy podpasować problem pod znane nam wzorce, analizowaliśmy konsekwencję proponowanych po kolei rozwiązań. A co się stanie, jak ktoś będzie chciał dodać jeszcze jeden sposób na X? Czy damy zapewnić silną typizację oraz utrzymać zasadę otwarty-zamknięty?

TenCoNieJestWDomu napisał(a)
  1. Czy jeżeli pozostało mi kilka funkcji, które nie pasują do żadnej sensownej klasy warto z nich zrobić dodatkową klasę, a funkcje zadeklarować jako static ?

Można, choć to oczywiście ciężko już nazwać programowaniem OO. Nieraz ciężko tego uniknąć.

Ponownie jednak: nie usprawiedliwiajmy się. Najpierw porządnie się zastanówmy, czy nie lepiej zrobić z tego jakąś klasę. Zawsze możesz opisać tu na forum sytuację i podać nazwy funkcji i problemy, jakie mają rozwiązywać. Może wymyślimy dla nich jakąś klasę.

Jako przykład mogę podać ciasteczka na stronach www. Nie wiem, czy siedzisz w webdevelopmencie. Ciasteczka to po prostu informacje zapisywane w przeglądarce: pary nazwa/wartość, które zostają zapisane na dysku i które można odczytać pomiędzy jedną wizytą użytkownika a drugą.

Przeważnie, ciasteczka obsługuje się funkcjami statycznymi. Chodzi mi akurat o język JavaScript, który nie ma dziedziczenia klasycznego, ale będę używał terminologii klasycznej. Przeglądarka udostępnia "średnio" wygodne API analogiczne do funkcji globalnych:

void setCookie(String name, String value);
String getCookie(String name);

Twórcy bibliotek dodają do tego trochę bajerów (które pominę) i porządkują ciasteczka, tworząc moduł/klasę i definiując metody klasyczne:

class Cookies {
  static void setCookie(String name, String value);
  static String getCookie(String name);
}

I używa się tego tak:

Cookies.setCookie("moje_ciasteczko", "wartosc ciasteczka");
println(Cookies.getCookie("moje_ciasteczko"));

Ale przecież można to napisać obiektowo -- w większości modułów, nad którymi pracowałem, takie API było dużo wygodniejsze:

class Cookie {
  Cookie(String name); // konstruktor
  void set(String value);
  String get();
}

Można go było używać tak:

Cookie myCookie = new Cookie("moje_ciasteczko");
myCookie.set("wartosc");
println(myCookie.get());

Zysk z podejścia obiektowego widać lepiej w prawdziwej aplikacji. Tu to trochę uprościłem, ale ciasteczka, oprócz nazwy, mają też inne ustawienia: ścieżkę, można też sobie zdefiniować domyślną wartość (zwróci ją get() gdy dane ciasteczko nie jest ustawione) itp. W podejściu obiektowym, gdy używamy ciasteczka, to ścieżkę podajemy -- tak samo jak nazwę -- tylko raz: przy wywołaniu konstruktora. W podejściu z metodami klasycznymi nazwę i ścieżkę podajemy przy każdym setCookie() i getCookie().

2
Azarien napisał(a)

Jeśli ci jakieś funkcje nigdzie nie pasują, to znaczy że coś jest nie tak z projektem, albo są niepotrzebne.

Oj panie...Chociaż podam kilka przykładów bo może to ja coś robiłem źle. W językach takich jak java (C# pewnie też) każda klasa opakowująca jakiś prymityw (Integer) ma coś na podobę toString(), jednak w języku takim jak C++ trzeba używać strumienia do zmiennej, tudzież formatowania wewnętrznego whateva. Do tej pory zamiast kopiować kod przekształcającego np int na string wolałem sobie zrobić osobną klasę gdzie zbierałem takie metody jak Int2String(); String2Int(). A samą klasę nazwałem sobie StringUtils, uważasz to za złą praktykę? Podobnie zrobiłem gdy w kilku protokołach binarnych które implementowałem zaczęły powtarzać mi się niektóre operacje na bajtach, to sobie zrobiłem klasę ByteUtils gdzie zbierałem takie metody jak Nkb2Bcd(); MakePrintableString(); SplitByte() itp.

(edit literówki)

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