Artykuł

kwi 17 2009
0

Piszemy elastyczny i czytelny kod

Niejednokrotnie pisząc kod źródłowy, nie zastanawiamy się nad jego estetyką czy elastycznością. Piszemy go, tylko po to by dział. Sprawdzamy podstawowe scenariusze, aby uniknąć klasycznych błędów, nanosimy ewentualne poprawki i cieszymy się z końcowych efektów. Wszystko jest ładne i pięknie, ale do czasu kiedy nie musimy zajrzeć do tego kodu ponownie lub po prostu gdy ktoś inny w niego nie zajrzy. Z czasem dostrzegamy nieład jaki panuje w naszych źródłach, dostrzegamy, że coś można było zrobić inaczej, zastanawiamy się do czego była dana zmienna itp. Jak uniknąć tego chaosu? Osoby pracujące w zawodzie informatyka - programisty, zapewne już wiedzą - skorzystać ze standardów kodowania, pisać elastyczny kod i testować każdy element naszej aplikacji na idioto odporność (musimy przewidzieć każde, nawet najgłupsze zachowanie potencjalnego użytkownika). O tym jak sobie radzić z powyższymi problemami, napiszę w dzisiejszym artykule.

Konwencje nazewnictwa

Jest wiele konwencji nazewnictwa, choć niewątpliwie najpopularniejszą jest tzw. styl wielbłądzi. Ideą stylu wielbłądziego jest to, że nazwę zmiennej/obiektu rozpoczynamy od małej litery, a każde kolejne słowo wchodzące w skład nazwy zaczyna się od dużej litery. Osobiście preferuje zmodyfikowany styl wielbłądzi (pierwsza litera również duża) wzbogacony o notację węgierską.

Notacja węgierska pozwala zawrzeć w nazwie zmiennej, informacje o jej typie. Przykładowo zmienna tekstowa zawierająca informację o imieniu klienta sklepu może być zapisana tak:

String sCustomerName = "Jan";

W powyższym zapisie należy zwrócić uwagę na kilka aspektów. Po pierwsze użyliśmy przedrostka s (od string), który określa typ naszej zmiennej. Nazwa zmiennej składa się ponad to z dwóch wyrazów: Customer oraz Name. Ostatni fakt, to angielskie nazewnictwo. Każdy dobrze wie, że językiem informatyki jest język ang., nazywanie elementów naszej aplikacji w tym właśnie języku wymusza na nas samodyscyplinę. Ponadto dzięki temu, zawsze pomyślimy nad logiczną nazwą dla naszej zmiennej, którą zawsze da się łatwo wytłumaczyć.

Wróćmy jednak do przedrostków używanych do nazywania zmiennych. W tabeli zamieszczonej poniżej przedstawiłem podstawową grupę zmiennych i zalecany przedrostek dla nich: (pełną listę można znaleźć na stronie Wikipedii). Oczywiście część z przedrostków jest umowna i możemy dokonać małej modyfikacji na nasze potrzeby w zależności od specyfikacji używanego języka.

Nazwa angielska Opis Przedrostek
string łańcuch tekstowy s
integer liczby dziesiętne i
double liczby zmiennoprzecinkowe d
boolean zmienne boolowskie b
array string tablica zmiennych tekstowych as
array integer tablica liczb dziesiętnych ai
array double tablica liczb zmiennoprzecinkowych ad
array boolean tablica zmiennych boolowskich ab
object obiekt o
class klasa C
member of class składowa klasy m_

Przejdźmy do analizy tabeli. Myślę, że pierwsze cztery wiersze są dość oczywiste (pierwsze litery określonego typu). W wierszach 5-8 znajdują się typy tablicowe. Ich przedrostki składają się z dwóch liter: a (skrót od array) oraz pierwsza litera określająca typ zmiennej. Najbardziej interesujące są jednak trzy ostatnie wierszy tabeli. Przedrostkiem o określamy wszystkie zmienne obiektowe. Obojętnie czy są to obiekty naszych klas, czy też klas dostarczonych przez określony język programowania. Swój przedrostek posiadają również klasy. Należy jednak zwrócić uwagę, że nazwę klasy poprzedzamy dużą literką C. Ostatni wiersz przedstawia oznaczenie składowych klas. Każda z takich zmiennych musi być poprzedzona takim przedrostkiem. Po takim przedrostku umieszczamy typ zmiennej a dopiero potem właściwą nazwę. Przykład:

