Skracanie kodu, a czytelność

0

Witam,

Zastanawia mnie co myślicie o skracaniu kodu. Tzn. jak to wpływa na czytelność. Przykładowo dla mnie nieraz wygodniej jest czytać dosyć skrócony kod, ale dopóki nie osiągnie poziomu WTF.

Krótki przykład:

return (m_NameHash != 0) ? m_NameHash : (m_NameHash = std::hash<const wchar_t*>()(getName()));

Równie dobrze mógłbym zakodzić to za pomocą paru if'ów, ale ta wersja jest krótsza i jeszcze IMHO nie powoduje WTF.

A jak jest według was? Czy ten pokazany powyżej fragment kodu byłby według was czytelniejszy przy paru if'ach, czy teraz jest lepiej? I czy krótszy kod według was łatwiej się czyta? Wiem, że to zależy od sytuacji (np. jakiś trudnych obliczeń bym sobie tak nie skracał, by potem zachodzić w głowę gdzie coś jest nie tak), ale w prostych sprawach jak ta powyżej to chyba jest ok?

0

Na pewno czytelniej by było to zapisać za pomocą ifów. Wyrażenie warunkowe czasem potrafi nieźle zobfuscować kod

0

Kod z notacją węgierską jest zły, bez względu na to jak został napisany.

0

Każdy pisze tak by mu było wygodnie.

Akurat w tym przypadku wg mnie jest ok.

0

Kod z notacją węgierską jest zły, bez względu na to jak został napisany.

Co jest złego w notacji węgierskiej? Ja rozumiem, że jakieś dziwne twory, które powstały w nazwach typów w WinAPI to jest zło, ale tutaj chyba nie ma problemu. Zmienne składowe klasy poprzedzone "m_" (w C# stosuje tylko "_", w C++ "m" jest po to by nie było jakiegoś konfliktu nazw przypadkiem) i dzięki temu m.in. podpowiadajka lepiej działa (przynajmniej dla mnie).
Rozwiń, jeśli możesz, co jest według Ciebie złego w notacji węgierskiej.

Każdy pisze tak by mu było wygodnie.

Zależy mi też by kod nie powodował drapania się po głowie u kogoś innego, kto czytałby kod ;)

0
skracanie_kodu napisał(a):

Co jest złego w notacji węgierskiej?

Co znaczy "m_" w nazwie zmiennej m_NameHash? Jeśli nic to nie powinno tego być. Od tego żeby nie było problemów z nazwami zmiennych są namespace'y.
Jak w ogóle przeczytać na głos nazwę zmiennej m_NameHash?

0
Igor1981 napisał(a):
skracanie_kodu napisał(a):

Co jest złego w notacji węgierskiej?

Co znaczy "m_" w nazwie zmiennej m_NameHash? Jeśli nic to nie powinno tego być. Od tego żeby nie było problemów z nazwami zmiennych są namespace'y.
Jak w ogóle przeczytać na głos nazwę zmiennej m_NameHash?

To standard kodowania w C++ powszechnie stosowany w wielu opublikowanych standardach. Kto o to pyta widać że nie zna C++.

Pierwszy z brzegu:
http://www.yolinux.com/TUTORIALS/LinuxTutorialC++CodingStyle.html
Inne:
http://www.macadamian.com/insight/best_practices_detail/coding_conventions_for_c_and_java
https://developer.mozilla.org/en-US/docs/Developer_Guide/Coding_Style#Classes (tu bez podkreślenia)

@skracanie_kodu:
A co do skracania kodu to jest takie fajne hasło które wprawdzie odnosi się do projektowania stron WWW, ale też sprawdza się w kodzie "don't make me think!".
Programy piszesz dla dwóch odbiorców:

  • dla komputerów - żeby wiedziały co mają zrobić - dla nich wystarczyłby kod maszynowy / hex
  • dla ludzi - którzy będą konserwować Twoje oprogramowanie

