Принцип инверсии зависимостей (Dependency Inversion Principle) гласит:
Модуль высокого уровня не должен зависеть от модулей низкого уровня. И то, и другое должно зависеть от абстракций.
Абстракции не должны зависеть от деталей реализации. Детали реализации должны зависеть от абстракций.
Если ваш код уже реализует принципы открытости/закрытости и подстановки Лисков, он уже будет неявно согласован с принципом инверсии зависимостей.
Следуя принципу открытости/закрытости, вы создаете интерфейсы, которые можно использовать для предоставления различных высокоуровневых реализаций. Следуя принципу подстановки Лисков, вы гарантируете, что сможете заменить экземпляры класса низкого уровня объектами класса высокого уровня без какого-либо негативного воздействия на приложение. Таким образом, следуя этим двум принципам, вы гарантируете, что ваши классы высокого уровня и классы низкого уровня зависят от интерфейсов. Следовательно, вы неявно следуете принципу инверсии зависимостей. Как показано в коде ниже, у нас есть класс Student, который мы используем для создания экземпляров Student и класса TeamMemberships, который содержит сведения о принадлежности учеников к разным командам. Теперь мы определим высокоуровневый класс Analysis, где нам нужно отсеять всех учеников, принадлежащих красной команде.
fromenumimportEnumfromabcimportabstractmethodclassTeams(Enum):BLUE_TEAM=1RED_TEAM=2GREEN_TEAM=3classStudent:def__init__(self,name):self.name=nameclassTeamMemberships():def__init__(self):self.team_memberships=[]defadd_team_memberships(self,student,team):self.team_memberships.append((student,team))classAnalysis():def__init__(self,team_student_memberships):memberships=team_student_memberships.team_membershipsformembersinmemberships:ifmembers[1]==Teams.RED_TEAM:print(f'{members[0].name} is in RED team')student1=Student('Ravi')student2=Student('Archie')student3=Student('James')team_memberships=TeamMemberships()team_memberships.add_team_memberships(student1,Teams.BLUE_TEAM)team_memberships.add_team_memberships(student2,Teams.RED_TEAM)team_memberships.add_team_memberships(student3,Teams.GREEN_TEAM)Analysis(team_memberships)
<?phpenumTeams{caseBLUE_TEAM;caseRED_TEAM;caseGREEN_TEAM;}classStudent{publicstring$name;publicfunction__construct(string$name){$this->name=$name;}}classTeamMemberships{publicarray$team_memberships=[];publicfunctionadd_team_memberships(Student$student,Teams$team){$this->team_memberships[]=[$student,$team];}}classAnalysis{publicfunction__construct(TeamMemberships$team_student_memberships){$memberships=$team_student_memberships->team_memberships;foreach($membershipsas$member){if($member[1]===Teams::RED_TEAM){echo$member[0]->name.' is in RED team';}}}}$student1=newStudent('Ravi');$student2=newStudent('Archie');$student3=newStudent('James');$team_memberships=newTeamMemberships();$team_memberships->add_team_memberships($student1,Teams::BLUE_TEAM);$team_memberships->add_team_memberships($student2,Teams::RED_TEAM);$team_memberships->add_team_memberships($student3,Teams::GREEN_TEAM);newAnalysis($team_memberships);
ArchieisinREDteam
Задачи
Задача 1. Отправка уведомлений
Класс OrderService напрямую создаёт экземпляр EmailSender и вызывает его метод. Нарушает ли это DIP? Перепроектируйте так, чтобы OrderService зависел от абстракции.
# Нарушение DIP — модуль высокого уровня зависит от конкретной реализацииclassEmailSender:defsend(self,recipient:str,message:str):print(f"Email → {recipient}: {message}")classOrderService:def__init__(self):self.notifier=EmailSender()# жёсткая зависимостьdefplace_order(self,order_id:int,email:str):print(f"Заказ #{order_id} оформлен")self.notifier.send(email,f"Ваш заказ #{order_id} принят")# Вызовservice=OrderService()service.place_order(101,"[email protected]")
<?php// Нарушение DIP — модуль высокого уровня зависит от конкретной реализацииclassEmailSender{publicfunctionsend(string$recipient,string$message):void{echo"Email → {$recipient}: {$message}\n";}}classOrderService{privateEmailSender$notifier;publicfunction__construct(){$this->notifier=newEmailSender();// жёсткая зависимость}publicfunctionplace_order(int$order_id,string$email):void{echo"Заказ #{$order_id} оформлен\n";$this->notifier->send($email,"Ваш заказ #{$order_id} принят");}}// Вызов$service=newOrderService();$service->place_order(101,"[email protected]");
Задача 2. Логирование в приложении
Класс UserService напрямую использует FileLogger для записи событий. Введите абстракцию логгера и внедрите зависимость через конструктор.
# Нарушение DIPclassPdfRenderer:defrender(self,content:str)->str:returnf"[PDF] {content}"classReportGenerator:def__init__(self):self.renderer=PdfRenderer()# жёсткая зависимостьdefgenerate(self,data:str)->str:content=f"Отчёт: {data}"returnself.renderer.render(content)# Вызовgenerator=ReportGenerator()print(generator.generate("Продажи за март"))
<?php// Нарушение DIPclassPdfRenderer{publicfunctionrender(string$content):string{return"[PDF] {$content}";}}classReportGenerator{privatePdfRenderer$renderer;publicfunction__construct(){$this->renderer=newPdfRenderer();// жёсткая зависимость}publicfunctiongenerate(string$data):string{$content="Отчёт: {$data}";return$this->renderer->render($content);}}// Вызов$generator=newReportGenerator();echo$generator->generate("Продажи за март").PHP_EOL;
Задача 4. Подключение к базе данных
Класс ProductRepository создаёт внутри себя MySQLConnection. Перепроектируйте так, чтобы репозиторий зависел от абстракции соединения, и можно было легко подключить PostgreSQL.
# Нарушение DIPclassMySQLConnection:defquery(self,sql:str)->list:print(f"[MySQL] Выполнение: {sql}")return[{'id':1,'name':'Товар A'}]classProductRepository:def__init__(self):self.db=MySQLConnection()# жёсткая зависимостьdeffind_all(self)->list:returnself.db.query("SELECT * FROM products")deffind_by_id(self,id:int)->dict:results=self.db.query(f"SELECT * FROM products WHERE id = {id}")returnresults[0]ifresultselse{}# Вызовrepo=ProductRepository()print(repo.find_all())print(repo.find_by_id(1))
<?php// Нарушение DIPclassMySQLConnection{publicfunctionquery(string$sql):array{echo"[MySQL] Выполнение: {$sql}\n";return[['id'=>1,'name'=>'Товар A']];}}classProductRepository{privateMySQLConnection$db;publicfunction__construct(){$this->db=newMySQLConnection();// жёсткая зависимость}publicfunctionfind_all():array{return$this->db->query("SELECT * FROM products");}publicfunctionfind_by_id(int$id):array{$results=$this->db->query("SELECT * FROM products WHERE id = {$id}");return$results[0]??[];}}// Вызов$repo=newProductRepository();print_r($repo->find_all());print_r($repo->find_by_id(1));
Задача 5. Кэширование данных
Класс ArticleService напрямую использует RedisCache для кэширования статей. Перепроектируйте с введением абстракции кэша, чтобы можно было подменить Redis на in-memory кэш в тестах.
<?php// Нарушение DIPclassRedisCache{privatearray$store=[];publicfunctionget(string$key):mixed{return$this->store[$key]??null;}publicfunctionset(string$key,mixed$value):void{$this->store[$key]=$value;echo"[Redis] Сохранено: {$key}\n";}}classArticleService{privateRedisCache$cache;publicfunction__construct(){$this->cache=newRedisCache();// жёсткая зависимость}publicfunctionget_article(int$article_id):array{$key="article:{$article_id}";$cached=$this->cache->get($key);if($cached!==null){return$cached;}$article=['id'=>$article_id,'title'=>"Статья #{$article_id}"];$this->cache->set($key,$article);return$article;}}// Вызов$service=newArticleService();print_r($service->get_article(1));print_r($service->get_article(1));// из кэша
Как видно из реализации, мы напрямую используем team_student_memberships. team_memberships в высокоуровневом классе Analysis, и мы используем реализацию этого списка непосредственно в классе высокого уровня. На данный момент все нормально, но представьте ситуацию, в которой нам нужно изменить эту реализацию со списка на что-то другое. В этом случае наш класс высокого уровня Analysis сломается, поскольку он зависит от деталей реализации TeamMemberships низкого уровня. Теперь взгляните на пример ниже, в котором мы меняем эту реализацию и приводим ее в соответствие с принципом инверсии зависимостей.
fromenumimportEnumfromabcimportabstractmethodclassTeams(Enum):BLUE_TEAM=1RED_TEAM=2GREEN_TEAM=3classTeamMembershipLookup():@abstractmethoddeffind_all_students_of_team(self,team):passclassStudent:def__init__(self,name):self.name=nameclassTeamMemberships(TeamMembershipLookup):def__init__(self):self.team_memberships=[]defadd_team_memberships(self,student,team):self.team_memberships.append((student,team))deffind_all_students_of_team(self,team):formembersinself.team_memberships:ifmembers[1]==team:yieldmembers[0].nameclassAnalysis():def__init__(self,team_membership_lookup):forstudentinteam_membership_lookup.find_all_students_of_team(Teams.RED_TEAM):print(f'{student} is in RED team.')student1=Student('Ravi')student2=Student('Archie')student3=Student('James')team_memberships=TeamMemberships()team_memberships.add_team_memberships(student1,Teams.BLUE_TEAM)team_memberships.add_team_memberships(student2,Teams.RED_TEAM)team_memberships.add_team_memberships(student3,Teams.GREEN_TEAM)Analysis(team_memberships)
<?phpenumTeams{caseBLUE_TEAM;caseRED_TEAM;caseGREEN_TEAM;}interfaceTeamMembershipLookup{publicfunctionfind_all_students_of_team(Teams$team);}classStudent{publicstring$name;publicfunction__construct(string$name){$this->name=$name;}}classTeamMembershipsimplementsTeamMembershipLookup{privatearray$team_memberships=[];publicfunctionadd_team_memberships(Student$student,Teams$team){$this->team_memberships[]=[$student,$team];}publicfunctionfind_all_students_of_team(Teams$team){returnarray_map(function($item){return$item[0]->name;},array_filter($this->team_memberships,function($item)use($team){return$item[1]===$team;}));}}classAnalysis{publicfunction__construct(TeamMemberships$team_student_memberships){$memberships=$team_student_memberships->find_all_students_of_team(Teams::RED_TEAM);foreach($membershipsas$member){echo$member.' is in RED team';}}}$student1=newStudent('Ravi');$student2=newStudent('Archie');$student3=newStudent('James');$team_memberships=newTeamMemberships();$team_memberships->add_team_memberships($student1,Teams::BLUE_TEAM);$team_memberships->add_team_memberships($student2,Teams::RED_TEAM);$team_memberships->add_team_memberships($student3,Teams::GREEN_TEAM);newAnalysis($team_memberships);