728 x 90

, i

Clean code – czyli jak poprawnie programować

Clean code – czyli jak poprawnie programować

W życiu każdego początkującego (i nie tylko!) programisty nadchodzi taki moment, gdy zaczyna się dla niego liczyć nie tylko poprawne działanie kodu, ale i jego przejrzystość. Powstało wiele książek na temat tak zwanego clean code, ze swojej strony możemy polecić „Czysty kod. Podręcznik dobrego programisty” R. C. Martin’a. Wszystkich zainteresowanych poprawnym pisaniem kodu zapraszamy do przeczytania artykułu.

 

Clean code – czyli jak poprawnie programować.

 

Czym dokładnie jest czysty kod?

Często gdy początkujący programista pisze swój kod, nie zastanawia się co będzie, gdy zajrzy do niego ktoś obcy, ani gdy on sam spojrzy na niego za kilka miesięcy. Wychodzi wtedy z założenia „byleby działało”. Problem pojawia się w momencie, gdy próbuje wrócić do swojego programu po upływie dłuższego czasu. Zaczyna wtedy zadawać sobie pytania „o co mi chodziło?”, „po co to jest?” oraz „co to robi?”. To samo dzieje się w przypadku, gdy kod czyta osoba trzecia – stale zastanawia się „co autor miał na myśli?”. A przecież nie o to chodzi w poprawnym programowaniu.

Dobrze napisana implementacja powinna być elegancka i przejrzysta. Znacznie przyjemniej czyta się kod po którym widać, że twórca o niego dbał. Powinien być prosty, bezpośredni. Musi być napisany tak, by zwykły czytelnik mógł szybko i poprawnie wywnioskować działanie wszystkich procedur w nim zawartych.

Mamy nadzieję, że przekonaliśmy Was do słuszności teorii clean code.

Nazwy zmiennych

Jak pewnie się domyślacie nazwy powinny przedstawiać zamiary. Wbrew pozorom wybór odpowiedniego określenia zmiennej nie jest aż tak prosty. Wszystkie nazwy pojawiające się w naszym kodzie powinny być tak dobrane, by odpowiadały na wszystkie ważne pytania. Muszą informować w jakim celu zostały stworzone, co robią czy też w jaki sposób są używane.

Zwróćmy uwagę na przykład:

Int d; // odległość w cm

Podana powyżej zmienna nic nie mówi czytelnikowi. Brak informacji o jej zastosowaniu wypełnia w tym przypadku komentarz. Według zasad clean code komentarze nie powinny być potrzebne do wyjaśniania celu istnienia zmiennych – to zmienne powinny mówić same za siebie.

Spróbujmy więc zmienić nieco nazwę:

Int OdlegloscOdSrodkaOkregu;

Int OdlegloscOdSrodkaUkladuWspolrzednych;

Wybór nazwy odzwierciedlającej zamiary programisty znacznie ułatwia zrozumienie kodu czytelnikowi. Nie trzeba bać się długich nazw – ten drobny czas poświęcony na ich zapisanie jest naprawdę dobrą inwestycją 😉

Należy również uważać, aby nazwą nie zdezorientować czytelnika. Przykładowo, słowo „lista” ma dla programisty specjalne znaczenie. Z tego powodu nie powinno się nazwać zmiennej ListaPracowników, o ile faktycznie nie jest ona listą. Innym przykładem dezorientacji jest używanie dużego O  i małego l – ze względu na ich duże podobieństwo do zera i jedynki.

Powinniśmy również używać nazw łatwych do wyszukania. Wyobraźcie sobie, że macie przed sobą tysiąc linijek kodu. Niby nie tak dużo? To teraz spróbujcie wyszukać konkretną zmienną. Klikacie tradycyjnie Ctrl + F. Ustawiacie kursor na polu „wyszukaj”. Wpisujecie nazwę, otrzymujecie wyniki. Jeśli zmienna jest jednoliterowa możecie dostać nawet kilkadziesiąt różnych wyników – większość nie tych, których oczekiwaliście. Jeżeli jednak jej nazwa jest łatwa do wyszukania, np. Wektor_Ortogonalny_W_Kracie poprawny wynik zobaczycie od razu.

