Написание кода, который легко изменить, — это Святой Грааль программирования. Добро пожаловать в программирование нирваны! Но в действительности все намного сложнее: исходный код сложен для понимания, зависимости указывают в бесчисленных направлениях, связь раздражает, и вы скоро почувствуете жар ада программирования. В этом руководстве мы обсудим несколько принципов, методов и идей, которые помогут вам написать код, который легко изменить.
Некоторые объектно-ориентированные концепции
Объектно-ориентированное программирование (ООП) стало популярным благодаря обещанию организации и повторного использования кода; это совершенно не удалось в этом начинании. Мы используем концепции ООП уже много лет, но мы продолжаем неоднократно применять ту же логику в наших проектах. ООП представил набор хороших базовых принципов, которые, при правильном использовании, могут привести к созданию лучшего, более чистого кода.
когезия
Вещи, которые принадлежат друг другу, должны храниться вместе; в противном случае они должны быть перемещены в другое место. Это то, к чему относится термин «сплоченность». Лучший пример сплоченности можно продемонстрировать с помощью класса:
1
|
class ANOTCohesiveClass { private $firstNumber;
|
Этот пример определяет класс с полями, которые представляют числа и размеры. Эти свойства, судя только по их названиям, не принадлежат друг другу. Затем у нас есть два метода, add()
и substract()
, которые работают только с двумя числовыми переменными. У нас также есть метод area()
, который работает с полями length
и width
.
Очевидно, что этот класс отвечает за отдельные группы информации. У него очень низкая когезия . Давайте рефакторинг это.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
class ACohesiveClass {
private $firstNumber;
private $secondNumber;
function __construct($firstNumber, $secondNumber) {
$this->firstNumber = $firstNumber;
$this->secondNumber = $secondNumber;
}
function add() {
return $this->firstNumber + $this->secondNumber;
}
function subtract() {
return $this->firstNumber — $this->secondNumber;
}
}
|
Это очень сплоченный класс . Почему? Потому что каждый раздел этого класса принадлежит друг другу. Вы должны стремиться к сплоченности, но будьте осторожны, это может быть трудно достичь.
Ортогональность
Проще говоря, ортогональность относится к изоляции или устранению побочных эффектов. Метод, класс или модуль, который изменяет состояние других не связанных классов или модулей, не является ортогональным. Например, черный ящик самолета является ортогональным. Он имеет свои внутренние функции, внутренний источник питания, микрофоны и датчики. Это не влияет на самолет, в котором он находится, или на внешний мир. Это только обеспечивает механизм для записи и получения данных о полете.
Примером одной такой неортогональной системы является электроника вашего автомобиля. Увеличение скорости вашего автомобиля имеет несколько побочных эффектов, таких как увеличение громкости радио (среди прочего). Скорость не ортогональна к машине.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
class Calculator {
private $firstNumber;
private $secondNumber;
function __construct($firstNumber, $secondNumber) {
$this->firstNumber = $firstNumber;
$this->secondNumber = $secondNumber;
}
function add() {
$sum = $this->firstNumber + $this->secondNumber;
if ($sum > 100) {
(new AlertMechanism())->tooBigNumber($sum);
}
return $sum;
}
function subtract() {
return $this->firstNumber — $this->secondNumber;
}
}
class AlertMechanism {
function tooBigNumber($number) {
echo $number .
}
}
|
В этом примере метод add()
класса Calculator
демонстрирует неожиданное поведение: он создает объект AlertMechanism
и вызывает один из его методов. Это неожиданное и нежелательное поведение; потребители библиотеки никогда не ожидают сообщения, напечатанного на экране. Вместо этого они ожидают только сумму предоставленных чисел.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
class Calculator {
private $firstNumber;
private $secondNumber;
function __construct($firstNumber, $secondNumber) {
$this->firstNumber = $firstNumber;
$this->secondNumber = $secondNumber;
}
function add() {
return $this->firstNumber + $this->secondNumber;
}
function subtract() {
return $this->firstNumber — $this->secondNumber;
}
}
class AlertMechanism {
function checkLimits($firstNumber, $secondNumber) {
$sum = (new Calculator($firstNumber, $secondNumber))->add();
if ($sum > 100) {
$this->tooBigNumber($sum);
}
}
function tooBigNumber($number) {
echo $number .
}
}
|
Это лучше. AlertMechanism
не влияет на Calculator
. Вместо этого AlertMechanism
использует все, что ему нужно, чтобы определить, следует ли выдавать предупреждение.
Зависимость и связь
В большинстве случаев эти два слова взаимозаменяемы; но в некоторых случаях один термин предпочтительнее другого.
Так что же такое зависимость ? Когда объект A
должен использовать объект B
, чтобы выполнить предписанное поведение, мы говорим, что A
зависит от B
В ООП зависимости очень распространены. Объекты часто работают и зависят друг от друга. Таким образом, хотя устранение зависимости является благородным занятием, сделать это практически невозможно. Однако контроль зависимостей и их уменьшение предпочтительнее.
Термины « тяжелая и слабая связь» обычно относятся к тому, насколько объект зависит от других объектов.
В слабосвязанной системе изменения в одном объекте оказывают меньшее влияние на другие объекты, которые зависят от него. В таких системах классы зависят от интерфейсов, а не от конкретных реализаций (мы поговорим об этом позже). Вот почему слабосвязанные системы более открыты для модификаций.
Сцепление в поле
Давайте рассмотрим пример:
1
2
3
4
5
6
7
8
9
|
class Display {
private $calculator;
function __construct() {
$this->calculator = new Calculator(1,2);
}
}
|
Это обычный код такого типа. Класс Display
в этом случае зависит от класса Calculator
, напрямую ссылаясь на этот класс. В приведенном выше коде поле $calculator
Display
имеет тип Calculator
. Объект, который содержит это поле, является результатом прямого вызова конструктора Calculator
.
Связь через доступ к другим методам класса
Просмотрите следующий код для демонстрации такого рода связи:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class Display {
private $calculator;
function __construct() {
$this->calculator = new Calculator(1, 2);
}
function printSum() {
echo $this->calculator->add();
}
}
|
Класс Display
вызывает метод add()
объекта Calculator
. Это еще одна форма связи, потому что один класс обращается к методу другого.
Связь по методу
Вы также можете связать классы со ссылками на методы. Например:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
class Display {
private $calculator;
function __construct() {
$this->calculator = $this->makeCalculator();
}
function printSum() {
echo $this->calculator->add();
}
function makeCalculator() {
return new Calculator(1, 2);
}
}
|
Важно отметить, что метод makeCalculator()
возвращает объект Calculator
. Это зависимость.
Сцепление по полиморфизму
Наследование, вероятно, самая сильная форма зависимости:
1
2
3
4
5
6
7
|
class AdvancedCalculator extends Calculator {
function sinus($value) {
return sin($value);
}
}
|
AdvancedCalculator
не только не может выполнять свою работу без Calculator
, но даже не может существовать без него.
Уменьшение сцепления путем внедрения зависимости
Можно уменьшить связь, вводя зависимость. Вот один из таких примеров:
01
02
03
04
05
06
07
08
09
10
11
|
class Display {
private $calculator;
function __construct(Calculator $calculator = null) {
$this->calculator = $calculator ?
}
// … //
}
|
Внедрив объект Calculator
через конструктор Display
, мы уменьшили зависимость Display
от класса Calculator
. Но это только половина решения.
Уменьшение связи с интерфейсами
Мы можем еще больше уменьшить связь, используя интерфейсы. Например:
01
02
03
04
05
06
07
08
09
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
|
interface CanCompute {
function add();
function subtract();
}
class Calculator implements CanCompute {
private $firstNumber;
private $secondNumber;
function __construct($firstNumber, $secondNumber) {
$this->firstNumber = $firstNumber;
$this->secondNumber = $secondNumber;
}
function add() {
return $this->firstNumber + $this->secondNumber;
}
function subtract() {
return $this->firstNumber — $this->secondNumber;
}
}
class Display {
private $calculator;
function __construct(CanCompute $calculator = null) {
$this->calculator = $calculator ?
}
function printSum() {
echo $this->calculator->add();
}
function makeCalculator() {
return new Calculator(1, 2);
}
}
|
Вы можете думать о ISP как о принципе сплоченности более высокого уровня.
Этот код представляет интерфейс CanCompute
. Интерфейс настолько абстрактный, насколько вы можете получить в ООП; он определяет членов, которые класс должен реализовать. В приведенном выше примере Calculator
реализует интерфейс CanCompute
.
Конструктор Display
ожидает объект, который реализует CanCompute
. На этом этапе зависимость Display
от Calculator
фактически нарушена. В любое время мы можем создать другой класс, который реализует CanCompute
и передать объект этого класса в конструктор Display
. Display
теперь зависит только от интерфейса CanCompute
, но даже эта зависимость не является обязательной. Если мы не передадим аргументы в конструктор Display
, он просто создаст классический объект Calculator
, вызвав makeCalculator()
. Этот метод часто используется и чрезвычайно полезен для разработки через тестирование (TDD).
ТВЕРДЫЕ ПРИНЦИПЫ
SOLID — это набор принципов для написания чистого кода, который облегчает его изменение, поддержку и расширение в будущем. Это рекомендации, которые при применении к исходному коду оказывают положительное влияние на удобство сопровождения.
Маленькая история
Принципы SOLID, также известные как Agile, были первоначально определены Робертом К. Мартином. Несмотря на то, что он не изобрел все эти принципы, он был тем, кто соединил их. Вы можете прочитать больше о них в его книге « Разработка гибкого программного обеспечения, принципы, шаблоны и практики» . Принципы SOLID охватывают широкий спектр тем, но я изложу их настолько простым способом, насколько смогу. Не стесняйтесь просить дополнительные детали в комментариях, если это необходимо.
Принцип единой ответственности (SRP)
У класса есть единственная ответственность. Это может показаться простым, но иногда это трудно понять и применить на практике.
1
2
3
4
5
6
|
class Reporter {
function generateIncomeReports();
function generatePaymentsReports();
function computeBalance();
function printReport();
}
|
Как вы думаете, кому выгодно поведение этого класса? Ну, бухгалтерия — это вариант (для баланса), финансовый отдел может быть другим (для отчетов о доходах / платежах), и даже отдел архивации может печатать и архивировать отчеты.
Есть четыре причины, по которым вам, возможно, придется изменить этот класс; каждый отдел может захотеть, чтобы их методы были адаптированы под их нужды.
SRP рекомендует разбивать такие классы на более мелкие, специфичные для поведения классы, каждый из которых имеет только одну причину для изменения. Такие классы имеют тенденцию быть очень связными и слабо связанными. В некотором смысле, SRP — это сплоченность, определенная с точки зрения пользователей.
Открытый Закрытый Принцип (OCP)
Классы (и модули) должны приветствовать расширение их функциональности, а также противостоять изменениям их текущей функциональности. Давайте поиграем с классическим примером электрического вентилятора. У вас есть переключатель, и вы хотите управлять вентилятором. Таким образом, вы могли бы написать что-то вроде:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
class Switch_ {
private $fan;
function __construct() {
$this->fan = new Fan();
}
function turnOn() {
$this->fan->on();
}
function turnOff() {
$this->fan->off();
}
}
|
Наследование, вероятно, самая сильная форма зависимости.
Этот код определяет класс Switch_
который создает и управляет объектом Fan
. Обратите внимание на подчеркивание после «Switch_». PHP не позволяет вам определять класс с именем «Switch».
Ваш босс решает, что он хочет управлять светом с помощью того же переключателя. Это проблема, потому что вы должны изменить Switch_
.
Любые изменения в существующем коде являются риском; другие части системы могут быть затронуты и потребовать даже дальнейшей модификации. При добавлении новых функций всегда предпочтительнее оставлять существующие функции в покое.
В терминологии ООП видно, что Switch_
сильно зависит от Fan
. Вот где наша проблема и где мы должны внести свои изменения.
01
02
03
04
05
06
07
08
09
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
|
interface Switchable {
function on();
function off();
}
class Fan implements Switchable {
public function on() {
// code to start the fan
}
public function off() {
// code to stop the fan
}
}
class Switch_ {
private $switchable;
function __construct(Switchable $switchable) {
$this->switchable = $switchable;
}
function turnOn() {
$this->switchable->on();
}
function turnOff() {
$this->switchable->off();
}
}
|
Это решение представляет интерфейс Switchable
. Он определяет методы, которые должны быть реализованы всеми объектами с включенной коммутацией. Fan
реализует Switchable
, а Switch_
принимает ссылку на объект Switchable
в своем конструкторе.
Как это поможет нам?
Во-первых, это решение нарушает зависимость между Switch_
и Fan
. Switch_
имеет ни малейшего представления, что он запускает фанат, и не заботится Во-вторых, введение класса Light
не повлияет на Switch_
или Switchable
. Вы хотите управлять объектом Light
с помощью вашего класса Switch_
? Просто создайте объект Light
и передайте его Switch_
, например так:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
|
class Light implements Switchable {
public function on() {
// code to turn ligh on
}
public function off() {
// code to turn light off
}
}
class SomeWhereInYourCode {
function controlLight() {
$light = new Light();
$switch = new Switch_($light);
$switch->turnOn();
$switch->turnOff();
}
}
|
Принцип замещения Лискова (LSP)
В LSP говорится, что дочерний класс никогда не должен нарушать функциональность родительского класса. Это чрезвычайно важно, потому что потребители родительского класса ожидают, что класс будет вести себя определенным образом. Передача дочернего класса потребителю должна просто работать и не влиять на исходную функциональность.
На первый взгляд это сбивает с толку, поэтому давайте рассмотрим еще один классический пример:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
class Rectangle {
private $width;
private $height;
function setWidth($width) {
$this->width = $width;
}
function setHeigth($heigth) {
$this->height = $heigth;
}
function area() {
return $this->width * $this->height;
}
}
|
Этот пример определяет простой класс Rectangle
. Мы можем установить его высоту и ширину, а метод area()
предоставляет площадь прямоугольника. Использование класса Rectangle
может выглядеть следующим образом:
1
2
3
4
5
6
7
8
9
|
class Geometry {
function rectArea(Rectangle $rectangle) {
$rectangle->setWidth(10);
$rectangle->setHeigth(5);
return $rectangle->area();
}
}
|
Метод rectArea()
принимает объект Rectangle
в качестве аргумента, устанавливает его высоту и ширину и возвращает область формы.
В школе нас учат, что квадраты — это прямоугольники. Это намекает на то, что если мы моделируем нашу программу для нашего геометрического объекта, класс Square
должен расширять класс Rectangle
. Как бы выглядел такой класс?
1
2
3
|
class Square extends Rectangle {
// What code to write here?
}
|
Мне трудно понять, что писать в классе Square
. У нас есть несколько вариантов. Мы могли бы переопределить метод area()
и вернуть квадрат $width
:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
class Rectangle {
protected $width;
protected $height;
// … //
}
class Square extends Rectangle {
function area() {
return $this->width ^ 2;
}
}
|
Обратите внимание, что я изменил поля Rectangle
на protected
, предоставив Square
доступ к этим полям. Это выглядит разумно с геометрической точки зрения. Квадрат имеет равные стороны; возврат квадрата ширины является разумным.
Однако у нас есть проблема с точки зрения программирования. Если Square
— это Rectangle
, у нас не должно возникнуть проблем с передачей его в класс Geometry
. Но, делая это, вы можете видеть, что код Geometry
не имеет особого смысла; он устанавливает два разных значения высоты и ширины. Вот почему квадрат не является прямоугольником в программировании. ЛСП нарушен.
Принцип разделения интерфейса (ISP)
Модульные тесты должны выполняться быстро — очень быстро.
Этот принцип концентрируется на разбиении больших интерфейсов на маленькие специализированные интерфейсы. Основная идея заключается в том, что разные потребители одного и того же класса не должны знать о разных интерфейсах — только те интерфейсы, которые должен использовать потребитель. Даже если потребитель напрямую не использует все открытые методы объекта, он все равно зависит от всех методов. Так почему бы не предоставить интерфейсы, которые объявляют только методы, которые нужны каждому пользователю?
Это близко соответствует тому, что интерфейсы должны принадлежать клиентам, а не реализации. Если вы адаптируете свои интерфейсы к потребляющим классам, они будут уважать ISP. Сама реализация может быть уникальной, так как класс может реализовывать несколько интерфейсов.
Давайте представим, что мы реализуем приложение на фондовом рынке. У нас есть брокер, который покупает и продает акции, и он может сообщать о своих ежедневных доходах и убытках. Очень простая реализация будет включать что-то вроде интерфейса Broker
, класс NYSEBroker
который реализует Broker
и пару классов пользовательского интерфейса: один для создания транзакций ( TransactionsUI
) и один для отчетов ( DailyReporter
). Код для такой системы может быть похож на следующее:
01
02
03
04
05
06
07
08
09
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
|
interface Broker {
function buy($symbol, $volume);
function sell($symbol, $volume);
function dailyLoss($date);
function dailyEarnings($date);
}
class NYSEBroker implements Broker {
public function buy($symbol, $volume) {
// implementsation goes here
}
public function currentBalance() {
// implementsation goes here
}
public function dailyEarnings($date) {
// implementsation goes here
}
public function dailyLoss($date) {
// implementsation goes here
}
public function sell($symbol, $volume) {
// implementsation goes here
}
}
class TransactionsUI {
private $broker;
function __construct(Broker $broker) {
$this->broker = $broker;
}
function buyStocks() {
// UI logic here to obtain information from a form into $data
$this->broker->buy($data[‘sybmol’], $data[‘volume’]);
}
function sellStocks() {
// UI logic here to obtain information from a form into $data
$this->broker->sell($data[‘sybmol’], $data[‘volume’]);
}
}
class DailyReporter {
private $broker;
function __construct(Broker $broker) {
$this->broker = $broker;
}
function currentBalance() {
echo ‘Current balace for today ‘ .
echo ‘Earnings: ‘ .
echo ‘Losses: ‘ .
}
}
|
Хотя этот код может работать, он нарушает интернет-провайдера. И DailyReporter
и TransactionUI
зависят от интерфейса Broker
. Однако каждый из них использует только часть интерфейса. TransactionUI
использует методы buy()
и sell()
, а DailyReporter
использует dailyEarnings()
и dailyLoss()
.
Вы можете утверждать, что
Broker
не сплочен, потому что у него есть методы, которые не связаны и, следовательно, не принадлежат друг другу
Это может быть правдой, но ответ зависит от реализации Broker
; продажа и покупка могут быть сильно связаны с текущими потерями и прибылью. Например, вам может быть запрещено покупать акции, если вы теряете деньги.
Вы также можете утверждать, что Broker
также нарушает SRP. Поскольку у нас есть два класса, которые используют его по-разному, могут быть два разных пользователя. Ну, я говорю нет. Единственный пользователь, вероятно, фактический брокер. Он / она хочет покупать, продавать и просматривать свои текущие средства. Но опять же, фактический ответ зависит от всей системы и бизнеса.
Интернет-провайдер наверняка нарушен. Оба класса пользовательского интерфейса зависят от всего Broker
. Это распространенная проблема, если вы думаете, что интерфейсы принадлежат их реализациям. Однако изменение вашей точки зрения может предложить следующий дизайн:
01
02
03
04
05
06
07
08
09
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
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
|
interface BrokerTransactions {
function buy($symbol, $volume);
function sell($symbol, $volume);
}
interface BrokerStatistics {
function dailyLoss($date);
function dailyEarnings($date);
}
class NYSEBroker implements BrokerTransactions, BrokerStatistics {
public function buy($symbol, $volume) {
// implementsation goes here
}
public function currentBalance() {
// implementsation goes here
}
public function dailyEarnings($date) {
// implementsation goes here
}
public function dailyLoss($date) {
// implementsation goes here
}
public function sell($symbol, $volume) {
// implementsation goes here
}
}
class TransactionsUI {
private $broker;
function __construct(BrokerTransactions $broker) {
$this->broker = $broker;
}
function buyStocks() {
// UI logic here to obtain information from a form into $data
$this->broker->buy($data[‘sybmol’], $data[‘volume’]);
}
function sellStocks() {
// UI logic here to obtain information from a form into $data
$this->broker->sell($data[‘sybmol’], $data[‘volume’]);
}
}
class DailyReporter {
private $broker;
function __construct(BrokerStatistics $broker) {
$this->broker = $broker;
}
function currentBalance() {
echo ‘Current balace for today ‘ .
echo ‘Earnings: ‘ .
echo ‘Losses: ‘ .
}
}
|
Это действительно имеет смысл и уважает интернет-провайдера. DailyReporter
зависит только от BrokerStatistics
; он не заботится и не должен знать о каких-либо операциях продажи и покупки. TransactionsUI
другой стороны, TransactionsUI
знает только о покупке и продаже. NYSEBroker
идентичен нашему предыдущему классу, за исключением того, что теперь он реализует интерфейсы BrokerTransactions
и BrokerStatistics
.
Вы можете думать о ISP как о принципе сплоченности более высокого уровня.
Когда оба класса пользовательского интерфейса зависели от интерфейса Broker
, они были похожи на два класса, каждый из которых имел четыре поля, два из которых использовались в одном методе, а два других — в другом методе. Класс не был бы очень сплоченным.
Более сложный пример этого принципа можно найти в одной из первых статей Роберта К. Мартина на эту тему: «Принцип сегрегации интерфейса» .
Принцип обращения зависимостей (DIP)
Этот принцип гласит, что модули высокого уровня не должны зависеть от модулей низкого уровня; оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей; детали должны зависеть от абстракций. Проще говоря, вы должны максимально полагаться на абстракции, а не на конкретные реализации.
Хитрость с DIP заключается в том, что вы хотите изменить зависимость, но всегда хотите сохранить контроль над потоком. Давайте рассмотрим наш пример из OCP (классы Switch
и Light
). В первоначальной реализации у нас был переключатель, непосредственно управляющий освещением.
Как вы можете видеть, как зависимость, так и управление перетекают из Switch
в Light
. Хотя это то, что мы хотим, мы не хотим напрямую зависеть от Light
. Итак, мы представили интерфейс.
Удивительно, как простое введение интерфейса заставляет наш код уважать DIP и OCP. Как видите, класс зависит от конкретной реализации Light
, а Light
и Switch
зависят от интерфейса Switchable
. Мы перевернули зависимость, и поток управления не изменился.
Дизайн высокого уровня
Другим важным аспектом вашего кода является ваш высокоуровневый дизайн и общая архитектура. Запутанная архитектура создает код, который трудно изменить. Поддержание чистой архитектуры очень важно, и первым шагом является понимание того, как разделить различные проблемы вашего кода.
На этом снимке я попытался обобщить основные проблемы. В центре схемы находится наша бизнес-логика. Он должен быть хорошо изолирован от остального мира и иметь возможность работать и вести себя так, как ожидается, без наличия каких-либо других частей. Воспринимайте это как ортогональность на более высоком уровне.
Начиная справа, у вас есть «главный» — точка входа в приложение — и фабрики, которые создают объекты. Идеальное решение получит свои объекты от специализированных заводов, но это в основном невозможно или нецелесообразно. Тем не менее, вы должны использовать фабрики, когда у вас есть такая возможность, и держать их вне вашей бизнес-логики.
Затем внизу (оранжевым цветом) у нас есть постоянство (базы данных, доступ к файлам, сетевые коммуникации) с целью сохранения информации. Ни один объект в нашей бизнес-логике не должен знать, как работает постоянство.
Слева находится механизм доставки.
MVC, как Laravel или CakePHP, должен быть только механизмом доставки, не более того.
Это позволяет вам менять один механизм на другой, не затрагивая бизнес-логику. Это может показаться возмутительным для некоторых из вас. Нам говорят, что наша бизнес-логика должна быть размещена в наших моделях. Ну, я не согласен. Наши модели должны быть «моделями запросов», т.е. тупыми объектами данных, используемыми для передачи информации из MVC в бизнес-логику. При желании я не вижу проблем, включая проверку входных данных в моделях, но не более того. Бизнес-логика не должна быть в моделях.
Когда вы смотрите на архитектуру вашего приложения или структуру каталогов, вы должны увидеть структуру, которая указывает на то, что программа делает, в отличие от того, какую платформу или базу данных вы использовали.
Наконец, убедитесь, что все зависимости указывают на нашу бизнес-логику. Пользовательские интерфейсы, фабрики, базы данных являются очень конкретными реализациями, и вы никогда не должны зависеть от них. Инверсия зависимостей, указывающая на нашу бизнес-логику, модулирует нашу систему, позволяя нам изменять зависимости без изменения бизнес-логики.
Некоторые мысли о шаблонах дизайна
Шаблоны проектирования играют важную роль в облегчении изменения кода, предлагая общее решение для проектирования, понятное каждому программисту. Со структурной точки зрения шаблоны проектирования явно выгодны. Это хорошо проверенные и продуманные решения.
Если вы хотите больше узнать о шаблонах проектирования, я создал для них курс Tuts + Premium!
Сила тестирования
Разработка через тестирование поощряет написание кода, который легко тестировать. TDD заставляет вас уважать большинство вышеперечисленных принципов, чтобы облегчить тестирование кода. Внедрение зависимостей и написание ортогональных классов имеют важное значение; в противном случае вы получите огромные методы испытаний. Модульные тесты должны выполняться быстро — на самом деле очень быстро, и все, что не тестировалось, должно быть проверено. Насмешка над многими сложными классами для простого теста может быть ошеломляющей. Поэтому, когда вы обнаруживаете, что дразняете десять объектов для тестирования одного метода в классе, у вас могут возникнуть проблемы с вашим кодом … а не с вашим тестом.
Последние мысли
В конце концов, все сводится к тому, насколько вы заботитесь о своем исходном коде. Наличие технических знаний недостаточно; Вы должны применять эти знания снова и снова, никогда не будучи на 100% удовлетворены вашим кодом. Вы должны сделать свой код простым в обслуживании, чистым и открытым для изменения.
Спасибо за чтение и не стесняйтесь вносить свои методы в комментариях ниже.