Obiektówka w Javie, czyli jej nie poprawne używanie

1

Witam,
Mój problem jest dosyć nie typowy, gdyż dotyczy odpowiedzi na pytanie "jak dobrze (obiektowo i optymalnie) programować", w moim przypadku głównie w Javie. Moim zdaniem moje programy są nie wystarczająco poprawne obiektowo - są brzydkie. Według mnie nie poprawnie rozkładam problem główny na pod problemy i to komplikuje bardzo mój kod przy jego rozbudowie. Ponad to według mnie często komplikuje sobie pracę z powodu nawyków z C tzn. programuje proceduralnie, a nie obiektowo. Dodatkowo utrudniam sam sobie pracę, poprzez pisanie samemu gotowych już rozwiązań dostępnych w Javie, o których istnieniu dowiaduję się często po fakcie stworzenia sporego kawałka kodu.

Celowo założyłem temat w dziale "Java", gdyż w tym języku głównie piszę i nie wiem czy w kwestii metod programowania obiektowego są jakieś różnice w zaawansowanym posługiwaniu się językami tj. Java, C#. Dodam, że programuję obiektowo od kilku ładnych lat, a nadal posiadam fatalne nawyki, które są nie wybaczalne w programowaniu obiektowym. Studiuję informatykę na 4-tym roku (tzn. ostatni semestr inż).

Proszę o rady bardziej doświadczonych kolegów z forum jak sobie poradzić z moimi problemami.

Pozdrawiam, Kubuś :)

1

Pociesz się, że jakaś połowa bibliotek w Javy jest napisana tak, że nawet nie spełnia jednocześnie kilku zasad (tych podrzuconych w linku Antonio4s powyżej). Tak więc nie jesteś sam. Powiedziałbym wręcz, że znajdujesz się w doborowym towarzystwie. ;)

Co do czystego C, to w nim pisze się funkcyjnie, a nie proceduralnie. Proceduralnie pisze się często w Pascalu bo tam nawet proste wyrażenia robią się bardzo długaśne, więc człowiek ma odruch obronny i wali wszystko jedno pod drugim.
Co do pisania ciężkiego kodu, na którego samemu nie chce się patrzeć, to próbuj za każdym razem tak kombinować, żeby tego co piszesz dało się jak najwięcej użyć ponownie - już dwa użycia wystarczają, żeby stwierdzić, że napisany kod jest do d**y. A nawet więcej bo te użycia trochę podpowiedzą jak to poprawić (i tu wchodzi refaktoryzacja).
Przede wszystkim u mnie zwykle się okazuje, że nazwa tego co zrobiłem (klasa, metoda, pole) jest nieadekwatna do obsługiwanych zadań lub zupełnie bez sensu, więc właściwe nazwanie, to podstawa (czasem po głębokiej refaktoryzacji znowu trzeba zmieniać nazwę bo stara znowu przestaje być właściwa).
Drugą rzeczą jaka mi się ewolucyjnie pojawiła, to coś takiego co nazwałbym interfejsem prywatnym. Wszystkie metody publiczne korzystają wyłącznie z niego (i ewentualnie pól klasy). Niezwykle niebezpieczne jest np. wywoływanie jednych metod polimorficznych przez inne lub te z interfejsu prywatnego.
Podobnie pojawiła mi się potrzeba osobnego traktowania i wydzielania takich metod, które potrzebują dodatkowej pamięci przydzielanej przez new, takich które używają transmisji sieciowych oraz (osobno) systemu plików (oprócz innego stopnia niezawodności dochodzą też inne narzuty czasowe). W Javie pokrywa to się częściowo z wymuszonymi deklaracjami rzucania wyjątkami (throws), więc o wiele lepiej mi się pisze w Javie niż C++. Przy okazji prawie mi to zlikwidowało potrzebę używania profilera bo mogę bardzo łatwo oszacować wąskie gardła (używam do tego swoich adnotacji) i ryzykowne przepływy, które mogą się nie powieść.