Nazwy klas (klasy)

Nazwy klas i obiektów powinny być rzeczownikami np. Client czy AddressParser.

Abstrakcje powinny być przede wszystkim małe. Należy używać ujednoliconej struktury dla każdej z nich, czyli najpierw publiczne zmienne statyczne, następnie prywatne zmienne statyczne i instancyjne. Po liście wykorzystywanych zmiennych powinna znajdywać się sekcja metod: najpierw publiczne, po nich prywatne.

Napisaliśmy, że klasy powinny być małe, ale co to znaczy? Jak mamy to mierzyć?  Czy klasa z pięcioma metodami jest mała? To zależy, w porównaniu z pięćdziesięcio elementową strukturą tak, ale klas nie można mierzyć tylko za pomocą długości kodu. Występuje tu pojęcie odpowiedzialności. Każda klasa powinna zajmować się tylko jedną czynnością.

Spójrzmy na przykład:

Class Program{

    Public int ZwrocNumerAktualnejWersji;

    Public int ZwrocNumerPoprzedniaWersji;

    Public int ZwrocKodWeryfikacyjnyWersji;

    Public void UstawLogiUzytkownika;

    Public void ZmienHasloUzytkownika;

}

Na pierwszy rzut oka klasa ta zawiera metody, które odpowiadają tylko za informację o programie. Jednak po głębszej analizie widzimy, że można ją podzielić na dwie osobne. Możemy np. stworzyć osobną klasę wersja z dokładnie jedna odpowiedzialnością.

Class Wersja{

    Public int ZwrocNumerAktualnejWersji;

    Public int ZwrocNumerPoprzedniaWersji;

    Public int ZwrocKodWeryfikacyjnyWersji;

}
 

Nazwy metod

Metody, w przeciwieństwie do klas,  powinny w swoich nazwach zawierać czasowniki, np. Save czy DeletePage. Akcesory, mutatory i predykaty powinny mieć nazwy opatrzone przedrostkiem get, set lub is np. setName, isExist

Po pierwsze i najważniejsze: funkcje powinny być małe. Im mniejsze, tym łatwiej je czytać.

Dodatkowo, wszelkie instrukcje if, else, while i im podobne powinny mieć po jednym wierszu. Jak tego dokonać? Jeżeli instrukcja ma za zadanie wykonać bardziej skomplikowane operacje, najlepiej byłoby, aby była wywołaniem innej funkcji. Dzięki temu znacznie zmniejszania jest objętość głównej funkcji.

Komentarze

Pewnie wiele razy zdarzyło się wam powrócić do dawno pisanego kodu. Jak się przekonaliście nie jest to proste, jeśli nasz program nie został dobrze udokumentowany. Jedną z metod zwiększania przejrzystości kodu są komentarze. W tym akapicie przybliżymy wam, jak wykorzystywać je z głową.

Sens stosowania komentarzy jest zapewne wszystkim znany, jednak ich ilość i miejsce gdzie dokładnie powinny się pojawić już nie. Oczywiście istnieje wiele teorii, ale my podzielamy opinię, że nie powinno ich być wcale. Dobry kod mówi programiście o jego funkcji. Ma być czytany jak proza. Nie może być zawiły i niezrozumiały. Komentarze pojawiają się wtedy gdy chcemy ukryć nasz zły kod lub są one naprawdę niezbędne. Warto jednak zastanowić się co zrobić aby było ich jak najmniej.

Spójrzmy na przykład

 // sprawdzenie czy student jest uprawniony do otrzymania stypendium

If((student.wiek<26 && student.rokstudiow>1 && student.dochod>500) || student.srednia>4.0)

Struktura tego if jest bardzo złożona. Ciężko od razu powiedzieć co jest sprawdzane. Nie wiemy czemu 26, czemu 500 i czemu coś jest większe od 1. Spróbujmy to zmienić.

 if(student. czy_jest_uprawniony_do_stypendium())

