@OneTwoThreeFourFive

Правильно ли реализован класс для работы с базой данных по принципу SOLID?

Интерфейс для всех баз данных
interface DatabaseInterface {
	public function create();

	public function read();

	public function update();

	public function delete();
}


MySQL база данных
class MySQLDatabase implements DatabaseInterface {
	private $database;

	public function __construct( $dsn, $username, $password ) {
		try {
			$this->database = new PDO( $dsn, $username, $password );
			$this->database->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
		} catch ( Exception $e ) {
			
		}
	}

	public function create() {

	}

	public function read() {

	}

	public function update() {

	}

	public function delete() {

	}
}


Слой "менеджер" для базы данных
class DatabaseManager implements DatabaseInterface {
	private $database;

	public function __construct( DatabaseInterface $database ) {
		$this->database = $database;
	}

	public function create() {
		$this->database->create();
	}

	public function read() {
		$this->database->read();
	}

	public function update() {
		$this->database->update();
	}

	public function delete() {
		$this->database->delete();
	}
}


Приложение
$mysql = new MySQLDatabase( 'mysql:host=localhost;dbname=name;charset=UTF8', 'root', '' );
$database_manager = new DatabaseManager( $mysql );


Смысл
Интерфейс обязывает реализовать все методы. Класс MySQLDatabse работает с mysql и реализует всё из интерфейса. Менеджер базы данных также реализует все методы из интерфейса и ожидает "инъекцию" класса базы данных. Менеджеру базы данных всё равно какую базу данных он использует. Он знает только методы из интерфейса и знает, что они есть в любой базе данных. Если надо поменять базу данных на другую (не mysql), то в приложении надо создать новый класс для базы данных (на основе интерфейса) и передать в менеджер базы данных новый класс.

Почему я думаю, что этот код SOLID?
S - интерфейс и классы отвечают только за работу с базой данных (единственная ответственность).
O - можно добавить новые базы данных, не меняя код приложения, то есть не редактируется старый код, а только добавляется используя наследование и смысл классов не меняется (открытость/закрытость).
L - Класс менеджера не зависит от какой-то базы данных. Все классы базы данных имеют одинаковые методы и менеджеру всё равно какой класс подставляют (подстановка Лисков).
I - В интерфейсе нет лишнего (разделение интерфейса на маленькие).
D - Приложение (высокий уровень) зависит от базы данных, но не зависит от её реализации (низкий уровень). Если использовать другую базу данных, то весь код редактировать не надо, так как все методы и т.д такие же.

Вопросы
Этот код соответствует SOLID?
Нет ничего лишнего в этом коде (например, может менеджер лишняя обёртка/слой)?
Как вы реализуете работу с базой данных по принципу SOLID?
  • Вопрос задан
  • 428 просмотров
Пригласить эксперта
Ответы на вопрос 1
ipatiev
@ipatiev Куратор тега PHP
Потомок старинного рода Ипатьевых-Колотитьевых
Во-первых, это никакой не DatabaseManager , а CRUDManager. Работа с БД далеко не ограничивается этими 4 примитивными функциями.

Отсюда мы делаем логичный вывод, что соединение с БД никаким местом не должно создаваться в конструкторе менеджера крудов. А должно точно так же передаваться в него в качестве зависимости. Это может быть либо ванильная ПДО, либо инстанс реального MySQLDatabase (но поскольку мы пока не знаем, как он должен выглядеть, то лучше остановиться на PDO).

Сам по себе DatabaseManager выглядит избыточным. Непонятно, зачем он нужен, если любой потребитель DatabaseManager-а может просто написать
public function __construct(CRUDInterface $crud) {
}

и получить тот самый полиморфизм, которого мы изначально и добивались. Передадим другую реализацию - будет другая, но с тем же публичным контрактом, то есть всё будет работать.

В-четвёртых, хоть это и не относится напрямую к теме SOLID, но для меня является очень важным: собственно, реализация методов CRUD-а. Что в них передаётся? Откуда берутся названия таблиц, полей? Передаются в параметрах методов? Это прямая дорога к SQL инъекции, не говоря уже о нарушении инкапсуляции. Поэтому, отвечая на вопрос "Как вы реализуете работу с базой данных", лично я всё больше в последнее время от развесистых ORM-ов склоняюсь к простым TableGateway-ам. Да, кода писать больше, но он строже и понятнее. И не встаёт колом в нестандартных ситуациях. Тем более что приведённый пример кода как раз очень и похож на этот паттерн. То есть
abstract class MysqlTableGateway implements CrudInterface
{
    protected $db;
    protected $table;
    protected $fields;
    protected $primary = 'id';

    public function __construct(\PDO $db)
    {
        $this->db = $db;
    }
    public function read($id): ?array
    {
        $stmt = $this->db->prepare("SELECT * FROM `$this->table` WHERE `$this->primary`=?");
        $stmt->execute([$id]);
        return $stmt->fetch();
    }
     // ну и так далее
}

И дальше уже классы по работе с отдельными табличками наследовать от него,
final class UserGateway extends MysqlTableGateway {
    protected $table = 'users';
    protected $fields = ['email', 'password','phone'];
}

Соответственно, если мы захотим перейти с мускуля на какой-нибудь редис с джейсоном внутре, то надо будет создать новый абстрактный класс с тем же интерфейсом, и от него отнаследовать реализации. Соответственно, в интерфейсе надо нормально прописать входные и выходные параметры:
interface CRUDInterface {
    public function create(array $data):int;
    public function read(int $id):?array;
    public function update(array $data);
    public function delete(int $id);
}

Другое дело, что в реальности такой шалтай-болтай будет сделать довольно сложно, поскольку классы для работы с отдельными таблицами будут расширяться запросами, специфичными для данной таблицы - то есть все их придется дописывать во все драйверы. То есть в реальности с D будут проблемы. Но чисто с теоретической точки зрения примерно вот так оно будет выглядеть.
Ответ написан
Ваш ответ на вопрос

Войдите, чтобы написать ответ

Похожие вопросы