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. :)