Ниже — разбор четырех интересных задачек с очередного собеседования. Это задача на проведение код-ревью, на создание индексов в БД, на кэширование данных и на парсинг конфиг-файла. Рекомендуется перед обзором решений, попробовать решить представленные задачи самостоятельно, затем сравнить ваше решение с моим.

/**/


Задача 1: код-ревью

Провести код-ревью: https://gist.github.com/MaxSem/b3014252d56dc2b7bf7653633f0e9fb5

<?php

class CurrencyConverter {
	private function connect_db ( $sql ){
		$conn = mysql_connect('host', 'user', 'password');
		mysql_select_db('database');
		$res=mysql_query($sql,$conn);
		mysql_close($conn);
		return $res;
	}
	public function get_rate ( $currency ) {
		$res = $this->connect_db("SELECT rate FROM rates WHERE currency = '$currency'");
		$row = mysql_fetch_array($res);
		return $row['rate'];
	}

	public function parse ( $input ) {
		$input = trim($input);
		$pos = strpos($input,' ');
		if( $pos != strrpos($input,' ')){
			return array ( 'error' => 'true', 'msg' => 'invalid input: '.$input );
		}
		return array ( 'error' => 'false', 'currency' => substr($input,0,$pos), 'amount' => substr($input,$pos+1) );
	}

	public function set_rate ( $currency, $rate ) {
		$currency = strtoupper($currency);
		$res = $this->connect_db("SELECT rate FROM rates WHERE currency = '$currency'");
		$row = mysql_fetch_array($res);
		if($row['rate']){
			$this->connect_db("UPDATE rates SET rate=$rate WHERE currency='$currency'");
		}else{
			$this->connect_db("INSERT INTO rates (currency,rate) VALUES ('$currency',$rate)");
		}
	}

	public function convert_to_usd ( $input ) {
		$result = array();
		if(gettype($input) == 'string'){
			$input = array( $input );
		}
		if(gettype($input) != 'array'){
			return array ( 'error' => 'true', 'msg' => 'invalid input' );
		}else{
			foreach( $input as $item){
				$item_parsed = $this->parse($item);
				$rate = $this->get_rate( $item_parsed['currency']);
				$usd = $rate * $item_parsed['amount'];
				array_push($result,'USD '.$usd);
			}
		}
		return $result;
	}

	public function convert_from_usd ( $currency, $amount) {
		function walk_amount (&$item,$key,$rate){
			$item = number_format($item / $rate, 2, '.', '');
		}	
		if(!$currency || !$amount){
			$return = array ( 'error' => 'true', 'msg' => 'invalid input argument' );
		}else{
			$rate = $this->get_rate( $currency );
			$amounts = explode(',',$amount);
			array_walk($amounts,'walk_amount',$rate);
			$return = array ( 'error' => 'false', 'content' => implode(',',$amounts));
		}
		return '{"error":'.$return['error'].',"content":"'.$return['content'].'"}';
	}
}

$converter = new CurrencyConverter;
echo $converter->convert_from_usd($_GET['currency'],$_GET['amount']);

// for testing each function:
echo $converter->get_rate($_GET['currency']);
echo $converter->set_rate($_GET['currency'],$_GET['rate']);

var_dump($converter->parse($_GET['input']));
var_dump($converter->convert_to_usd('JPY 10000'));
echo '<br />';

var_dump($converter->convert_to_usd(array('JPY 10000','AUD 2000','CZK 4000')));
?>

Решение:

Основное

  • не SOLID-ный код

    • Single responsebility: разбить класс на три надо (CurrencyConverter, CurrrencyRate, DbConnection)

  • соединение с БД поправить на mysqli / PDO

  • избавиться от хардкода в sql (биндинг использовать)

Все замечания (в т.ч. от нейронки Claude)

В целом код выглядит неплохо, но есть несколько замечаний:

  1. Использование устаревших функций MySQL без обработки ошибок (mysql_query, mysql_fetch_array и т.д.). Лучше использовать PDO или mysqli с проверкой результатов запросов.

  2. Отсутствует валидация и экранирование входных данных перед запросами к БД. Это создает уязвимости для SQL-инъекций.

  3. Нет обработки ошибок при получении данных из БД.

  4. Функция connect_db вызывается в каждом методе — лучше сделать подключение один раз при создании объекта.

  5. Желательно вынести работу с БД в отдельный класс для лучшего разделения логики.

  6. Имена методов и переменных не соответствуют стандартам именования в PHP (например, convert_to_usd вместо convertToUsd).

  7. В методах отсутствуют phpdoc комментарии с описанием параметров и возвращаемых значений.

  8. В целом класс довольно большой, лучше разбить на несколько классов по назначению.

Рекомендую:

  • Использовать PDO или mysqli для работы с БД

  • Добавить валидацию и экранирование входных данных

  • Обрабатывать ошибки базы данных

  • Вынести работу с БД в отдельный класс

  • Разбить класс на несколько по назначению

  • Добавить phpdoc комментарии

  • Придерживаться стандартов именования в PHP

Это поможет сделать код более безопасным, структурированным и поддерживаемым.


Задача 2: Индексы в базах данных

https://collabedit.com/d6bgj

CREATE TABLE x (
    a INT,
    b INT,
    INDEX(a, b)
);
  • Привести примеры, когда будет использоваться индекс?

  • Что будет если поменять ключи местами?