Doszedłem tez w trakcie swojej pisaniny, że praktycznie nigdy nie trzeba z góry definiować klas. Taki kod staje się wymuszony i bardzo ciężki (trzeba go potem mocno poprawiać, albo wręcz wyrzucać). Potrzeba zgrupowania czegoś jako obiekt (i jego klasę) pojawia mi się sama kiedy wszystkie próby uproszczenia już napisanego "nieładnego" kodu zawiodły. Ewentualnie koncepcja obiektu pojawia mi się sama próbując zapisać w Javie niby coś w języku naturalnym.

Co do C#, to poza domknięciami praktycznie wszystkie elementy języka różniącego go od Javy są zbędne bo wydają się nadmiarowymi przypadkami szczególnymi ogólniejszych konstrukcji, które już w języku występują (oraz dodatkowo konstrukcjami ratującymi przez skutkami ubocznymi tych "ulepszeń"*). Poza tym (i konwencjami) nie różnią się prawie wcale, więc da się pisać kod wspólny dla obu języków z takim samym poziomem abstrakcji (dopóki nie trzeba skorzystać z elementów bibliotecznych).

Żeby soft nie był "brzydki" trzeba go po prostu refaktoryzować. Pamiętam, że kiedyś miałem potrzebę zbudowania kontenera, który będzie szybko i sprawnie iterował po elementach, które leżały w kontenerach tego kontenera. Potrzebny był mi przeskok od razu z poziomu głównego kontenera do pojedynczych zagnieżdżonych elementów. W szczególnym wypadku taki kod jest potrzebny na przykład w hash mapie gdzie można iterować najpierw po kubełkach, a te ponieważ są listami muszą iterować po kluczach lub parach (zależnie od implementacji mapy). Pierwszy kod konkretnego iteratora dla konkretnego kontenera jaki wyprodukowałem był koszmarny, paskudny wręcz i zajmował jakieś 250 wierszy. Poszczególne wywołania były zawiłe, w wielu miejscach występowały powtórzenia i bardzo mi się to nie podobało.
W międzyczasie przerabiałem kod kontenera i po mniej więcej 20 iteracjach refaktora i uproszczeń kod zrobił się tak prosty jak to możliwe i wręcz oczywisty. Na koniec zachciało mi się sparametryzować kontener, więc musiałem również sparametryzować iteratora. Dzisiaj jego kod wygląda w taki sposób, że nie potrafię nic z niego wywalić. Ani jedna rzecz w żadnym miejscu nie jest zbędna. A ja sam zrobiłem się z kodu bardzo zadowolony bo użyłem go już w ~20 miejscach i wszędzie oszczędził mi nie tylko męczącego kodowania, ale wręcz myślenia o tym. Sam kod zmniejszył się do zaledwie 80 wierszy (z javadocem).
Dlatego pozwolę go sobie tu wrzucić, żeby pokazać, że koncepcja będąca na pierwszy rzut oka banalna potem okazuje się tak skomplikowana, że ciężko ją napisać, a na koniec znowu okazuje się bardzo czytelna:

package com.olamagato.util;
import java.util.Iterator;

/**
 * Klasa reprezentująca podwójny iterator typów I oraz E, z których
 * pierwszy iteruje po drugim. Klasa pozwala iterować po E tak długo jak
 * długo obiekt klasy I może iterować po E.
 * @param <I> typ elementu iterującego po typie E
 * @param <E> typ elementu ostatecznie iterowanego za pośrednictwem I
 * @author Olamagato
 * @version 1.0
 */
public class DoubleIterator<I extends Iterable<E>, E> implements Iterator<E>
{
	/**
	 * Tworzy nowy podwójny iterator przeglądający elementy docelowe za
	 * pośrednictwem agregatu iterującego po obiektach typu I, które iterują
	 * po elementach docelowych.
	 * @param doubleIterable kolekcja iterująca po obiektach pośrednich I
	 */
	public DoubleIterator(Iterable<I> doubleIterable)
		{ this.doubleIterable = doubleIterable.iterator(); }

	/**
	 * Sprawdza dostępność aktualnego iteratora pośredniego i za jego pomocą
	 * dostępność następnego elementu docelowego.
	 * @return true jeżeli następny element docelowy może być uzyskany
	 */
	@Override public boolean hasNext()
	{
		while(true)
			if(!existCurrentIterator())
				return false; //nie ma więcej => nie ma więcej elementów E
			else if(currentElementIterator.hasNext())
				return true; //aktualny obiekt I ma kolejny element E
			else
				//dezaktualizuje wykorzystany obiekt I i powtarza
				currentElementIterator = null; //wszystko od nowa
	}