W powyższym przypadku czytelnik już po przeczytaniu nazwy funkcji wie jakie wartości są sprawdzane i nie jest potrzebny żaden komentarz wyjaśniający.

Klauzuli pomijanych podczas kompilacji możemy używać, aby wyjaśnić argumenty funkcji, których nazw nie jesteśmy w stanie zmienić np. funkcji znajdujących się w różnych bibliotekach. Jednak należy przed tym sprawdzić dokładnie ich dokumentację, aby nie wprowadzić innych programistów w błąd.

Assert.AreEquala(-1,b); // b==-1

W poprzednich akapitach wyjaśniliśmy jak zastąpić niepotrzebne komentarze. Najczęściej należało dokonać zmian w kodzie. Teraz postaramy się opowiedzieć coś o treści jaką powinien poruszać w nich programista. Należy używać poprawnej gramatyki i struktury języka w którym piszemy. Mają to być proste do zrozumienia komunikaty, tak aby przypadkowa osoba z ulicy mogła je zrozumieć. Spójrzmy na przykład:

 a=tmp.ustaw()
 if( a==true){

    ……

} else{

// wartości domyślne

    a.dom();

}

O co chodzi w tym kodzie? Prawdopodobnie nikt nie wie. Możemy się jedynie domyślać, a to jest niedopuszczalne w programowaniu. Nie wolno nazywać zmiennych pojedynczymi literami. Nazwa obiektu tmp także nic nam nie mówi, podobnie jak nazwa metody ustaw(). Co ustaw? Datę, godzinę, adres, kolor, smak? Spójrzmy jeszcze na komentarz. Programista prawdopodobnie chciał pokazać, że w przypadku klauzuli błędu zostanie ustawiona wartość domyślna dla obiektu. Pomijając już, że „dobrym stylem programowania” byłoby użycie klauzuli try catch o której opowiemy w następnej sekcji, nie wiemy co ustawia tę wartość domyślną ani gdzie. Postarajcie się teraz poprawić ten kod na kartce lub w kompilatorze

Udało się? Sprawdźmy Wasze rozwiązania.

Try{

    NowyZegarek.UstawAktualnaGodzine()

}catch{

    NowyZegarek.UstawDomyslnaGodzine()

}

W powyższej formie kod jest bardziej przejrzysty i nie potrzeba już komentarza. Każdy wie „co autor miał na myśli”.

Warto także zapamiętać, że to kod ma nieść informację o swoim działaniu i stanowić w pewnym sensie baśń. Czytelnik (programista) ma ją czytać nie musząc zastanawiać się nad sensem kodu. Implementacja ma być dziełem sztuki, które to wprawia czytelnika (często klienta) w zachwyt.

Nie warto używać komentarzy do oczywistych rzeczy. Nie zakładajmy, że osoba która przegląda kod, chce się nauczyć programować czytając go. Nie powinno się zatem podpisywać konstruktorów np.

Class Punkt{

    Private:

    int x;

    Int y;

    String TypUkladuWspolrzednych; //określa typ układu współrzędnych

    Public:

    // Konstruktor klasy Punkt

    Punkt(int x, int y){

        This.x=x; // ustawienie współrzędnej x

        This.y=y // ustawienie współrzędnej y

    }

}

Każdy kto programował obiektowo wie czym jest konstruktor, więc po co zanieczyszczać kod? Podobnie jest z komentarzami przy ustawieniu wartości. Pomińmy je, a nasz kod stanie się bardziej przejrzysty. Wracając do pierwszego komentarza – także on jest zbędny, ponieważ nazwa zmiennej niesie już tę informację.

Przeglądając akademickie kody (w tym nasze :D) praktycznie zawsze możemy się spotkać z zakomentowanymi wierszami. Są to najczęściej fragmenty, w których programista zmieniał kod lub się nad nim zastanawiał. Możemy z całą pewnością powiedzieć, że nie zostaną one nigdy użyte, a jedyne co powodują to szum informacyjny i dezorientację osoby analizującej.

