Принцип единой ответственности (Single Responsibility Principle) гласит, что у каждого класса должна быть только одна «ответственность» и он не должен брать на себя другие обязанности. Роберт К. Мартин объяснял его так: «У класса должна быть лишь одна причина для изменения». Давайте в качестве примера возьмем приложение телефонного справочника. Мы будем делать телефонный справочник, в котором будет класс TelephoneDirectory. Он будет «нести ответственность» за ведение записей справочника, то есть телефонных номеров и названий организаций, которым принадлежат номера. Ожидается, что класс будет выполнять следующие операции:
добавлять новую запись (Name и Telephone Number)
удалять существующую запись
изменять номер телефона, присвоенный сущности Name
предоставлять поиск, который будет возвращать номер, присвоенный сущности Name.
Класс TelephoneDirectory может выглядеть следующим образом:
Сейчас наш класс TelephoneDirectory выглядит хорошо, в нем точно реализованы ожидаемые функции: А теперь скажем, что в проекте есть еще два требования – Сохранить содержимое справочника в базе данных и перенести содержимое справочника в файл. Теперь добавим еще два метода в класс TelephoneDirectory, как показано ниже:
classTelephoneDirectory:def__init__(self):self.telephone_directory=dict()defadd_entry(self,name,number):self.telephone_directory[name]=numberdefdelete_entry(self,name):delself.telephone_directory[name]defupdate_entry(self,name,number):self.telephone_directory[name]=numberdeflookup_number(self,name):returnself.telephone_directory.get(name)defsave_to_file(self,file_name,location):#код для сохранения телефонного справочника в файлpassdefpersist_to_database(self,database_details):#код для сохранения телефонного справочника в базу данныхpassdef__str__(self):ret_dct=''forkey,valueinself.telephone_directory.items():ret_dct+=f'{key} : {value}\n'returnret_dct
<?phpclassTelephoneDirectory{privatearray$telephone_directory=[];publicfunctionadd_entry(string$name,string$number){$this->telephone_directory[$name]=$number;}publicfunctiondelete_entry(string$name){if(array_key_exists($name,$this->$telephone_directory)){unset($this->telephone_directory[$name]);}}publicfunctionupdate_entry(string$name,string$number){$this->telephone_directory[$name]=$number;}publicfunctionlookup_number(string$name){if(array_key_exists($name,$this->$telephone_directory)){return$this->telephone_directory[$name];}returnnull;}publicfunction__toString(){$data=[];foreach($this->$telephone_directoryas$name=>$phone){$data[]=$name.' : '.$phone;}returnimplode(PHP_EOL,$data);}publicfunctionsave_to_file(string$file_name,string$location){// код для сохранения телефонного справочника в файл}publicfunctionpersist_to_database(string$database_details){// код для сохранения телефонного справочника в базу данных}}
Так вот, именно сейчас мы нарушили принцип единственной ответственности. Добавив функции сохранения в базу данных и сохранения в файл, мы дали классу дополнительные обязанности, которые не входят в его основную зону ответственности. Теперь в классе есть дополнительные функции, которые могут привести к его изменению. В будущем, если появятся какие-то требования, связанные с сохранением данных, это может привести к изменениям в классе TelephoneDirectory. Получается, что класс TelephoneDirectory подвержен изменениям по причинам, которые не являются его основной ответственностью. Принцип единственной ответственности требует от нас не добавлять дополнительные обязанности к классу, чтобы нам не приходилось менять класс, когда нам нужно изменить функционал сохранения справочника в базу данных или в файл. Мы можем передать экземпляр класса TelephoneDirectory экземплярам этих классов и записать любые дополнительные функции в них. Так мы гарантируем, что у класса TelephoneDirectory есть лишь одна причина для изменения – это изменения в его основной «ответственности».
<?phpclassPersistToDatabase{publicfunction__construct(TelephoneDirectory$object_to_persist){// сохранение в базу данных}}classSaveToFile{publicfunction__construct(TelephoneDirectory$object_to_save){// сохранение в файл}}
Задачи
Задача 1. Разделение класса пользователя
Дан класс User, который хранит данные пользователя, проверяет email и отправляет приветственное письмо. Нарушает ли он SRP? Разбейте его на классы с единственной ответственностью.
# Нарушение SRP — один класс делает слишком многоclassUser:def__init__(self,name:str,email:str):self.name=nameself.email=emaildefvalidate_email(self)->bool:return'@'inself.emaildefsend_welcome_email(self):print(f"Отправка письма на {self.email}: Добро пожаловать, {self.name}!")# Вызовuser=User("Алексей","alex@example.com")user.send_welcome_email()
<?php// Нарушение SRP — один класс делает слишком многоclassUser{publicstring$name;publicstring$email;publicfunction__construct(string$name,string$email){$this->name=$name;$this->email=$email;}publicfunctionvalidate_email():bool{returnstr_contains($this->email,'@');}publicfunctionsend_welcome_email():void{echo"Отправка письма на {$this->email}: Добро пожаловать, {$this->name}!\n";}}// Вызов$user=newUser("Алексей","alex@example.com");$user->send_welcome_email();
Задача 2. Отчёт о продажах
Класс SalesReport формирует данные отчёта и сразу выводит его в HTML. Выделите форматирование в отдельный класс.
importcsv,io# Нарушение SRPclassCsvProcessor:defprocess(self,csv_content:str,output_path:str):reader=csv.DictReader(io.StringIO(csv_content))rows=[rowforrowinreader]# сразу пишем в файлwithopen(output_path,'w',newline='')asf:writer=csv.DictWriter(f,fieldnames=rows[0].keys())writer.writeheader()writer.writerows(rows)returnrows# Вызовcontent="name,age\nАнна,30\nИван,25"processor=CsvProcessor()data=processor.process(content,"/tmp/output.csv")forrowindata:print(row)
<?php// Нарушение SRPclassCsvProcessor{publicfunctionprocess(string$csv_content,string$output_path):array{$lines=explode("\n",trim($csv_content));$headers=str_getcsv(array_shift($lines));$rows=array_map(function($l){returnarray_combine($headers,str_getcsv($l))},$lines);// сразу пишем в файл$handle=fopen($output_path,'w');fputcsv($handle,$headers);foreach($rowsas$row){fputcsv($handle,$row);}fclose($handle);return$rows;}}// Вызов$content="name,age\nАнна,30\nИван,25";$processor=newCsvProcessor();$data=$processor->process($content,"/tmp/output.csv");foreach($dataas$row){echoimplode(', ',$row)."\n";}