Jeśli pytasz o to czy można pisać nieczytelnie kod to zapominasz o tej drugiej części.
Niektórzy robią to świadomie (poszukaj "how to write unmaintable code"), niektórzy mają jakieś wydumane widzi-misie, inni robią to z lenistwa.
Ja zawsze staram się kod upraszczać, mimo że przez to czasami komputer wykona parę cykli więcej.

0
Igor1981 napisał(a):
skracanie_kodu napisał(a):

Co jest złego w notacji węgierskiej?

Co znaczy "m_" w nazwie zmiennej m_NameHash? Jeśli nic to nie powinno tego być. Od tego żeby nie było problemów z nazwami zmiennych są namespace'y.
Jak w ogóle przeczytać na głos nazwę zmiennej m_NameHash?

W poście powyżej napisałem, kiedy to stosuje. Na podstawie kodów, które widziałem, wydaje mi się, że jest to dosyć często używana konwencja nazewnictwa.

1

Co do notacji węgierskiej:
http://ootips.org/hungarian-notation.html
http://stackoverflow.com/questions/111933/why-shouldnt-i-use-hungarian-notation (wbrew pozorom całkiem rzeczowa dyskusja, nie tylko `czemu nie).

Jeśli chodzi o jedyne jej sensowne (i oryginalne) zastosowanie, poczytaj http://www.joelonsoftware.com/articles/Wrong.html.
Później ludzie (tak, ludzie w MS przede wszystkim) zaczęli tego baardzo nadużywać...

W poście powyżej napisałem, kiedy to stosuje. Na podstawie kodów, które widziałem, wydaje mi się, że jest to dosyć często używana konwencja nazewnictwa.

Co nie znaczy że dobra... Ale co kto lubi.

No ale nie o tym było pytanie.
Najlepszy kod, to kod którego nie ma, to jasne. Tylko nie można przesadzać z tym skracaniem z drugiej strony.

Dalej, jeśli już skracasz to porządnie ;]. To m_ tylko wszystko wydłuża, tak samo (x != 0) można potraktować jako idiom:

return (m_NameHash != 0) ? m_NameHash : (m_NameHash = std::hash<const wchar_t*>()(getName()));

Zamiast tego:

return nameHash ? nameHash : (nameHash = std::hash<const wchar_t*>()(getName()));

Mimo wszystko, chyba lepiej tu dać if-a, nie jest niby źle ale tak będzie trochę czytelniej (gdyby nie to że z operatorem || nie zadziała w C++, można by też tak kombinować).

0

Jeśli pytasz o to czy można pisać nieczytelnie kod

Wydaje mi się, że skracanie póki kod jest czytelny jest ok. Tak właściwie to właśnie chciałem się dowiedzieć mniej więcej na jakim poziomie jest czytelność pokazanego wyżej kodu (dla mnie on jest niedaleko granicy której nie-wolno-przekraczać).

msm napisał(a):

Dalej, jeśli już skracasz to porządnie ;]. To m_ tylko wszystko wydłuża, tak samo (x != 0) można potraktować jako idiom:

return (m_NameHash != 0) ? m_NameHash : (m_NameHash = std::hash<const wchar_t*>()(getName()));

Zamiast tego:

return nameHash ? nameHash : (nameHash = std::hash<const wchar_t*>()(getName()));

Po tym okrojeniu kod wydaje mi się już sporo mniej czytelny. Bo: 1) lubię jak warunek jest w nawiasie - jakoś łatwiej mi się to czyta. 2) Wolę po samej nazwie widzieć, czy zmienna jest lokalna, globalna, czy jest zmienna obiektu itd. Notacja jakiej używam jasno mi to mówi (mimo że nie daje literki do każdej własności - po prostu wiem kiedy i jak nazywam zmienne i się tego trzymam. Jak robię jakąś zmianę konwencji, to albo poprawiam kod wszędzie, albo nie robię tego - nie chcę mieć burdelu).

