Вопрос: Как правильно реализовать ООП класс базы данных с PDO?

Лучший ответ:
Я уже отвечал как-то на подобный вопрос. И не один раз. И не два.
Поскольку мозги всех пользователей пхп ходят по одним и тем же рельсам, не сворачивая. Впрочем, не всех. 85% всю жизнь продолжают писать mysl_query, которую выучили из видеоурока, и не видят в этом проблем. И только у самых талантливых 15-и процентов в какой-то момент возникает мысль ВСЁ АВТОМАТИЗИРОВАТЬ. Это, на самом деле, хороший знак. Такое желание как раз и отличает потенциального программиста от клепальщика гуано-кода.
Но всё портит недостаток знаний в SQL. Искренне полагая SQL не более чем key-value хранилищем, они всерьез уверены в том, что функция select() с двумя аргументами - это все что им надо.Настоятельно рекомендую прочесть аргументацию по ссылке выше. 
После этого понять, что существует ТРИ класса классов для работы с БД:
1. DB-хелпер. Класс, берущий на себя всю грязную работу по исполнению запросов. В случае с ПДО не сильно-то и нужен. Позволяет исполнять любые запросы. НИКАКИХ функций типа select(), ограничивающих функциональность, в нем быть не должно ни в коем случае.
2. Query builder. Функция типа select() может быть только в квери билдере, который маскирует SQL в функции РНР. Заведомо ущербен по сравнению с первым, но зато позволяет использовать запросы более сложные, чем ORM. 
3. ORM. То, что начинающему пользователю на самом деле нужно, но он об этом просто не догадывается. Как раз та самая волшебная палочка, которая делает примитивное доставание данных из базы по первичному ключу столь маняще единообразным.
Cамое главное, что надо понять: Все вышеперечисленное - это РАЗНЫЕ типы классов, не имеющие между собой ничего общего. 
И не пытаться под видом первого городить нежизнеспособное второе, имея в виду третье. Надо очень четко понимать, что сначала делаем первое, а потом, на его основе - либо второе, либо третье. Но не все вместе. 
А можно не пытаться изобретать велосипед, а использовать готовое. Например - популярный фреймворк. Тогда желаемая функция будет выглядеть вот так:public function viewUser($id)
{
    return User::model()->findByPk($id);
}
Это в самом предпочтительном случае - при использовании ORM.
На квери билдере это будет что-то вродеpublic function viewUser($id)
{
    return DB::select('*')->from('users')->where("id", '=', $id);
}
При этом можно использовать и чистый SQL. Запрос прямо в классе юзера - это не так уж и страшно. Тем более, что есть такие запросы, которые по другому просто не выполнишь. Другое дело, что всю работу по исполнению запроса должен брать на себя хелпер. Пример можно посмотреть по ссылке выше - там хоть и SQL , но того ужаса, который здесь, нету:public function viewUser($id)
{
    $sql = 'SELECT * FROM users WHERE id=?';
    return DB::prepare($sql)->execute([$id])->fetch();
}Дальнейшую работы над классом можно производить только после того как ты определишься, какой именно класс ты хочешь написать.

Худший ответ:
Вам нужен класс Connection, в котором есть методы connect и exec (для запросов). И не делайте подключения прямо в конструкторе, лучше в exec сделать проверку и подключать при необходимости (ленивое подключение). Зачем нужно оборачивать соединение? Что бы можно было мокать в тестах.
Так же вам нужен класс Query умеющий строить запросы и который будет принимать метод exec класса Connection. Так же можно его мокать в тестах.
Так же можно замутить QueryBuilder что бы совсем круто было.... Вот его мокать в тестах не стоит, ибо надоест быстро.
Проектируйте интерфейсы классов сначала. Клиенский код (тот который будет использовать вашу библиотеку) не должен знать о реализации интерфейса ничего. Он так же ничего не должен знать о PDO. Так же было бы неплохо перед тем как выполнять запросы брать мета данные таблиц, с которыми вам нужно работать (их можно кешировать) и делать prepared statements внутри Query.
Ну и да, можете ориентироваться на популярные реализации DBAL. Например doctrine/dbal