	/**
	 * Sprawdza czy istnieje następny element w aktualnym lub dostępnym
	 * iteratorze i zwraca go. Zwraca null jeżeli nie ma więcej elementów w
	 * aktualnym iteratorze, a następny iterator nie jest dostępny.
	 * @return kolejny element lub null jeżeli nie jest on dostępny
	 */
	@Override public E next()
	{
		if(!hasNext())
			return null;
		return currentElementIterator.next(); //kolejne E z bieżącego I
	}

	/**
	 * Usuwa ostatni element jeżeli aktualny iterator jest dostępny, a ostatnią
	 * operacją była metoda next().
	 */
	@Override public void remove()
	{
		if(currentElementIterator != null)
			currentElementIterator.remove(); //po currentElementIterator.next
	}

	/**
	 * Sprawdza obecność aktualnego iteratora elementów docelowych. Jeżeli nie
	 * jest on aktualny, a jest możliwość uzyskania kolejnego iteratora
	 * elementów pośrednich, to jest on uzyskiwany jako aktualny iterator
	 * elementów docelowych.
	 * @return true jeżeli bieżący lub najbliższy iterator jest dostępny
	 */
	private boolean existCurrentIterator()
	{
		if(currentElementIterator == null && doubleIterable.hasNext())
			currentElementIterator = doubleIterable.next().iterator();
		return currentElementIterator != null;
	}

	private Iterator<I> doubleIterable;
	private Iterator<E> currentElementIterator;
}

Tu przy okazji jest właśnie przykład interfejsu prywatnego w postaci metody existCurrent, który choć jest używany tylko w jednym miejscu, to nie mam zamiaru go likwidować i wbudowywać w metodę hasNext. A to dlatego, że nazwa metody dokładnie oddaje sens wydzielenia tego kawałka kodu i powoduje, że treść hasNext jest perfekcyjnie jasna już na pierwszy rzut oka. Wnętrze metody existCurrent też jest tak oczywiste, że nie potrzeba napisać ani słowa komentarza. Na dodatek bardzo mi się uprościła metoda remove ponieważ z automatu spełnia warunek wcześniejszego wywołania hasNext w zagnieżdżonym iteratorze. Podobnie metoda next była oryginalnie bardzo rozpierniczona i nieoczywista, aż nie wpadłem na to, że hasNext można wywoływać nieskończoną liczbę razy bez konsekwencji i zawsze dostanę ten sam wynik.
Tak niby prosty kod, to skutek mniej więcej 30 przeróbek rozciągniętych na kilka miesięcy. Dzisiaj mogę go używać jak zamknięte pudełko, a w połączeniu z diamentową składnią Javy 7 korzysta się z tego wręcz przyjemnie.
I tak może mieć każdy kod.
Ale nie sądzę, żebym był zdolny stworzyć go za pierwszym razem - raczej nie było żadnych szans. Trzeba po prostu refaktoryzować, używać i znowu refaktoryzować... Aż dojdzie się do ściany. :)

*-wariacja twierdzenia, że komputer jest potrzebny głównie do rozwiązywania problemów, które się wraz z komputerem pojawiły. :)

0

@Rev, nie każdy szpanuje angielskim tak jak Ty :D

0

Ten perfekcyjny kod ma juz w 2 linijce babola:

this.doubleIterable = doubleIterable.iterator();

skoro to iterator, to dlaczego zmienna sie nazywa iterable?

0