private String m_sCustomerName = "";

Należy zwrócić uwagę, że nie stosujemy żadnych przedrostków w wypadku metod użytych w naszej klasie.

Poniższy listing przedstawia przykładowy kod, w którym zastosowano powyższe zasady:

public class CCustomer
{
	private StringBuilder m_oStringBuilder = null;
	private String m_sCustomerName = "";
	private String m_sCustomerSurname = "";
	private int m_iCustomerAge = 0;
	
	public CCustomer(String sCustomerName, String sCustomerSurname, 
		int iCustomerAge)
	{
		m_sCustomerName = sCustomerName;
		m_sCustomerSurname = sCustomerSurname;
		m_iCustomerAge = iCustomerAge;
	}
	
	public void BuildString()
	{
		m_oStringBuilder = new StringBuilder();
		m_oStringBuilder.append("Imię: " + m_sCustomerName);
		m_oStringBuilder.append("Nazwisko: " + m_sCustomerSurname);
		m_oStringBuilder.append("Wiek: " + m_iCustomerAge);
	}
	
	public String GetString()
	{
		return m_oStringBuilder.toString();
	}
}

Utworzyliśmy tutaj klasę CCustomer, która posiada 4 zmienne składowe, konstruktor przyjmujący trzy parametry oraz dwie metody publiczne. Warto zwrócić uwagę na nazewnictwo. Zgodnie ze standardami jest ono angielskie. Ponadto użyto przedrostków określających typ zmiennej/obiektu. Powyższy listing przyda się nam jeszcze w dalszej części artykułu.

Dobre praktyki programistyczne

Używanie nawiasów klamrowych

Jak wszyscy zapewne wiedzą, nawiasy klamrowe pozwalają wydzielić określony blok kodu, np. ciało klasy, metody, pętli itd. Aby poprawić estetykę kodu warto odpowiednio formatować te bloki. Przykładowo, domyślny kod wygenerowany przez większość edytorów wygląda tak:

class CCustomer{
	// ciało klasy
}

Jednak najlepszą praktyką jest umieszczanie klamer w nowych liniach:

class CCustomer
{
	// ciało klasy
}

Odstępstwo od tej reguły warto jednak stosować w językach skryptowych używanych w Internecie, ponieważ w takim wypadku musimy wysłać więcej linii kodu.

Klamer warto również używać np. wtedy gdy teoretycznie nie są one wymagane. Np. gdy tworzymy instrukcję warunkową IF z 1 linią poleceń. Wiele języków nie wymaga wtedy klamer. Dla przejrzystości kodu zawsze warto je stosować. Zawsze należy również pamiętać o koniecznych wcięciach przy zagłębianiu kodu.

Ustawianie wartości początkowych zmiennych

