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