Решение

SELECT *
FROM x
WHERE a = 1 AND b = 2;
-- да, тут будет использоваться

-- будет ли использован индекс тут? 
-- не будет использоваться
SELECT * FROM x WHERE a > 1 AND b = 2

-- будет ли использован индекс тут?  
-- будет использоваться
SELECT * FROM x WHERE a = 1 AND b > 2

Задача 3: кэширование данных

В существующем коде есть ограничения на редактирование базовых классов.

Как добавить кэширование сюда, чтобы не запускать метод slow() каждый раз?

(с учетом ограничений на редактирование классов)

Решение

// Can't change
interface MyInterface {
    public function slow(): SomeData;
}

// Can't change
class MyClass implements MyInterface {
    public function slow(): SomeData { ... } // needs caching
}

// Can't change
class Receiver {
    public function __construct(MyInterface $my) { … }
    
    function f() {
        $data = $this->my->slow();
        ...
    }
}

$receiver = new Receiver(new MyClass());
$receiver->f();

(звезда) Заметка П.: можно использовать паттерн Wrapper, Proxy, Adapter

Решение 1 (не идеальное)

class ReceiverChild extends Receiver {

    public function __construct(MyInterface $my) 
    { 
    }

    function f() {
        if(!$this->isCached()) {
            $data = $this->my->slow();
            $this->saveToCache($data);
        } else {
            $data = $this->getFromCache();
        }
        
        // .... extra code from Receiver->f()
        
        return $data;
    }
}

(минус) плохое решение: т.к. в методе Receiver->f() может быть много логики и не надо ее копи-пастить тут (переопределяя этот метод)

Решение 2 (верное)

Создаем потомка MyClass где пишем нужную нам логику

class MyClassChild implements MyInterface {
    public function slow(): SomeData {
        if(!$this->isCached()) {
            $data = $this->my->slow();
            $this->saveToCache($data);
        } else {
            $data = $this->getFromCache();
        }
        return $data;     
    } 
}

Задача 4: парсер ini файла

Написать парсер ini-файла где есть сабсекции внутри секций, пример ниже:

/*

Example input:

foo=aaa

[bar]
baz = bbb

[bar/quux]
frob = ccc

Should result in the following output:

[
    ‘foo’ => ‘aaa’,
    ‘bar’ => [
        ‘baz’ => ‘bbb’,
        ‘quux’ => [
            ‘frob’ => ‘ccc’,
        ],
    ],
]

*/

function parse(string $filename): array
{
}

Написать свой парсер конфигов с указанными вложенными секциями

(звезда) есть готовая ф-я parse_ini_file($filename, true); но она не умеет парсить сабсекции вида sec1/sec2/sec3 как в примере

function parse(string $filename): array
{
    $res = parse_ini_file($filename, true);
    
    // var_dump($res);
    if(is_array($res) && count($res)) {
        foreach($res as $k => &$v) {
            if(strstr($k, '/') !== false) {
                $sub = explode('/', $k);
                foreach($sub as $subName) {
                    $v[$subName] = $v[$k];  
                } 
            }
        }
    }
}

Решение 1 (не идеальное)

Парсим файл базовой функцией parse_ini_file далее разбираем имена секций, ищем в них составные и обрабатываем их (создаем массивы с саб-секциями в результате).

function parse(string $filename): array
{
    $res = parse_ini_file($filename, true);
    
    // var_dump($res);
    if(is_array($res) && count($res)) {
        foreach($res as $k => &$v) {
            if(strstr($k, '/') !== false) {
                $sub = explode('/', $k);
                foreach($sub as $subName) {
                    processSubsection($subName, $v);            
                }
            }
        }
    }
}

function processSubsection($subName, &$data) {
//                     $v[$subName] = $v[$k];  
    $data[$subName] = $data;
    // .... process child sections above        
}

(минус) не будет работать для секций с 3го уровня и далее , нужно доработать…

Решение 2 (финальное, рабочее)

Будем обрабатывать подсекции в цикле.

<?php

/**
 * Parse ini file with subsections
 * @param string $filename
 * @return array
 */
function parse(string $filename): array
{
    $res = parse_ini_file($filename, true);

    if(is_array($res) && count($res)) {
        foreach($res as $k => &$data) {
            if(strstr($k, '/') !== false) {
                $subNames = explode('/', $k);
                // make subsections inside current section
                $subsec = &$res;
                foreach($subNames as $subName) {
                    $subsec = &$subsec[$subName];
                }
                $subsec = $data;
                // delete original section with composite key
                unset($res[$k]);
            }
        }
    }

    return $res;
}

$data = parse('config.ini');
print_r($data);

Ввод (файл)

foo=aaa

[bar]
baz = bbb

[bar/quux]
frob = ccc

Вывод

Array
(
    [foo] => aaa
    [bar] => Array
        (
            [baz] => bbb
            [quux] => Array
                (
                    [frob] => ccc
                )
        )
)

(warning) PS: рекомендую курсы Отус для начинающих и продвинутых разработчиков на языке PHP, после которых данные задачки вы сможете решать “на лету“. Также рекомендую скачаь мою книгу по подготовке к собеседованиям веб-разработчика. Здесь подробно разобрано множество вопросов и задач с интервью, накопленных мной за 10+ лет опыта.

Tags

Нет Ответов

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.

Рубрики


Подпишись на новости
👋

Есть вопросы?