Задать вопрос
kot2566
@kot2566

Как провести соответствие между строкой и классом с точки зрения SOLID?

Здравствуйте. Подскажите как правильно соотнести например, получаемую Строку извне строку и Класс
и при этом выполнить Принцип открытости/закрытости из SOLID?

Например, извне я получаю строку $animal_type="cat" и должен вызвать метод say() класса Cat;

Первый вариант: через оператор switch
class Human {
      public function touchAnimal($animal_type)
    {
        switch ($animal_type) {
            case 'dog':
                $animal = new Dog();
                break;
            case 'cat':
                $animal = new Cat();
                break;
        }
        $animal->say();
    }
}


Второй вариант: массив соответствий
class Human {
      $home_animals = [
            'dog' => Dog::class,
            'cat' => Cat::class,
        ];

    public function touchAnimal($animal_type)
    {
        $animal = new $this->home_animals[$animal_type](); 
        $animal->say();  // IDE не знает объектом какого класса является переменная и не подсказывает 
    }
}


Третий правильный вариант:
class Human {
    public function touchAnimal($animal: VoiceAnimal) // или интерфейс IVoiceAnimal
    {
        $animal->say();
    }
}


В третьем случае мы выносим логику соотношения наверх, за пределы класса и выполняем принцип.
Но наверху всё равно надо будет делать этот выбор switch, в каком-нибудь другом классе и уже там нарушать принцип открытости/закрытости.
Как правильно сделать в данном случае?

Просто содержимое классов
class VoiceAnimal
{
    public $sound = '';

    public function say()
    {
        print_r('Animal says: ' . $this->sound);
    }
}

class Dog extends VoiceAnimal
{
    public $sound = 'GAV-GAV';

    public function say()
    {
        print_r('DOG SAY: ' . $this->sound);
    }
}

class Cat extends VoiceAnimal
{
    public $sound = 'MYAU';

    public function say()
    {
        print_r('CAT SAY: ' . $this->sound);
    }
}
  • Вопрос задан
  • 246 просмотров
Подписаться 2 Простой Комментировать
Решения вопроса 2
FanatPHP
@FanatPHP
Чебуратор тега РНР
Это всё очень плохо.
В первом варианте человек должен знать, как мяукают кошки, а в последнем "правильном" варианте человек трогает не кошку, а кошачий голос(?!).
В "идеальном" варианте опять же выбирается не животное, которое надо погладить, а его голос.

Чтобы следовать принципам солид, надо понять в первую очередь ЗАЧЕМ это всё делается.
А делается это для того чтобы уменьшить связность. Чтобы класс, использующий какой-либо функционал, не знал деталей его реализации. И, соответственно, мы могли бы менять реализацию без опасения поломать что-то в классе-пользователе.

При этом extends, кроме как от абстрактного класса, эту связность всегда увеличивает.
И его надо избегать. А использовать принцип Composition over inheritance. То есть нужный функционал получать не наследованием, а передачей независимых функциональных модулей в виде параметров.

Соответственно, нам надо сделать иерархию: голос - животное - потрогать.
И вот теперь у нас хоть голос, хоть животное, будут открыты для каких угодно изменений, до тех пор пока они поддерживают публичный контракт.
/ ******* голоса *******/
abstract class VoiceEngine {
	public function getVoice() {}
}
class CatVoiceEngine extends VoiceEngine {
	public function getVoice() {
		return "Meow!";
	}
}
class DogVoiceEngine extends VoiceEngine {
	public function getVoice() {
		return "Bark!";
	}
}
class HumanVoiceEngine extends VoiceEngine {
	public function getVoice() {
		return "Да пошёл ты!";
	}
}
/ ******* животные *******/
abstract class Animal {
	public function __construct(public VoiceEngine $voiceEngine) {}
	public function say() {
		echo $this->voiceEngine->getVoice();
	}
}
class Cat extends Animal{}
class Dog extends Animal{}
class Human extends Animal{
	public function touchAnimal(Animal $animal) {
		$animal->say();
	}
}
/ ******* исполнение *******/
$cat = new Cat(new CatVoiceEngine());
$dog = new Dog(new DogVoiceEngine());
$human = new Human(new HumanVoiceEngine());
$human->touchAnimal($cat);
$human->touchAnimal($dog);
$human->touchAnimal($human);


После того как я, раздуваясь от гордости, написал этот ответ, до меня вдруг дошло что на вопрос-то я так и не ответил.
Соответственно, задачу выбора животного возлагаем на отдельную сущность:

class AnimalFactory {
    public static function create($type) {
        return match($type) {
            'cat' => new Cat(new CatVoiceEngine()),
            'dog' => new Dog(new DogVoiceEngine()),
            'human' => new Human(new HumanVoiceEngine()),
        };
    }
}
$human = new Human(new HumanVoiceEngine());
$human->touchAnimal(AnimalFactory::create('cat'));

В итоге мы вернулись к тому же кейсу (match - это улучшенный case), но при этом у нас всё разделено, и каждый класс занимается строго своим делом.
Ответ написан
@Akela_wolf
Extreme Programmer
Если уж совсем идеально, то:
interface Voice {
  void say();
}

class Dog implements Voice { ... }
class Cat implements Voice { ... }

interface VoiceFactory {
  Voice getVoice(String voiceType);
}

class VoiceFactoryImpl implements VoiceFactory {
  private Map<String, Supplier<Voice>> suppliers = new HasMap<>();
  
  void addSupplier(String type, Supplier<Voice> supplier) { suppliers.put(type, supplier); }

  @Override
  Voice getVoice(String type) {
    final Supplier<Voice> supplier = suppliers.get(type);
    if (supplier != null) {
      return supplier.get();
    } else {
      throw new RuntimeException("No supplier for type: "+type);
    }
  }
}


Класс VoiceFactoryImpl выполняет принцип OCP - открыт для дополнений через метод addSupplier, в него можно добавлять новые сопоставления строка - животное и закрыт для изменений.
Ответ написан
Пригласить эксперта
Ваш ответ на вопрос

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

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