Btw. Dzięki za odpowiedzi. Chyba jednak będę pisał kod nieco dłuższy, ale tak by nie było żadnych wątpliwości, co on robi (bo jeśli ktoś mało osiedziany w C++ zobaczy takie "cudo", to mógłby się wystraszyć) ;)

0
if (!nameHash)
    nameHash = std::hash<const wchar_t*>()(getName());
return nameHash;

IMHO wcale nie jest dłuższe, a jest znacznie czytelniejsze. Mogłoby być jeszcze krótsze, gdyby C++ miał lepszą inferencję typów i dałoby się pominąć <> jak w Javie.

Czego nie lubię w kodzie:

  1. wciskania na siłę efektów ubocznych w wyrażenia, które wyglądają jak funkcje, np. w warunkach albo właśnie w :?
  2. zbyt dużej liczby zagnieżdżeń (dopuszczam maks. 3 poziomy)
  3. zbyt dużej liczby parametrów do funkcji, znowu maks. 3, ale to też już ostateczność
  4. zbyt długich funkcji / metod, przy czym im więcej zagnieżdżeń, tym limit ten jest mniejszy - dla mocno zagnieżdżonego kodu maks. 10 linii.
  5. skomplikowanego przepływu sterowania, np. while, w nim pierdyliard warunków a w połowie break lub continue
  6. mieszania różnych poziomów abstrakcji w tej samej metodzie

Powyższe rzeczy są ważniejsze niż sumaryczna długość kodu. Wolę kod dłuższy o dobrze przemyślanej strukturze, niż jeden wielki blob spaghetti, nawet jeśli jest krócej.

A, takiego kodu nie cierpię:

public synchronized void updateTaskStatus(TaskInProgress tip, 
                                            TaskStatus status) {

    double oldProgress = tip.getProgress();   // save old progress
    boolean wasRunning = tip.isRunning();
    boolean wasComplete = tip.isComplete();
    boolean wasPending = tip.isOnlyCommitPending();
    TaskAttemptID taskid = status.getTaskID();
    boolean wasAttemptRunning = tip.isAttemptRunning(taskid);

    // If the TIP is already completed and the task reports as SUCCEEDED then 
    // mark the task as KILLED.
    // In case of task with no promotion the task tracker will mark the task 
    // as SUCCEEDED.
    // User has requested to kill the task, but TT reported SUCCEEDED, 
    // mark the task KILLED.
    if ((wasComplete || tip.wasKilled(taskid)) && 
        (status.getRunState() == TaskStatus.State.SUCCEEDED)) {
      status.setRunState(TaskStatus.State.KILLED);
    }
    
    // If the job is complete and a task has just reported its 
    // state as FAILED_UNCLEAN/KILLED_UNCLEAN, 
    // make the task's state FAILED/KILLED without launching cleanup attempt.
    // Note that if task is already a cleanup attempt, 
    // we don't change the state to make sure the task gets a killTaskAction
    if ((this.isComplete() || jobFailed || jobKilled) && 
        !tip.isCleanupAttempt(taskid)) {
      if (status.getRunState() == TaskStatus.State.FAILED_UNCLEAN) {
        status.setRunState(TaskStatus.State.FAILED);
      } else if (status.getRunState() == TaskStatus.State.KILLED_UNCLEAN) {
        status.setRunState(TaskStatus.State.KILLED);
      }
    }
    
    boolean change = tip.updateStatus(status);
    if (change) {
      TaskStatus.State state = status.getRunState();
      // get the TaskTrackerStatus where the task ran 
      TaskTracker taskTracker = 
        this.jobtracker.getTaskTracker(tip.machineWhereTaskRan(taskid));
      TaskTrackerStatus ttStatus = 
        (taskTracker == null) ? null : taskTracker.getStatus();
      String httpTaskLogLocation = null; 

      if (null != ttStatus){
        String host;
        if (NetUtils.getStaticResolution(ttStatus.getHost()) != null) {
          host = NetUtils.getStaticResolution(ttStatus.getHost());
        } else {
          host = ttStatus.getHost();
        }
        httpTaskLogLocation = "http://" + host + ":" + ttStatus.getHttpPort(); 
           //+ "/tasklog?plaintext=true&attemptid=" + status.getTaskID();
      }

      TaskCompletionEvent taskEvent = null;
      if (state == TaskStatus.State.SUCCEEDED) {
        taskEvent = new TaskCompletionEvent(
                                            taskCompletionEventTracker, 
                                            taskid,
                                            tip.idWithinJob(),
                                            status.getIsMap() &&
                                            !tip.isJobCleanupTask() &&
                                            !tip.isJobSetupTask(),
                                            TaskCompletionEvent.Status.SUCCEEDED,
                                            httpTaskLogLocation 
                                           );
        taskEvent.setTaskRunTime((int)(status.getFinishTime() 
                                       - status.getStartTime()));
        tip.setSuccessEventNumber(taskCompletionEventTracker); 
      } else if (state == TaskStatus.State.COMMIT_PENDING) {
        // If it is the first attempt reporting COMMIT_PENDING
        // ask the task to commit.
        if (!wasComplete && !wasPending) {
          tip.doCommit(taskid);
        }
        return;
      } else if (state == TaskStatus.State.FAILED_UNCLEAN ||
                 state == TaskStatus.State.KILLED_UNCLEAN) {
        tip.incompleteSubTask(taskid, this.status);
        // add this task, to be rescheduled as cleanup attempt
        if (tip.isMapTask()) {
          mapCleanupTasks.add(taskid);
        } else {
          reduceCleanupTasks.add(taskid);
        }
        // Remove the task entry from jobtracker
        jobtracker.removeTaskEntry(taskid);
      }
      //For a failed task update the JT datastructures. 
      else if (state == TaskStatus.State.FAILED ||
               state == TaskStatus.State.KILLED) {
        // Get the event number for the (possibly) previously successful
        // task. If there exists one, then set that status to OBSOLETE 
        int eventNumber;
        if ((eventNumber = tip.getSuccessEventNumber()) != -1) {
          TaskCompletionEvent t = 
            this.taskCompletionEvents.get(eventNumber);
          if (t.getTaskAttemptId().equals(taskid))
            t.setTaskStatus(TaskCompletionEvent.Status.OBSOLETE);
        }
        
        // Tell the job to fail the relevant task
        failedTask(tip, taskid, status, taskTracker,
                   wasRunning, wasComplete, wasAttemptRunning);

        // Did the task failure lead to tip failure?
        TaskCompletionEvent.Status taskCompletionStatus = 
          (state == TaskStatus.State.FAILED ) ?
              TaskCompletionEvent.Status.FAILED :
              TaskCompletionEvent.Status.KILLED;
        if (tip.isFailed()) {
          taskCompletionStatus = TaskCompletionEvent.Status.TIPFAILED;
        }
        taskEvent = new TaskCompletionEvent(taskCompletionEventTracker, 
                                            taskid,
                                            tip.idWithinJob(),
                                            status.getIsMap() &&
                                            !tip.isJobCleanupTask() &&
                                            !tip.isJobSetupTask(),
                                            taskCompletionStatus, 
                                            httpTaskLogLocation
                                           );
      }          

      // Add the 'complete' task i.e. successful/failed
      // It _is_ safe to add the TaskCompletionEvent.Status.SUCCEEDED
      // *before* calling TIP.completedTask since:
      // a. One and only one task of a TIP is declared as a SUCCESS, the
      //    other (speculative tasks) are marked KILLED by the TaskCommitThread
      // b. TIP.completedTask *does not* throw _any_ exception at all.
      if (taskEvent != null) {
        this.taskCompletionEvents.add(taskEvent);
        taskCompletionEventTracker++;
        JobTrackerStatistics.TaskTrackerStat ttStat = jobtracker.
           getStatistics().getTaskTrackerStat(tip.machineWhereTaskRan(taskid));
        if(ttStat != null) { // ttStat can be null in case of lost tracker
          ttStat.incrTotalTasks();
        }
        if (state == TaskStatus.State.SUCCEEDED) {
          completedTask(tip, status);
          if(ttStat != null) {
            ttStat.incrSucceededTasks();
          }
        }
      }
    }
        
    //
    // Update JobInProgress status
    //
    if(LOG.isDebugEnabled()) {
      LOG.debug("Taking progress for " + tip.getTIPId() + " from " + 
                 oldProgress + " to " + tip.getProgress());
    }
    
    if (!tip.isJobCleanupTask() && !tip.isJobSetupTask()) {
      double progressDelta = tip.getProgress() - oldProgress;
      if (tip.isMapTask()) {
          this.status.setMapProgress((float) (this.status.mapProgress() +
                                              progressDelta / maps.length));
      } else {
        this.status.setReduceProgress((float) (this.status.reduceProgress() + 
                                           (progressDelta / reduces.length)));
      }
    }
  }