Jak się pewnie już przekonaliście komentarz ma nieść za sobą prosty przekaz. Spójrzmy teraz na inny przykład:

/*

Szyfr Cezara (zwany też szyfrem przesuwającym, kodem Cezara lub przesunięciem Cezariańskim) – jedna z najprostszych technik szyfrowania. Jest to rodzaj szyfru podstawieniowego, w którym każda litera tekstu jawnego (niezaszyfrowanego) zastępowana jest inną, oddaloną od niej o stałą liczbę pozycji w alfabecie, literą (szyfr monoalfabetyczny), przy czym kierunek zamiany musi być zachowany. Nie rozróżnia się przy tym liter dużych i małych. Nazwa szyfru pochodzi od Juliusza Cezara, który prawdopodobnie używał tej techniki do komunikacji ze swymi przyjaciółmi.
 */

class SzyfrCezara{

    private:

    int klucz;

    String TekstJawny;

    String Szyfrogram;

    Public: void Zaszyfruj();

}

 

Komentarz opisujący historię czy całą ideę działania kryptosystemu jest absolutnie bez sensu. Osobę czytającą kod nie interesuje przecież, że szyfr Cezara jest monoalfabetyczny. Wystarczy tylko dobra implementacja i poprawne nazwy zmiennych i metod.

Spójrzmy na chwilę na poniższy kod. Czy da się coś z niego zrozumieć?

import java.awt.Color;

public class Trafienia{
   int liczba1=10;
   int liczba2=10;
   Statek[]tab1;     //10 statkow gracza1
   Statek[]tab2;     //10 statkow gracza2
   public Trafienia(){
      tab1 = new Statek[10];
      tab2 = new Statek[10];
      int x=0;
      for(int i=0; i<10; i++){
         if(i==0)
            x=4;
         else{
            if(i<3)
               x=3;
            else{
               if(i<6)
                  x=2;
               else
                  x=1;
               }
            }
         tab1[i]= new Statek(x);
         tab2[i]= new Statek(x);
      }
   }
   int trafilo(int i, int j, int gracz, Przycisk[][]tab){
      int x=0, y=0, flaga=0, w;
         Statek s=null;
         Pole p;
      if(gracz==1){                    //kto strzelal - gracz       
         while(x<10&&flaga==0){                //szuka podanych wspolrzednych
            s= tab2[x];
            w=s.wielkosc;
            while(y<w&&flaga==0){
               //System.out.println("spr polestatku: "+y);

               p = s.polastatku[y];
               //System.out.println("pola: "+p.x+" "+p.y);
               if(p.x==i&&p.y==j){
                  flaga=1;
                  p.trafienie=1;
                  s.plywa=s.plywa-1;
                  //System.out.println("wielkosc statku zmniejszona, wynosi teraz "+s.plywa);
               }
               y++;
            }
            x++;
            y=0;
         }
         //System.out.println("wielkosc statku ="+s.plywa);
         if(s.plywa == 0){     //statek tonie - narysuj czarna obwodke
            y=0;
            w=s.wielkosc;
            liczba2--;
            //System.out.println("869ilosc statkow ="+liczba2);
            while(y<w){
               p = s.polastatku[y];
               int l=p.x-1;
               int m=p.y-1;
               for(int k=0; k<3; k++){
                  for(int n=0; n<3; n++){
                     if(l+k>=0&&m+n>=0&&l+k<10&&n+m<10){
                     if(tab[l+k][m+n].pole==3)
                        tab[l+k][m+n].przycisk.setBackground(Color.white);}
                  }
               }
                  y++;
                  }
            if(liczba2==0)
            return 0;           ////koniec gry
            else
               return 1;
         }
         else
            return 1;
      }
######to jest dla opcji gdzie strzela komputer a nie gracz, dziala analogicznie tylko na innej tablicy
      else{
         //System.out.println("wspolrzedne: "+i+" "+j);
         System.out.println("szukam statku z : "+i+" "+j);
         while(x<10&&flaga==0){                //szuka podanych wspolrzednych

            s= tab1[x];
            w=s.wielkosc;
            System.out.println("statek: "+x);
            //System.out.println(" wielkosc: "+w);
            while(y<w&&flaga==0){
               //System.out.println("spr polestatku: "+y);

               p = s.polastatku[y];
               System.out.println("pola: "+p.x+" "+p.y);
               if(p.x==i&&p.y==j){
                  flaga=1;
                  p.trafienie=1;
                  s.plywa=s.plywa-1;
                  System.out.println("wielkosc statku zmniejszona, wynosi teraz "+s.plywa);
               }
               y++;
            }
            x++;
            y=0;
         }
         System.out.println("wielkosc statku ="+s.plywa);
         if(s.plywa == 0){     //statek tonie - narysuj czarna obwodke
            y=0;
            w=s.wielkosc;
            liczba1--;
            System.out.println("DFGilosc statkow ="+liczba1);
            while(y<w){
               p = s.polastatku[y];
               int l=p.x-1;
               int m=p.y-1;
               for(int k=0; k<3; k++){
                  for(int n=0; n<3; n++){
                     if(l+k>=0&&m+n>=0&&l+k<10&&n+m<10){
                     if(tab[l+k][m+n].pole==3)
                        tab[l+k][m+n].przycisk.setBackground(Color.gray);
                     // System.out.println("Analizuje: "+(l+k)+" "+(m+n)+ "Pole= "+tab[l+k][m+n].pole);
                     
                     }
                  }
               }
                  y++;
                  }

            if(liczba1==0)
            return 3;           ////koniec gry, wygral komputer
            else{
               if(s.plywa==0)
                  return 2;
               else
               return 1;}
         }
         else
            return 1;        
      }
   }
}

