Ne v kontakte Asocial programmer's blog

Обработка множества исключений: решение

Feature image

Немного позже, чем думал (что простительно, ибо дни выдались хлопотные) привожу свои варианты решения упражнения, которое я озвучил в предыдущем посте. Условие повторять не буду, по приведенной ссылке все описано.

Решение 1, читерское.

В условии было сказано, что надо обрабатывать исключения в методах init(), run() и shutdown(). И хотя это явно не оговаривалось, по логике функционирования эти методы вызываются подряд. Следовательно, можно сделать так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<?php

// Code ...


$api = new ApiDispatcher();
try {
	$api->init();
	$api->run();
	$api->shutdown();
} catch(MyException1 $e) {
	// Handle exception
}
// More exception handling...
catch(Exception $e) {
	// Handle exception
}

Ну или, чтобы не нарушать инкапсуляцию, этот код можно завернуть в статический метод в ApiDispatcher.

Достоинства решения:

  • простота;
  • минимум дублирования кода;
  • локальность модификации кода.

Недостатки:

  • жестко привязываемся к сценарию последовательного вызова методов init(), run(), shutdown(), в реальной жизни может потребоваться вызывать их из разных частей приложения в разные моменты времени;
  • метод-обработчик исключений должен очень многое знать обо всех возможных исключениях, при появлении нового исключения придется править метод-обработчик.

Решение 2, логичное.

Следующее решение приходит в голову практически сразу после прочтения условия, и оно действительно хорошо подходит для решения задачи.

Именно его в комментариях предложил Vladimir Rusinov, за что ему спасибо:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#!/usr/bin/env python

class ApiDispatcher(object):

    def _exception_handler(self, e):
        print str(e)
        print type(e)
        if type(e) == IndexError:
             print "Check your indexes!"
        # logging, error messages, etc...

class MyClass(ApiDispatcher):

    def init(self):
        try:
            # do something
            myfunction()
        except Exception, e:
            self._exception_handler(e)

    def run(self):
        try:
            # do something else
            myfunction()
        except Exception, e:
            self._exception_handler(e)

    def shutdown(self):
        try:
            # finish our work
            myfunction()
        except Exception, e:
            self._exception_handler(e)


def myfunction():
    #a = []
    #return a[42]
    raise IndexError('blah-blah-blah')

if __name__ == '__main__':
    c = MyClass()
    c.init()
    c.run()
    c.shutdown()

Достоинства решения:

  • простота,
  • независимость от мест вызова методов (то есть, их не обязательно вызывать подряд),
  • минимум дублирования кода,
  • локальность правок.

Недостатки решения:

  • метод-обработчик исключений должен очень многое знать обо всех возможных исключениях, при появлении нового исключения придется править метод-обработчик,
  • требуется дополнительная логика для прерывания обработки, если упадет init(), либо проброс исключения дальше.

Решение 3, хитрое.

Это решение в конечном итоге я и использовал в своем проекте. Сначала я приведу код, а потом дам дополнительные комментарии:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php

class ApiException {
	public function init() {
		try {
			throw MyException(); // Just exception without error code and message
		} catch(Exception $e) {
			$this->handleException($e);
		}
	}

	public function run() {
		try {
			$operation_name = 'foo';
			throw AnotherException("Failed to perform $operation_name"); // Custom error message
		} catch(Exception $e) {
			$this->handleException($e);
		}
	}
	public function init() {
		try {
			throw OneMoreException(Errors::ERROR_CODE_DATABASE); // Custom error code
		} catch(Exception $e) {
			$this->handleException($e);
		}
	}

	protected function handleException(Exception $e) {
		$error_code = $e->getCode();
		$error_message = $e->getMessage();
		if(empty($error_message)) {
			$error_message = Errors::getMessageByCode($error_code);
		}

		Logger::get()->log($error_code, $error_message);
		// Do other stuff...
	}
}

class MyException extends Exception {
	protected $code = Errors::ERROR_CODE_MYERROR;
}

class AnotherException extends Exception {
	protected $code = Errors::ERROR_CODE_ANOTHERERROR;
}

class OneMoreException extends Exception {
	protected $code = Errors::ERROR_CODE_ONEERROR;
}

Здесь я явно использую класс Errors, в котором задефайнены константами все возможные коды ошибок. Кроме того, в нем есть метод, который позволяет по коду ошибки получить его текстовую расшифровку.

В PHP все исключения имеют по умолчанию два поля (есть и другие, но они нас не интересуют): $message = null и $code = 0, сообщение и код ошибки соответственно. Во всех наших исключениях мы переопределяем значение по умолчанию для кода ошибки, чтобы оно соответствовало типу исключения.

Теперь, когда мы создаем исключение, мы можем задать ему сообщение, которое описывает причину его возникновения, и, если надо, можем уточнить и код ошибки. Если же мы сообщение вручную не зададим - оно будет получено через класс Errors. В случае, если мы поймаем “не наше” исключение (типа простого Exception), у него код ошибки будет 0, и класс Errors успешно опишет его как “Unknown exception”.

Достоинства решения:

  • гибкость;
  • простота добавления новых исключений (не надо модифицировать метод-обработчик ошибок),
  • независимость от мест вызова методов (то есть, их не обязательно вызывать подряд),
  • минимум дублирования кода.

Недостатки решения

  • требуется дополнительная логика для прерывания обработки, если упадет init(), либо проброс исключения дальше,
  • нелокальность правок при внедрении решения (надо проставить коды ошибок всем существующим исключениям),
  • менее тривиальное решение,
  • возможны сложности в случае, если есть исключения, которым нельзя переопределить код ошибки.

Итоги

Я привел целых три решения задачи и видно, что ни одно из них не является безоговорочно правильным. А это значит, что даже такая простая задача, как описано в этом упражнении, требует внимательного анализа требований к системе и ваших возможностей, как ее разработчика. В моем случае решение 3 оказалось наименьшим злом, и я использовал его. Но запросто может оказаться, что в другой ситуации оптимально “наивное” решение 1.

P.S. Beto Vazquez Infinity-The Laws Of The Future