Programiści, którzy za długo pracują z takim kodem cierpią na PHSD - Post-Hadoop Stress Disorder :D

0

To co najbardziej mnie rozsierdza to przekazywanie wyników z funkcji jakimś "bonycznymi kanałami" (typu modyfikacja zmiennej globalnej) zamiast normalnie przez interfejs. Ostatnio przez takie g**no straciłem sporo czasu i nerwów.
Co do pytania w temacie to wole stosować ify zamiast znaczki zapytania (chociaż ten kod jakoś strasznie nie wali po oczach).

0
0x200x20 napisał(a):

To co najbardziej mnie rozsierdza to przekazywanie wyników z funkcji jakimś "bonycznymi kanałami" (typu modyfikacja zmiennej globalnej) zamiast normalnie przez interfejs.

IMO zmienne globalne to zło. Funkcja powinna móc modyfikować tylko to do czego dałem jej bezpośredni dostęp i nic więcej. Ew. czasem można dać logger jako zmienną globalną (ale nie ma to prawa wpływać na działanie programu!).

0
skracanie_kodu napisał(a):
0x200x20 napisał(a):

To co najbardziej mnie rozsierdza to przekazywanie wyników z funkcji jakimś "bonycznymi kanałami" (typu modyfikacja zmiennej globalnej) zamiast normalnie przez interfejs.

IMO zmienne globalne to zło. Funkcja powinna móc modyfikować tylko to do czego dałem jej bezpośredni dostęp i nic więcej. Ew. czasem można dać logger jako zmienną globalną (ale nie ma to prawa wpływać na działanie programu!).

Zmienne globalne zmniejszają reużywalność funkcji. Tych czytających i piszących. Ustalają pewien niejawny kontekst który musi być zapewniony zanim wywołamy funkcję.
Kontekst można sobie wyczytać z kodu, chyba że kod ten jest baardzo długi.

0

Mały OT, zmienne globalne to zło, ale umiarkowane z porównaniu ze zmiennymi public i private z Clippera.
Wewnątrz dowolnej funkcji foo można użyć deklaracji

PUBLIC zm1
PRIVATE zm2

Zmienna zm1 jest widoczna wszędzie, ma więc charakter zmiennej globalnej, ale może nie istnieć - żeby istniała musi wpierw zostać wykonana funkcja foo.
Zmienna zm2 jest widoczna we wszystkich funkcjach wywołanych pośrednio lub bezpośrednio z funkcji foo i tylko w nich. Zatem istnienie tej zmiennej w konkretnej funkcji f zależy od stosu wywołań: main => ... => f.

0

Zmienne globalne to szatan wcielony, zło to zmienne instancyjne.
Wszystko tyczy się oczywiście nadużywania, bo czasem trzeba użyć, ale przemyśl, zanim użyjesz...

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