W rzeczywistości poniższa klasa jest fragmentem gry w statki – jeżeli ktokolwiek z Was podczas zajęć powiedział kiedyś do swojego towarzysza ze szkolnej ławki “C5!” a ten odpowiedział “pudło!” lub “trafiony” doskonale wie, o co chodzi. Czy patrząc na kod czytelnik wie, w jakim celu został on zaimplementowany? Z pewnością nie!

Postarajmy się zatem przerobić implementację zgodnie z zasadami clean code.

public class strzal_gracza{

  int liczba_statkow_pierwszego_gracza=10;

  int liczba_statkow_drugiego_gracza=10;

  Statek[] plansza_gracza_pierwszego;

  Statek[] plansza_gracza_drugiego;

}

Statek stworz_statki(){

  public strzal_gracza(){

  plansza_gracza_pierwszego = new Statek[10];

  plansza_gracza_drugiego = new Statek[10];
 
  Plancza_gracza_pierwszego = stworz_statki();

  Plansza_gracza_drugiego = stworz_statki();

}

 int zwroc_liczbe_plywajacych_statkow_gracza(int Numer_gracza){

 if(Numer_gracza==1)

   return liczba_statkow_pierwszego_gracza;

 else

  return liczba_statkow_drugiego_gracza;

}

{…}

 void narysuj_obwodke_zatopionego_statku(Statek zatopiony_statek, int Numer_gracza){

  …

 }

 void zamaluj_trafione_pole(){

 ...

 }

 int usun_pole_statku(trafione_pole, Numer_gracza, trafiony_statek){

 int liczba_plywajacych_statkow = zwróć_liczbe_plywajacych_statkow_gracza( Numer_gracza);

 trafiony_statek.plywajace_pola--;

 zamaluj_trafione_pole(trafione_pole);

  if(trafiony_statek.plywajace_pola==0){
 
    narysuj_obwodke_zatopionego_statku(trafiony statek, Numer_gracza);
 
    liczba_plywajacych_statkow = zmniejsz_liczbe_statkow(Numer_gracza);

  }
 
 return liczba_plywajacych_statkow;

}

 int trafilo(pole strzal, int Numer_gracza){

 pole trafione_pole =  znajdz_pole_statku(pole strzal, int Numer_gracza);

 int liczba_plywajacych_statkow;

 if(trafione_pole){

   liczba_plywajacych_statkow = usun_pole_statku(trafione_pole, Numer_gracza);

   return liczba_plywajacych_statkow;

}

 else{

  liczba_plywajacych_statkow = zwroc_liczbe_plywajacych_statkow_gracza(int Numer_gracza);

  return liczba_plywajacych_statkow;

}

Co dokładnie zmieniliśmy? Po pierwsze – wszelkie nazwy zmiennych czy metod jednoznacznie określają ich cel oraz powód powstania. Przykładowo liczba1 oraz liczba2 to obecnie liczba_statkow_pierwszego_gracza oraz liczba_statkow_drugiego_gracza. Po drugie metoda strzal_gracza (dawniej „trafienie”) została skutecznie  skrócona dzięki dwóm rozwiązaniom: część kodu została przeniesiona do innych, nowych metod, a nadmiar kodu (poprzednio if(gracz==1) … else powodowało implementację niemal identycznego kodu – jedyną różnicą była plansza odpowiedniego gracza) został zlikwidowany dzięki przekazywaniu zmiennej Numer_gracza do innych metod.

Formatowanie

Poprawne formatowanie można porównać do spoiwa, które scala wszystkie pozostałe zasady czystego kodu. Tak jak w kontaktach z ludźmi, tak i w programowaniu, ważne jest pierwsze wrażenie.  Wyobraźcie sobie, że potencjalny pracodawca przegląda Wasze projekty. Jeżeli na pierwszy rzut oka kod będzie wydawał się nieczytelny a kolejne linie będą się zlewały w całość, prawdopodobnie zrezygnuje on z dalszego wczytywania się w program. Część ludzi może założyć, że również od strony technicznej projekt nie jest dopracowany, skoro nie zadbaliście o estetyczne sformatowanie kodu.

Widoczne odstępy pomiędzy segmentami kodu służą do zakomunikowania, że zarówno pod, jak i nad pustym wierszem znajdują się różne elementy programu. Spójrzmy na poniższy przykład w języku Java.

 package pl.wat.ships.model;
 import java.util.Set;
 import java.util.TreeSet;
 public class Ship {
    private Size shipSize;
    private Set<Block> blocks;
    public Ship(Size size, Set<Block> blocks) {
       super();
       this.shipSize = size;
       this.blocks = new TreeSet<>(blocks);}
    public Size getShipSize() {
       return shipSize;}
    public Set<Block> getBlocks() {
       return blocks;}
    public boolean isDestroyed() {
       for (Block block : blocks) {
          if (!block.isDestroyed()) {
             return false;}}
       return true;
    }
 }

Nie patrzy się na to dobrze, prawda? Teraz dokonajmy drobnych poprawek. W środowisku IntelliJ IDEA wystarczy zaznaczyć wybrany blok i wcisnąć CTRL+ALT+L. Istnieją również narzędzia do formatowania online.

 package pl.wat.ships.model;
 
 import java.util.Set;
 import java.util.TreeSet;
 
 public class Ship {

    private Size shipSize;
    private Set<Block> blocks;

    public Ship(Size size, Set<Block> blocks) {
       super();
       this.shipSize = size;
       this.blocks = new TreeSet<>(blocks);
    }

    public boolean isDestroyed() {
       for (Block block : blocks) {
          if (!block.isDestroyed()) {
             return false;
          }
       }
       return true;
    }
 }

Chyba każdy zgodzi się, że wprowadzenie kilku pustych wierszy w tym przypadku jest zbawienne dla czytelności klasy.

Ważna jest również dbałość o czytelność powiązań między funkcjami, czy też dziedziczenia klas. Najprościej, funkcje, które razem tworzą odrębną, mniejszą całość, powinny znajdować się w małych odległościach od siebie, absolutnie nie mogą znajdować się w innych plikach źródłowych. Co więcej funkcja wywołująca powinna być umieszczona powyżej funkcji, którą wywołuje. Jest to zgodne z naturalnym przebiegiem programu.

Nie należy także zapominać o długości wierszy. Popularne są ograniczenia do 80 lub 120 znaków w linii. Czasem lepiej jedną porządną linię rozdzielić na kilka mniejszych (o ile jest to możliwe), ponieważ długie „kobyły” mogą odstraszać.

Kolejnym istotnym aspektem dotyczącym formatowania kodu jest budowanie wcięć. Robi się to według pewnej hierarchii. Przyjmuje się, że deklaracje klas nie są wcinane. Metody w klasach wcinane są o jeden poziom w prawo od klasy. Implementacje tych metod umieszcza się jeden poziom w prawo od ich deklaracji, i tak dalej.  Poniżej znajduję się przykład, w którym nie zastosowano wcięć.

 package pl.wat.ships.model;

 import java.util.HashSet;
 import java.util.Set;
 
 public class Battlefield {

   private Block[][] blocksGrid;
   public Set<Ship> shipsSet;

  public Battlefield() {
    shipsSet = new HashSet<>();
    blocksGrid = new Block[10][10];
  }
 
  public boolean hasAliveShips() {
    for (Ship ship : shipsSet) {
      if (!ship.isDestroyed()) {
         return true;
      }
    }
    return false;
  }
 }

A teraz ta sama klasa, ale z poprawnie sformatowanym kodem.

 package pl.wat.ships.model;

 import java.util.HashSet;
 import java.util.Set;
 
 public class Battlefield {

    private Block[][] blocksGrid;
    public Set<Ship> shipsSet;

    public Battlefield() {
       shipsSet = new HashSet<>();
       blocksGrid = new Block[10][10];
    }
 
    public boolean hasAliveShips() {
       for (Ship ship : shipsSet) {
          if (!ship.isDestroyed()) {
             return true;
          }
       }
       return false;
    }
 }

Wcięcia oczywiście możemy pomijać, tak jak w tym przypadku. Krótkie funkcje, jak get, czy set, aż proszą się o  zwinięcie.

 public Set<Block> getBlocks() {return blocks;}

Na koniec pragniemy wspomnieć o pojedynczych spacjach wokół operatorów ( ‘=’, ‘+’, ‘<’ , itp.), do których sami długo nie przywiązywaliśmy uwagi. Różnicę ładnie widać na przykładzie pętli for.

 for(i=0,i<100,i++)

Powyżej wszystko zlewa się w całość, co nie jest pożądanym efektem.

 for (i = 0, i < 100, i++) (tak jest dobrze 😊)

Obsługa błędów

Poruszaliśmy już temat funkcji zwracających wartości boolowskie, wiemy także doskonale, że nie jest to najlepsza metoda. Znacznie utrudnia czytanie, a przede wszystkim szybkie zrozumienie kodu. Jeśli chcemy by nasz kod stał się prozą, lub przynajmniej zbliżył się do bycia arcydziełem musimy stosować wyjątki. Ponieważ nie jest to kurs dla początkujących programistów, nie będziemy omawiać czym one są dokładnie, jedynie przypomnimy ich funkcje.

W przypadku błędów lub specyficznych sytuacji funkcja może rzucić wyjątek. W przypadku języka Java, po wystąpieniu wyjątku zrzucane są argumenty funkcji ze stosu, i powraca się do wykonywania kodu, który wywołał daną metodę. Podobnie jest w innych językach, lecz często rzucanie wyjątku różni się składnią np. throw new Exception(). Kolejną ważną kwestia w przypadku Clean Code jest tworzenie własnych wyjątków oraz ich nazwy, dlatego przed dalszą lekturą polecamy zapoznać się z dziedziczeniem po klasie Exception. Spójrzmy na przykład, aby się przekonać jak istotny to temat.

Public class KontrolerUrzadzenia{

…

 Public void Wylacz{

    Urzadzenie_Sterownik urzadzenie=Wczytaj_Funkcje_Sterowania_Urzadzeniem(DEV1);

    If(urzadzenie!=Urzadzenie.INVALID){

       ZapiszStan(urzadzenie)

    If(urządzenie.SprawdzStan ()!=DEVICE_SUSPENDED){

       Zatrzymaj (urzadzenie);

    Wyczysc_Kolejke_Urzadzen_Pracujacych(urządzenie);

  Zamknij(Urzadzenie)

}else{

   Logger.log(„Urządzenie wstrzymane”);

}else{

   Logger.log(„Niewłaściwy uchwyt”);

  }

}

….

}



}



 Public class KontrolerUrzadzenia{

   …

 Public void Wylacz_Urzadzenie(){

   Try{

     Sprobuj_Wylaczyc_Urzadznie();

   }catch(Blad_Wylaczania_Urzadzenia e){

    Logger.log(e);

    }

 }



  Private void Sprobuj_Wylaczyc_Urzadznie() throws Blad_Wylaczania_Urzadzenia {

  Urzadzenie_Sterownik urzadzenie = Wczytaj_Funkcje_Sterowania_Urzadzeniem (DEV1);

  Urządzenie.ZapiszStanUrzadzenia();

  ZatrzymajUrządzenie(handle);

  Wyczysc_Kolejke_Urzadzen_Pracujacych(handle);

  Zamknij_Urządzenie(handle);



}

Private DeviceHandle Wczytaj_Funkcje_Sterowania_Urzadzeniem (ID_Urzadzenia){

  …

  Throw Blad_Wylaczania_Urzadzenia („Niewłasciwy handler”)

  …

}

 ….

}

 

 

 

Klauzulę try, catch oraz finally są w pewnym sensie realizacją pojęcia jakim jest transakcja. Po wykonaniu się kodu, zawsze pozostaje on w stanie spójnym, i nie naruszone jest jego działanie. Dlatego jeśli chcemy aby nasz kod był stabilny zacznijmy pisanie zawsze od obsługi wyjątków.

 

Kolejną istotną rzeczą jest używanie wyjątków tak aby inny programista wiedział co on oznaczają i łatwo się czytało ich strukturę. Spójrzmy na przykład:

 

 Try{

     ….

     Aplikacja.Wylacz_Aplikacje();

 }catch(Blad_Portu_1 e){

    Logger.log(„Blad portu 1:”+e);
 
 } catch(Zatrzymania e){

   Logger.log(„Zatrzymania:”+e);

} catch(Blad_Zamkniecia_Bazy_Danych e){

   Logger.log(„Nie zakończono poprawnie polaczenia z Baza dancyh:”+e);

}

 …..

 Finally{

  ….

  }

 

 

Jak widzimy pomimo użycia wielu wyjątków, które mają sensowne nazwy kod jest nie czytelny. „Obudujmy ” zatem nasz wyjątek.

 

Try{

   Aplikacja.Wylacz_Aplikacje();



 }catch(Blad_Zamkniecia_Aplikacji e){

    Logger.log(e);

 }finally{

   ….

 }

Public class Aplikacja{

  …..

  Try{

    This.Zamknij();
 
  } catch(Blad_Portu_1 e){

     Throw new Blad_Zamniecia_Aplikacji(„Blad portu 1:”+e);

   } catch(Zatrzymania e){

     Throw new Blad_Zamniecia_Aplikacji („Zatrzymania:”+e);

   } catch(Blad_Zamkniecia_Bazy_Danych e){

       Throw new Blad_Zamniecia_Aplikacji („Nie zakończono poprawnie polaczenia z Baza danych:”+e);

 }
}

Ostatnią kwestią dotyczącą wyjątków jest wartość NULL. Postarajmy się jej nie zwracaćj, a zamiast tego używać wyjątków –  kod będzie znacznie czytelniejszy i ograniczymy ryzyko wystąpienia błędu. Brak jednego testu zwracanej wartości NULL przez funkcję będzie miało opłakane skutki. Dlatego programując pamiętajmy aby ograniczyć występowanie wyjątku NullPointerException().

Źródła

[1] „Czysty kod. Podręcznik dobrego programisty” R. C. Martin’a

Żródła obrazów: freeimages.com i opracowanie własne

 

Leave a Comment

Your email address will not be published. Required fields are marked with *

Cancel reply

Inne artykuły