Policz swój kod

metryki oprogramowania

Tomasz Kołodziej

Ocena jakości kodu

<?php

namespace Dbr\WebService\User;

class SoapClient implements Client
{
    use LoggerAwareTrait;
    use LoggerTrait;

    /**
     * @var \SoapClient
     */
    private $soapClient;

    /**
     * @var EventDispatcherInterface
     */
    private $eventDispatcher;

    /**
     * @param \SoapClient $soapClient
     * @param EventDispatcherInterface $eventDispatcher
     */
    public function __construct(
        \SoapClient $soapClient,
        EventDispatcherInterface $eventDispatcher
    ) {
        $this->soapClient = $soapClient;
        $this->eventDispatcher = $eventDispatcher;
    }

    /**
     * {@inheritdoc}
     */
    public function registerUser(User $user)
    {
        return $this->soapCall('registerUser', [
            'user' => $user
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function registerExternalUser(User $user)
    {
        return $this->soapCall('registerExternalUser', [
            'user' => $user
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function disconnectFromFacebook(User $user)
    {
        return $this->soapCall('disconnectExternalUser', [
            'login' => (string) $user->getEmail(),
            'extSystemType' => "Facebook",
            'extSystemId' => (string) $user->getFacebookId()
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function connectUserWithFacebook(User $user)
    {
        return $this->soapCall('connectExternalUser', [
            'login' => (string) $user->getEmail(),
            'extSystemType' => self::EXTERNAL_SYSTEM_TYPE_FACEBOOK,
            'extSystemId' => (string) $user->getFacebookId(),
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function findExternalUser(FacebookId $facebookId)
    {
        try {
            return $this->soapCall('getExternalUser', [
                'login' => null,
                'extSystemType' => self::EXTERNAL_SYSTEM_TYPE_FACEBOOK,
                'extSystemId' => (string) $facebookId,
            ]);
        } catch (\SoapFault $exception) {
            return null;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function findUserByEmail(Email $email)
    {
        try {
            return $this->soapCall('getUserByLogin', [
                'login' => (string) $email
            ]);
        } catch (\SoapFault $exception) {
            return null;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function findUserByEmailAndPassword(Email $email, $password)
    {
        return $this->soapCall('getUserByLoginAndPassword', [
            'login' => (string) $email,
            'password' => $password
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function requestResetPasswordToken(Email $email)
    {
        try {
            $token = $this->soapCall('createPasswordRenewToken', [
                'login' => (string) $email
            ]);

            return new Token($token);
        } catch (\SoapFault $exception) {
            switch($exception->getMessage()) {
                case 'User with given login does not exists':
                    throw new UserNotExistsException();
                    break;
                default:
                    $this->error(sprintf("Fault code: %s", $exception->getCode()));
                    $this->error(sprintf("Fault message: %s", $exception->getMessage()));
                    throw $exception;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function findUserByPasswordToken(Token $token)
    {
        try {
            return $this->soapCall('getUserByPasswordToken', [
                'token' => (string) $token
            ]);
        } catch (\SoapFault $exception) {
            return null;
        }

    }

    /**
     * {@inheritdoc}
     */
    public function changeUserPassword(User $user, User\Password $oldPassword, User\Password $newPassword)
    {
        try {
            return $this->soapCall('updateUser', [
                'UpdateSoapUser' => [
                    'password' => (string) $newPassword,
                    'old_password' => (string) $oldPassword
                ],
                'login' => (string)$user->getEmail(),
            ]);
        } catch (\SoapFault $exception) {
            switch ($exception->getMessage()) {
                case 'The two given tokens do not match;Field password_token is required':
                    throw new InvalidOldPasswordException();
                    break;
                default:
                    throw $exception;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function changePasswordUsingToken(Email $email, Token $token, User\Password $password)
    {
        return $this->soapCall('changePasswordUsingToken', [
            'login' => (String) $email,
            'password_token' => (string) $token,
            'password' => (string) $password
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function activateUser(Email $email, Token $token)
    {
        try {
            $result = $this->soapCall('activateUser', [
                'login' => (String) $email,
                'token' => (String) $token
            ]);

            if (null === $result) {
                throw new UserNotExistsException();
            }

            return $result;

        } catch (\SoapFault $exception) {
            switch($exception->getMessage()) {
                case 'The specified token is not valid':
                    throw new TokenInvalidException();
                    break;
                case 'This user is already activated':
                    throw new UserAlreadyActivatedException();
                    break;
                default:
                    $this->error(sprintf("Fault code: %s", $exception->getCode()));
                    $this->error(sprintf("Fault message: %s", $exception->getMessage()));
                    throw $exception;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function removeUser(User $user)
    {
        try {
            return $this->soapCall('unregisterUser', [
                'login' => (string) $user->getEmail()
            ]);
        } catch (\SoapFault $exception) {
            return false;
        }
    }

    /**
     * {@inheritdoc}
     */
    public function updateUserBillingData(User $user)
    {
        $companyBillingBlueprintCleaner = new Company(new Email((string)$user->getEmail()), new Address());

        $result = $this->soapCall('updateUser', [
            'UpdateSoapUser' => ($user->getBillingDataBlueprint()->forCompany())
                ? ['billing_data' => $user->getBillingDataBlueprint()]
                : [
                    'contact_data' => $user->getBillingDataBlueprint(),
                    'billing_data' => $companyBillingBlueprintCleaner
                ],
            'login' => (String) $user->getEmail()
        ]);

        if (!$result) {
            throw new IncorrectUserUpdateException();
        }

        return $user;
    }

    /**
     * {@inheritdoc}
     */
    public function updateUserContactData(User $user)
    {
        $result =  $this->soapCall('updateUser', [
            'UpdateSoapUser' => ['contact_data' => $user->getContactDataBlueprint()],
            'login' => (String) $user->getEmail()
        ]);

        if (!$result) {
            throw new IncorrectUserUpdateException();
        }

        return $user;
    }

    /**
     * {@inheritdoc}
     */
    public function addCoTraveler(User $user, CoTraveler $coTraveler)
    {
        return $this->soapCall('addCoTraveler', [
            'login' => (String) $user->getEmail(),
            'coTravelerDto' => $coTraveler
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function updateCoTraveler(User $user, CoTraveler $coTraveler)
    {
        return $this->soapCall('updateCoTraveler', [
            'login' => (String) $user->getEmail(),
            'coTravelerDto' => $coTraveler
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function getCoTravelers(User $user)
    {
        return $this->soapCall('getCoTravelers', [
            'login' => (String)$user->getEmail(),
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function registerUserSearch(User $user, Record $record)
    {
        return $this->soapCall('registerUserSearch', [
            'login'         => $user->getEmail(),
            'userSearchDto' => $record
        ]);
    }

    /**
     * @param User $user
     * @param int $searchFor
     * @param int $limit
     * @param int $offset
     * @return Record[]
     */
    public function getUserSearchHistory(User $user, $searchFor = self::SEARCH_ALL, $limit = 25, $offset = 0)
    {
        return $this->soapCall('getUserSearches', [
            'login' => (string)$user->getEmail(),
            'searchesGroup' => $searchFor,
            'limit' => $limit,
            'offset' => $offset
        ]);
    }

    public function getUserSearchHistoryCount(User $user, $searchFor = self::SEARCH_ALL)
    {
        return $this->soapCall('getUserSearchesTotalCount', [
            'login' => (string)$user->getEmail(),
            'searchesGroup' => $searchFor,
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function removeCoTraveler(User $user, CoTraveler\Id $id)
    {
        try {
            return $this->soapCall('deleteCoTraveler', [
                'login' => (String) $user->getEmail(),
                'coTravelerId' => (int) $id->getValue()
            ]);

        } catch (\SoapFault $exception) {
            switch($exception->getMessage()) {
                case 'Co-traveler with given ID does not exists':
                    throw new CoTravelerNotFoundException();
                    break;
                default:
                    $this->error(sprintf("Fault code: %s", $exception->getCode()));
                    $this->error(sprintf("Fault message: %s", $exception->getMessage()));
                    throw $exception;
            }
        }
    }

    /**
     * {@inheritdoc}
     */
    public function findCoTraveler(User $user, CoTraveler\Id $id)
    {
        try {
            return $this->soapCall('getCoTraveler', [
                'login' => (String) $user->getEmail(),
                'coTravelerId' => (int) $id->getValue()
            ]);
        } catch (\SoapFault $exception) {
            return null;
        }
    }

    /**
     * @param $endpoint
     * @param array $arguments
     * @throws \Exception
     * @throws \SoapFault
     * @return mixed
     */
    private function soapCall($endpoint, array $arguments = [])
    {
        try {
            $this->eventDispatcher->dispatch(Events::PRE_SOAP_CALL, new PreCallEvent($endpoint, $arguments));
            $this->debug(sprintf("Endpoint: %s", $endpoint));
            $this->debug("Arguments", ['args' => $arguments]);
            $result = $this->soapClient->__soapCall($endpoint, $arguments);
            $this->logLastApiCall();
            $this->triggerPostCallEvent();
        } catch (\SoapFault $e) {
            $this->error(sprintf("Fault code: %s", $e->getCode()));
            $this->error(sprintf("Fault message: %s", $e->getMessage()));
            $this->logLastApiCall();
            $this->triggerPostCallEvent();
            throw $e;
        } catch (\Exception $e) {
            $this->critical(sprintf("Exception message: %s", $e->getMessage()));
            $this->logLastApiCall();
            $this->triggerPostCallEvent();
            throw $e;
        }

        return $result;
    }

    /**
     * @param $level
     * @param $message
     * @param array $context
     */
    protected function log($level, $message, array $context = [])
    {
        if (is_null($this->logger)) {
            return ;
        }

        $this->logger->log($level, $message, $context);
    }

    private function triggerPostCallEvent()
    {
        $this->eventDispatcher->dispatch(Events::POST_SOAP_CALL, new PostCallEvent(
            $this->soapClient->__getLastRequestHeaders(),
            $this->soapClient->__getLastRequest(),
            $this->soapClient->__getLastResponseHeaders(),
            $this->soapClient->__getLastResponse()
        ));
    }

    private function logLastApiCall()
    {
        $this->debug(sprintf(
            "Last Request Headers: %s",
            preg_replace('/\s+/', ' ', $this->soapClient->__getLastRequestHeaders())
        ));
        $this->debug(sprintf(
            "Last Request: %s",
            preg_replace('/\s+/', ' ', $this->soapClient->__getLastRequest())
        ));
        $this->debug(sprintf(
            "Last Response Headers: %s",
            preg_replace('/\s+/', ' ', $this->soapClient->__getLastResponseHeaders())
        ));
        $this->debug(sprintf(
            "Last Response: %s",
            preg_replace('/\s+/', ' ', $this->soapClient->__getLastResponse())
        ));
    }
}

Recenzja - wyniki

  • Kod nie daje pewności co do poprawnego działania i może stwarzać zagrożenie...
     
  • Super klasa jest czytelna i widać co jest potrzebne...
     
  • ... duże nagromadzenie odpowiedzialności oraz duże zagnieżdżenie instrukcji warunkowych...
     
  • ... it has toooooo many public methods
     
  • Na pierwszy rzut oka wydaje mi sie ze zostaly tu zlamane zasady SOLID.

łącznie 4 strony tekstu

(1080 słów)

Jeśli to o czym mówisz potrafisz zmierzyć i wyrazić w liczbach – wiesz coś o tym. Inaczej twa wiedza jest mizerna

 Lord Kelvin

Liczby

  • Da się je porównać
     
  • Można je agregować (sumować, uśredniać)
     
  • Można je wizualizować
     
  • Są zrozumiałe dla wszystkich
     
  • O ile wiemy co oznaczają ... ;)

Metryka oprogramowania

miara pewnej własności oprogramowania lub jego specyfikacji. Termin ten nie ma precyzyjnej definicji i może oznaczać właściwie dowolną wartość liczbową charakteryzującą oprogramowanie

Metryki - wyniki

  • Logical lines of code: 106
     
  • Efferent coupling: 22
     
  • Lack of cohesion of methods: 2
     
  • Vocabulary: 147

  • Cyclomatic complexity: 24

Metryki rozmiaru

  • Ilość linii kodu (25)
     
  • Ilość logicznych linii kodu (5)
     
  • Ilość klas / metod (1/2) 
     
  • Średnia długość klasy / metody (5/2)
     
  • Ilość linii wymagań klienta
    (zawsze za dużo)
<?php
/**
 * Created by PhpStorm.
 * User: tkolodziej
 * Date: 2015-07-21
 * Time: 22:14
 */

class Foo
{
    private $state;

    public function __construct($state)
    {
        $this->state = $state;
    }

    public function bar($arg)
    {
        $result = $this->state + $arg;
        $this->state = $result;

        return $result;
    }
}

Metryki Halsteada

  • N1: całkowita liczba wystąpień operatorów
  • N2: całkowita liczba wystąpień operandów.
  • η1: liczba unikatowych operatorów
  • η2: liczba unikatowych operandów
     
  • N = N1 + N2: długość (length): 
  • η = η1 + η2: słownictwo (vocabulary)
  • V = N * log2(η): objętość (volume)

Metryki Halsteada - przykład

  • N1 = 13 
  • N2 = 10
  • η1 = 8 ( "public", "function", "return", "()", "{}", "=", "+", "->", ";" )
  • η2 = 5 ( "bar", "$arg", "$result", "$this", "state" )
    public function bar($arg)
    {
        $result = $this->state + $arg;
        $this->state = $result;

        return $result;
    }

Czy rozmiar ma znaczenie ?

Metryki Halsteada - złożoność

  • trudność (difficulty)

     
  • wysiłek (effort)

     
  • czas (time) potrzebny do zaimplementowania w sekundach

     
  • szacunkowa liczba błędów (bugs)
D = {\eta1 \over 2} \times {N2 \over \eta2}
D=η12×N2η2D = {\eta1 \over 2} \times {N2 \over \eta2}
E = V \times D
E=V×DE = V \times D
T = {E \over 18}
T=E18T = {E \over 18}
B = {E^{2 \over 3} \over 3000}
B=E233000B = {E^{2 \over 3} \over 3000}

Zgłożoność cyklomatyczna McCabe'a

Złożoność cyklomatyczna bloku kodu to liczba niezależnych ścieżek w grafie reprezentującym przepływ sterowania w metodzie

1 - 4 - niska złożoność

5 - 7 - średnia złożoność

6 - 10 - wysoka złożoność

powyżej 10 - bardzo wysoka złożoność

Regions

4

Edges - Nodes + 2

12 - 10 + 2 = 4

Decisions - Exit points + 2

3 - 1 + 2 = 4

Jak policzyć ?

Dobre...

ale dla procedur i algorytmów

Metryki programowania obiektowego

  • Depth of Inheritance Tree (DIT)
     
  • Number of Children (NOC)
     
  • Coupling Between Objects (CBO)
     
  • Response For a Class (RFC)
     
  • Lack of Cohesion of Methods (LCOM)

Architektura ??

Narzędzia

PHPDepend

Wykres Abstracness / Instability

Ca - Afferent Coupling (Sprzężenia dochodzące)

I = Ce / (Ca + Ce) - Instability ( Niestabilność )

Ce - Efferent Coupling (Sprzężenia odchodzące)

Na - Ilość klas abstrakcyjnych i interfejsów

Nc - Ilość klas konkretnych (nieabstrakcyjnych)

A = Na / Nc - Abstractness ( Abstrakcyjność ) 

PHPDepend

Wykres Abstracness / Instability

A w praktyce...

PHPDepend - Overview piramid

PhpMetrics

Mainatanability index

MAX(0,(171 - 5.2 * ln(Halstead Volume) - 0.23 * (Cyclomatic Complexity) - 16.2 * ln(Lines of Code))*100 / 171)

PhpMetrics - report

Do czego używać metryk ?

  • Jednorazowa ocena jakości
     
  • Stały pomiar - wyznacznie celów

Jakość metryk

  • prosta i możliwa do obliczenia przez komputer
  • przekonująca
  • konsekwentna i obiektywna
  • niezależna od języka programowania
  • dająca przydatne informacje

Czy metryki da się oszukać ?

tak... ale po co ?

PHPMetrics - w znanych projektach

ZF2

Symfony2

Laravel

Wordpress

Magento

Pytania ?

Policz swój kod - metryki oprogramowania

By Tomasz Kołodziej

Policz swój kod - metryki oprogramowania

Czy można liczbowo ocenić jak skomplikowany jest kod i czy kryją się w nim potencjalne problemy? Metryki oprogramowania dają nam tą możliwość. Pozwalają ocenić i porównać duże systemy w krótkim czasie. Opowiem o najczęściej stosowanych metrykach oraz zaprezentuję narzędzia do analizy oraz wizualizacji.

  • 607