Вопрос: Как правильно организовать классы?

Лучший ответ:
Я использую такой подход: 
Есть три интерфейса EntityInterface, RepositoryInterface, RepositoryDriverInterfaceinterface EntityInterface extends Serializable {
    public function getId();
}

interface RepositoryInterface {
    public function getDriver();
    public function findById($id, EntityInterface $Entity);
    public function save(EntityInterface $Entity);
}

interface RepositoryDriverInterface {
    public function set($id, $data);
    public function get($id);
}
Драйвер репозитория работает с базой данных. Сам репозиторий занимается сериализацией/десериализацией сущностей. Примеры драйверов:
Драйвер, работающий с массивами:class RepositoryDriverArray implements RepositoryDriverInterface {
    private $data = [];

    public function set($id, $data) {
        $this->data[$id] = $data;
    }

    public function get($id) {
        return $this->data[$id];
    }
}
Драйвер работающий с Redis'ом:class RepositoryDriverRedis implements RepositoryDriverInterface {
    protected $Redis = null;

    public function __construct(Client $Redis) {
        $this->Redis = $Redis;
    }

    public function set($id, $data) {
        $jsonData = json_encode($data);
        $this->Redis->hset($this->getContainerName(), $id, $jsonData);
    }

    public function get($id) {
        $jsonData = $this->Redis->hget($this->getContainerName(), $id);
        $data = json_decode($jsonData, true);

        return $data;
    }

    public function getContainerName() {
        return 'myContainer';
    }
}
Пример реализации самих репозиториев:abstract class AbstractRepository implements RepositoryInterface {
    protected $RepositoryDriver = null;

    public function __construct(RepositoryDriverInterface $RepositoryDriver) {
        $this->RepositoryDriver = $RepositoryDriver;
    }

    public function save(EntityInterface $Entity) {
        $this->getDriver()->set($Entity->getId(), $Entity->serialize());
    }

    public function findById($id, EntityInterface $Entity) {
        $data = $this->getDriver()->get($id);
        $Entity->unserialize($data);

        return $Entity;
    }

    public function getDriver() {
        return $this->RepositoryDriver;
    }
}
Далее, пишем бизнес-логику:class User implements EntityInterface {
    // TODO: Implement methods
}
class UserRepository extends AbstractRepository {}
И вот так работаем со всей этой системой:$Redis = new Client([/* connection params */]);
$RedisDriver = new RepositoryDriverRedis($Redis);
$UserRepository = new UserRepository($RedisDriver);

$User = new User();
$UserRepository->save($User);

$User = new User();
$UserRepository->findById(1, $User);
Пишем юнит тесты? Подменяем драйвер репозитория:$ArrayDriver = new RepositoryDriverArray();
$UserRepository = new UserRepository($ArrayDriver);

$User = new User();
$UserRepository->save($User);

$User = new User();
$UserRepository->findById(1, $User);
Такую систему очень просто тестировать. Всю "рутину" можно обернуть в различные фасады, "конфигураторы", провайдеры. 
Я написал лишь примеры, конечно, в реальной системе больше возможностей драйвера, репозиториев. 
Кэширование стоит делать на уровне репозитория. Драйвер занимается только сохранением/загрузкой данных.

Худший ответ:
Вы никогда не работаете "через интерфейс", хотя бы потому что он не имеет реализации.
Но вы можете сделать следующее:...
public function setRepository (RepositoryInterface $repository) {
    $this->repository = $repository;
}
...
Таким образом передать в качестве аргумента можно любой объект, класс которого реализует RepositoryInterface, и только такой объект. Так же можно передавать его в конструкторе при создании объекта, если он обязательно нужен.
А вообще рекомендую почитать про Inversion of Control (IoC) и Dependency Injection (DI) - это вещи, которые вы захотите использовать с таким подходом.