Kiedy tworzymy nasze zmienne czy też obiekty, obojętnie czy są one składowymi klasy, czy też nie, warto nadać im określoną wartość początkową. Z reguły jest jest to jakaś wartość zerującą. Np. dla liczb integer jest to 0, dla liczb double 0.0, dla zmiennych boolowskich najczęściej jest to false, dla stringów pusty cudzysłów (niektóre języki obsługują inne ciekawe konstrukcje, np. string.Empty w C#) itd. Jeśli wiemy, że w określonej aplikacji dana zmienna najczęściej przyjmuje określoną wartość (wartość domyślną), to właśnie taką wartość możemy ustawić na początku.

Stosowanie instrukcji Switch

Wielu początkujących programistów, często w łatwy sposób zabrudza kod, stosując rozbudowane instrukcje IF. Często można je w łatwy sposób zastąpić innymi konstrukcjami językowymi. Np. konstrukcją Switch. Wyobraźmy sobie taki kawałek kodu napisany w Javie:

BufferedReader oBufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Wpisz nazwę Twojego ulubionego dnia tygodnia");
string sDay = oBufferedReader.readLine();
if("Poniedziałek" == sDay)
{
	System.out.println("Nie lubię poniedziałku");
}
else if("Wtorek" == sDay)
{
	System.out.println("Wtorek nie jest taki zły");
}
//...
else
{
	System.out.println("Nie znam takiego dnia tygodnia!");
}

Jest to oczywiście fragment instrukcji IF, należy tu jeszcze zaimplementować pozostałe dni, jednak już teraz idzie zauważyć złożoność całej konstrukcji. W sytuacjach gdy w instrukcji IF za każdym razem przyrównujemy wartości, warto pokusić się o wykorzystanie instrukcji switch:

BufferedReader oBufferedReader = new BufferedReader(new InputStreamReader(System.in));
System.out.println("Wpisz nazwę Twojego ulubionego dnia tygodnia");
string sOutput = "";
string sDay = oBufferedReader.readLine();
switch(sDay)
{
	case "Poniedziałek":
		sOutput = "Nie lubię poniedziałku";
		break;
	case "Wtorek":
		sOutput = "Wtorek nie jest taki zły";
		break;
	//...
	default:
		sOutput = "Nie znam takiego dnia tygodnia!";
		break;
}
System.out.println(sOutput);

Czy powyższy kod nie jest czytelniejszy? Zastosowaliśmy tutaj jeszcze jeden trick. Użyliśmy tymczasową zmienną sOutput. Dzięki czemu uniknęliśmy wielokrotnego wypisywania konstrukcji wyświetlającej tekst na ekranie.

Instrukcje switch często znajdują zastosowanie w stronach internetowych, gdzie w szybki sposób pozwalają na wykonanie określonej akcji, w zależności od otrzymanej komendy.

Długość pojedynczej linii kodu

Dobrym zwyczajem jest ograniczanie długości linii kodu. Powszechnie przyjętym ograniczeniem jest granica 80 znaków. Jeśli nasza linia jest dłuższa, to powinniśmy ją złamać. Jeśli jest to tekst zawsze możemy skorzystać ze znaku konkatenacji (przykładowo w Javie i C# jest to +, a w PHP używamy .).

Zamykanie obiektów

Choć większość nowoczesnych języków posiada wbudowany tzw. Garbage Collector, który zamyka nieużywane obiekty, to dobry programista powinien zadbać o to sam. Szczególnie jest to ważne w przypadku operacji na plikach/bazie danych. Jeśli nie zamkniemy obiektu, to możemy mieć problem z późniejszym uzyskaniem dostępu do określonego zasobu. Dlatego ważne jest zamykanie obiektów, ich niszczenie. Warto również zwrócić uwagę na bloki try .. catch. Jeśli jakiś obiekt może spowodować wyjątek to najlepiej go zamykać w bloku finally, który wykona się nawet w przypadku niepowodzenia określonej operacji.

Optymalizacja kodu

Dzielenie kodu na klasy i funkcje

Pisząc obiektowy kod źródłowy, należy korzystać z różnych właściwości tego podejścia. Przykładowo jeśli pobieramy jakieś dane z bazy danych, to warto pokusić się o stworzenie specjalnej struktury do przechowywania tych danych. Dzięki temu będziemy mogli opracować metody, które pozwolą na szybki dostęp do interesujących nas wartości.

Nigdy nie należy również przesadzać z długością określonych klas/funkcji, a tym samym unikać nadmiarowości i powtarzania. Każda klasa/funkcja zawsze powinna spełniać określoną funkcjonalność, generować jakiś wynik itp. Należy unikać programowania, w którym cały kod wykonamy w jednej funkcji. Funkcje powinny być stosunkowo krótkie.

Przykładowo wyobraźmy sobie klasę CDataManager do pobierania informacji o klientach z naszej bazy danych. Taka klasa może posiadać następujące metody:

public CDataManager(CDatabaseConnection oConnection){}
public void ConnectDb()
public List<CCustomer> GetCustomersDetails()
public void CloseConnection()

Pierwszą z metod jest konstruktor, który jako parametr przyjmuje obiekt klasy CDatabaseConnection (jest to klasa wymyślona na potrzeby tego przykładu zawierająca informacje na temat połączenia z bazą). Drugą metoda służy do łączenia z bazą. Trzecia z wyżej wymienionych metod pozwala na pobieranie danych. Wynikiem działania tej funkcji jest lista obiektów klasy CCustomer. Ostatnia metoda służy do zamykania połączenia z bazą danych.

Unikanie nadmiarowości w stosowaniu składowych klasy

Pisząc nasze klasy powinniśmy unikać nadmiarowości w stosowaniu składowych klasy. Należy zwrócić uwagę, że każda taka składowa będzie znajdować się w pamięci przez cały czas życia obiektu. Dlatego stosujmy składowe tylko tam gdzie to jest konieczne. W przeciwnych wypadach składowe klasy możemy zastąpić np. przez dodanie nowego parametru dla określonej metody.

Porównywanie wartości

Kiedy porównujemy wartości (np. w instrukcjach IF) z reguły stosujemy następujący zapis:

if(sTest == "test") { }

Jednak ze względu na optymalizację zaleca się stosowanie formy odwrotnej czyli:

if("test" == sTest) { }

Oczywiście nie obracamy kodu w przypadku używania znaków mniejszości/większości, gdyż może to utrudnić odczyt takiego zapisu.

Dokumentacja projektu

Kiedy już utworzymy nasze dzieło programistyczne, to warto sporządzić dokumentację projektu. Istnieje wiele standardów dokumentacji, ale jednym z najpopularniejszych jest JavaDoc. Jest to sposób dokumentacji opracowany dla Javy, ale obecnie, ze względu na popularność narzędzi Eclipse oraz Netbeans, używany również w wielu innych językach.

W każdym kodzie największy nacisk kładzie się na dokumentacje klas oraz metod. Można również dokumentować pojedyncze składowe klasy. Kod dokumentacji umieszczamy w znacznikach komentarza. Przykładowa dokumentacja dla konstruktora klasy CCustomer wygląda następująco:

/**
* Constructor
* @param sCustomerName customer name
* @param sCustomerSurname customer surname 
* @param iCustomerAge customer age
*/

W pierwszej linii znajduję się opis (bardzo krótki) danej metody. W kolejnych liniach opisane są trzy parametry użyte w konstruktorze naszej klasy. Spójrzmy jeszcze na kolejny przykład:

/**
* Returns info about customer
* @return String Customer data
*/

Jest to kod dokumentacji do ostatniej metody (GetString). Warto zwrócić uwagę na słowo kluczowe return, które przechowuje informacje o zwracanej wartości.

Powyższe przykłady przedstawiły schemat działania JavaDoc. Określonych parametrów jest znacznie więcej i zachęcam do ich samodzielnej analizy. Warto zwrócić uwagę na fakt, że dokumentacja nie tylko pozwoli zapanować nad porządkiem w naszym kodzie, ale również umożliwia korzystanie z podpowiadania składni, oczywiście wtedy, gdy używamy odpowiedniego edytora.

Kod dokumentacji JavaDoc można w szybki sposób przetworzyć do HTML'a, co jest niewątpliwą zaletą tego sposobu.

Podsumowanie

Podsumowując, programować należy z głową. Zawsze należy pamiętać, że kiedyś może zajść potrzeba by powrócić do określonego kodu ponownie. Jeśli napiszemy go w sposób niechlujny, to zrozumienie jego działania zajmie nam długie godziny. Dlatego zawsze trzeba być dokładnym i precyzyjnym, a skończoną pracę udokumentować.

Powyższy artykuł przedstawia jedynie mały zakres technik, które wspomagają pisanie dobrego kodu. Zachęcam do własnych eksperymentów i wybrania najlepszych dla siebie metod.

Data ostatniej modyfikacji: 05.06.2011, 17:18.

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus