Пару лет назад я прочитал книгу Чистый код Роберта С. Мартина. Это отличная книга, особенно для тех, кто только начинает свою карьеру. Это поможет вам стать более зрелым разработчиком/инженером программного обеспечения и чаще писать качественный код.

Я собрал советы, приемы и практики, которые узнал из этой книги, и опубликую их в нескольких частях.

Обратите внимание, что я использовал цитаты и примеры кода из оригинальной книги этой серии.

Итак, без лишних слов, начнем.

Сокращенные функции №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: ПОСТРОИТЬ-ЭКСПЛУАТИРОВАТЬ-ПРОВЕРИТЬ

Вы, наверное, уже слышали о паттерне 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.