Mi się też nie podoba metoda existCurrent. Jeżeli mam przetłumaczyć ją jako "czy istnieje obecny" to ma zdecydowanie zbyt duży side-effect (i powinna się nazywać current(Iterator)Exists albo, tak jak sama Java ci podpowiada - hasCurrentIterator. Jeżeli jest to raczej "upewnij się czy obecny może istnieć i ewentualnie zapewnij go" to nazwa jest strasznie niegramatyczna (czasownik exist jest nieprzechodni) i powinno to być coś w stylu "ensureCurrentIterator".

Jedna z dobrych zasad nazewnictwa mówi o tym, że kod powinno dać się.. czytać.

0

@mr.Perfect
Zapewniam Cię, że nie ma babola. To kod mnóstwo razy przetestowany. Zmienna nazywa się doubleIterable ponieważ mimo iż sama jest iteratorem elementów I, to jest ona pośrednio iterowalna po E ponieważ każdy element I, który podaje sam iteruje po E. Zmieniałem te nazwy kilka razy aż okazało się, że to jest najlepsza i najbardziej intuicyjna. Mimo, iż kod z jednej strony wydaje się bardzo prosty, to z drugiej ma niespecjalnie prosty przepływ sterowania. Zmiana kolejności czy wyrzucenie czegokolwiek powoduje, że wszystko przestaje działać. Jeżeli nadal nie wyjaśniłem, to popatrz co się dzieje po podstawieniu typów:

private Iterator<Iterable<E>> doubleIterable;

@Rev
existCurrent, to trochę skrócona wersja existCurrentIterator. Skróciłem bo mam taką wredną przypadłość, że nie lubię zbyt długich nazw, jeżeli mi się po miesiącu wydają nadal zrozumiałe. :)
Tak więc masz rację.
ps. przeedytowałem tę nazwę w kodzie, żeby było jaśniej.

0

Zapewniam Cie, ze kod rozumiem... perfekcyjnie, sam pisalem takie cos, z tym ze moj wspiera nieskonczone ilosci zagniezdrzen, klasa nazywala sie FlatteningIterable (oraz FlatteningIterator) i byla piekniejsza niz to co wkleiles. Tutaj uwazam ze babol jest i tyle - jest to iterator, wiec nie powinien sie nazywac iterable. Wzglednie, nestedIterables albo cos w tym stylu - nestedIterable wskazuje ze jest tylko 1 a nie (potencjalnie) wiele.
existsCurrentIterator zmienajacy stan i nie wskazujacy na to jest kolejnym babolem, bardzo ladnie wylapanym przez Reva.
Dla Reva, ktory tak lubi angielski, krotkie podsumowanie kodu: 'beauty is in the eye of the beholder'. Moim zdaniem kod nie jest perfekcyjny, takiego nie ma, ale skoro Ty tak mowisz... Fajnie, ze nie dajesz sie krytyce.

0

Byl generyczny. Nie moge pokazac bo sie boje krytyki. I mi nie wolno bo to kod firmowy.

Sam sie wystawiles, przytaczajac kod jako perfekcyjny, jako przyklad godny nasladowania, jako wzor. Nie dziw sie wiec, ze zostaje krytykowany.
To nie jest typowo polskie, wszedzie jest tak samo, mozesz mi wierzyc.

0
Olamagato napisał(a):

Co do czystego C, to w nim pisze się funkcyjnie, a nie proceduralnie.

Jesteś tego pewien? Funkcyjne języki to Haskell, Clojure, Erlang, Scala.

1
rafal__ napisał(a):
Olamagato napisał(a):

Co do czystego C, to w nim pisze się funkcyjnie, a nie proceduralnie.

Jesteś tego pewien? Funkcyjne języki to Haskell, Clojure, Erlang, Scala.

Programowanie funkcyjne/ obiektowe / jakieś tam, to tylko techniki programowania, które nie zależą od użytych języków.
Można spokojnie funkcyjnie programować w C. Poza tym nie ma spójnej definicji programowania funkcyjnego.
Jedyna rzecz, która te języki różni to nacisk położony na pewne konstrukcje, tzw 'first class', które obsługują bez dodatkowego klepania (wynajdywania koła), a którymi można się posługiwać jak zmiennymi, czyli tworzyć, modyfikować, przekazywać przez parametr, itp.

0

@olo: jestes pewien? Ja na stacku widze pelno downvotow, pelno komentarzy ktore dodaja wartosc, poprawiaja, lub naprawiaja odpowiedzi; co wiecej, bardzo czesto widze odpowiedzi edytowane tak, aby wziac pod uwage komentarze. Moze faktycznie szukamy po innych stronkach. Aha, na stacku jest tez mnostwo Polakow ;d

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