Я прочитал книгу Роберта К. Мартина «Чистый код» пару лет назад. Это замечательная книга, особенно для тех, кто находится в младшем возрасте своей карьеры. Она поможет вам стать более зрелым разработчиком программного обеспечения/инженером и чаще писать качественный код.
Я собрал советы, приемы и практики, которые я почерпнул из этой книги, и опубликую их в нескольких частях.
Обратите внимание, что в этой серии есть цитаты и примеры кода, которые я использовал из оригинальной книги.
Итак, без лишних слов, давайте начнем.
#1 Короткие функции лучше
Дядя Боб говорит о длине функции так: чем меньше функция, тем лучше.
Он говорит, что блок кода внутри операторов if
, else
и while
должен быть всего в одну строку. Также уровень отступа функции не должен быть больше одного или двух.
Это хорошая практика, если вы можете это сделать. Не всегда возможно строго следовать этому правилу, но старайтесь делать это как можно чаще.
#2 Вводите переменные экземпляра, когда это возможно
Передача переменных экземпляра вместо примитивных параметров в функцию — хорошая идея, когда это правильно. Но когда это правильно?
Рассмотрим большую функцию с большим количеством переменных, объявленных в ней. Допустим, вы хотите извлечь одну небольшую часть этой функции в отдельную функцию. Однако код, который вы хотите извлечь, использует четыре переменные, объявленные в функции.
Это ситуация, когда передача переменной экземпляра может быть хорошей идеей.
class Sample
{
public function __construct()
{
}
public function render()
{
// doing some logic
$this->getHtml($var1, $var2, $var3, $var4);
}
private function getHtml($var1, $var2, $var3, $var4)
{
// doing some logic with vars
}
}
Станет:
class Sample
{
private $var;
public function __construct(VarThing $var)
{
$this->var = $var;
}
public function render()
{
// doing some logic
$this->getHtml();
}
private function getHtml()
{
// $this->var is accessible here
}
}
#3 Шаблон BUILD-OPERATE-CHECK
Вы, вероятно, уже слышали о паттерне Arrange-Act-Assert или AAA, который похож на паттерн Build-Operate-Check
.
Как вы думаете, как можно улучшить следующий код?
public function testGetPageHieratchyAsXml()
{
crawler()->addPage($root, PathParser()->parse("PageOne"));
crawler()->addPage($root,
PathParser()->parse("PageOne.ChildOne"));
crawler()->addPage($root, PathParser()->parse("PageTwo"));
request()->setResource("root");
request()->addInput("type", "pages");
$responder = new SerializedPageResponder();
$response = $responder->makeResponse(
new FitNesseContext($root), $request
);
$xml = $response->getContent();
assertEquals("text/xml", $response->getContentType());
assertSubString("<name>PageOne</name>", $xml);
assertSubString("<name>PageTwo</name>", $xml);
assertSubString("<name>ChildOne</name>", $xml);
}
public function testGetPageHieratchyAsXmlDoesntContainSymbolicLinks()
{
$pageOne = crawler()->addPage($root,
PathParser()->parse("PageOne"));
crawler()->addPage($root,
PathParser()->parse("PageOne.ChildOne"));
crawler()->addPage($root, PathParser()->parse("PageTwo"));
$data = $pageOne->getData();
$properties = $data->getProperties();
$symLinks = $properties->set(SymbolicPage::PROPERTY_NAME);
$symLinks.set("SymPage", "PageTwo");
$pageOne.commit($data);
request()->setResource("root");
request()->addInput("type", "pages");
$responder = new SerializedPageResponder();
$response = $responder->makeResponse(
new FitNesseContext($root), $request);
$xml = $response->getContent();
assertEquals("text/xml", $response->getContentType());
assertSubString("<name>PageOne</name>", $xml);
assertSubString("<name>PageTwo</name>", $xml);
assertSubString("<name>ChildOne</name>", $xml);
assertNotSubString("SymPage", $xml);
}
public function testGetDataAsHtml()
{
crawler()->addPage($root, PathParser()
->parse("TestPageOne"), "test page");
request()->setResource("TestPageOne");
request()->addInput("type", "data");
$responder = new SerializedPageResponder();
$response = $responder->makeResponse(
new FitNesseContext($root), $request);
$xml = $response->getContent();
assertEquals("text/xml", $response->getContentType());
assertSubString("test page", $xml);
assertSubString("<Test", $xml);
}
Вот как мы можем его улучшить:
public function testGetPageHierarchyAsXml()
{
makePages("PageOne", "PageOne.ChildOne", "PageTwo");
submitRequest("root", "type:pages");
assertResponseIsXML();
assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
}
public function testSymbolicLinksAreNotInXmlPageHierarchy()
{
$page = makePage("PageOne");
makePages("PageOne.ChildOne", "PageTwo");
addLinkTo($page, "PageTwo", "SymPage");
submitRequest("root", "type:pages");
assertResponseIsXML();
assertResponseContains("<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>");
assertResponseDoesNotContain("SymPage");
}
public function testGetDataAsXml()
{
makePageWithContent("TestPageOne", "test page");
submitRequest("TestPageOne", "type:data");
assertResponseIsXML();
assertResponseContains("test page", "<Test");
}
Преимущество этой схемы в том, что подавляющее большинство раздражающих деталей устранено. Тесты сразу переходят к делу и используют только те типы данных и функции, которые действительно необходимы. Любой, кто прочитает эти тесты, сможет быстро понять, что они делают, не будучи введенным в заблуждение или перегруженным деталями.
#4 Единая ответственность и обработка ошибок
Лучший способ описать это — процитировать прямо из книги:
Функции должны делать что-то одно. Обработка ошибок — это одно дело. Таким образом, функция, которая обрабатывает ошибки, не должна делать ничего другого. Это подразумевает, что если в функции есть ключевое слово try, то оно должно быть самым первым словом в функции и что после catch/finally не должно быть ничего.
#5 Лучшая обработка ошибок
как, по-вашему, можно улучшить форму этого куска кода?
$port = new ACMEPort(12);
try {
$port->open();
} catch (DeviceResponseException $e) {
reportPortError($e);
logger()->log("Device response exception", $e);
} catch (ATM1212UnlockedException $e) {
reportPortError($e);
logger()->log("Unlock exception", $e);
} catch (GMXError $e) {
reportPortError($e);
logger()->log("Device response exception");
} finally {
/// ...
}
Мы можем значительно упростить наш код, обернув API, который мы вызываем, и убедившись, что он возвращает общий тип исключения:
$port = new LocalPort(12);
try {
$port->open();
} catch (PortDeviceFailure $e) {
reportError($e);
logger()->log($e->getMessage(), $e);
} finally {
// ...
}
class LocalPort {
private $innerPort;
public function __construct(int $portNumber) {
$this->innerPort = new ACMEPort($portNumber);
}
public function open() {
try {
$this->innerPort->open();
} catch (DeviceResponseException $e) {
throw new PortDeviceFailure($e);
} catch (ATM1212UnlockedException $e) {
throw new PortDeviceFailure($e);
} catch (GMXError $e) {
throw new PortDeviceFailure($e);
}
}
// ...
}
Обертки, подобные той, которую мы определили для ACMEPort, могут быть очень полезны.
Хорошо. Это все для этой части. Я продолжу публиковать следующие части в ближайшее время. Вы можете присоединиться к моему каналу Telegram, чтобы получать уведомления о последних публикациях. Также вы можете следить за мной в Twitter и LinkedIn.