Principio de responsabilidade única, tamén coñecido como principio de responsabilidade única,
tamén coñecido como o principio de variabilidade uniforme: un tipo moi esvaradío de entender e unha pregunta tan nerviosa nunha entrevista de programador.
O meu primeiro coñecemento serio con este principio tivo lugar a principios do primeiro ano, cando os mozos e os verdes foron levados ao bosque para facer estudantes de larvas, verdadeiros estudantes.
No bosque, dividímonos en grupos de 8-9 persoas cada un e tivemos unha competición: que grupo bebería unha botella de vodka máis rápido, sempre que a primeira persoa do grupo verte vodka nun vaso, o segundo beba, e o terceiro merenda. A unidade que completou a súa operación móvese ao final da cola do grupo.
O caso no que o tamaño da cola era múltiplo de tres foi unha boa implementación de SRP.
Definición 1. Responsabilidade única.
A definición oficial do Principio de Responsabilidade Única (SRP) establece que cada entidade ten a súa propia responsabilidade e razón de existencia, e só ten unha responsabilidade.
Considere o obxecto "Bebedor" (Tippler).
Para implementar o principio SRP, dividiremos as responsabilidades en tres:
- Un verte (Operación de vertedura)
- Un bebe (Operación DrinkUp)
- Un merenda (TakeBiteOperation)
Cada un dos participantes no proceso é responsable dun compoñente do proceso, é dicir, ten unha responsabilidade atómica: beber, verter ou merenda.
O bebedoiro, á súa vez, é unha fachada para estas operacións:
сlass Tippler {
//...
void Act(){
_pourOperation.Do() // налить
_drinkUpOperation.Do() // выпить
_takeBiteOperation.Do() // закусить
}
}
Por que?
O programador humano escribe código para o home-mono, e o home-mono é desatento, estúpido e sempre ten présa. Pode manter e comprender entre 3 e 7 termos á vez.
No caso dun borracho, hai tres destes termos. Non obstante, se escribimos o código cunha soa folla, entón conterá mans, lentes, pelexas e interminables discusións sobre política. E todo isto estará no corpo dun método. Estou seguro de que xa viches ese código na túa práctica. Non é a proba máis humana para a psique.
Por outra banda, o home mono está deseñado para simular obxectos do mundo real na súa cabeza. Na súa imaxinación, pode xuntalos, montar novos obxectos a partir deles e desmontalos do mesmo xeito. Imaxina un modelo de coche vello. Na túa imaxinación, podes abrir a porta, desenroscar o revestimento da porta e ver alí os mecanismos de elevación da fiestra, dentro dos cales haberá engrenaxes. Pero non podes ver todos os compoñentes da máquina ao mesmo tempo, nunha "lista". Polo menos o "home mono" non pode.
Polo tanto, os programadores humanos descompoñen mecanismos complexos nun conxunto de elementos menos complexos e de traballo. Non obstante, pódese descompoñer de diferentes xeitos: en moitos coches antigos, o conduto de aire entra na porta e, nos coches modernos, un fallo na electrónica da pechadura impide que o motor se arranque, o que pode ser un problema durante as reparacións.
Así, SRP é un principio que explica COMO descompoñer, é dicir, onde trazar a liña divisoria.
Di que é necesario descompoñerse segundo o principio de división da "responsabilidade", é dicir, segundo as tarefas de determinados obxectos.
Volvemos á bebida e ás vantaxes que recibe o home mono durante a descomposición:
- O código quedou moi claro en todos os niveis
- O código pode ser escrito por varios programadores á vez (cada un escribe un elemento separado)
- As probas automatizadas simplifícanse: canto máis sinxelo sexa o elemento, máis fácil será probalo
- Aparece a composicionalidade do código: pode substituír Operación DrinkUp a unha operación na que un borracho bota líquido debaixo da mesa. Ou substituír a operación de vertedura por unha operación na que mesturas viño e auga ou vodka e cervexa. Dependendo dos requisitos empresariais, podes facer todo sen tocar o código do método Tippler.Act.
- A partir destas operacións podes dobrar o glotón (usando só TakeBitOperation), Alcohólico (só consumir Operación DrinkUp directamente da botella) e cumpre moitos outros requisitos comerciais.
(Oh, parece que este xa é un principio OCP, e violei a responsabilidade desta publicación)
E, por suposto, os contras:
- Teremos que crear máis tipos.
- Un borracho bebe por primeira vez un par de horas máis tarde do que tería doutro xeito.
Definición 2. Variabilidade unificada.
Permítanme, señores! A clase de bebidas tamén ten unha única responsabilidade: bebe! E, en xeral, a palabra "responsabilidade" é un concepto moi vago. Alguén é responsable do destino da humanidade e alguén é responsable de criar os pingüíns que foron envorcados no polo.
Consideremos dúas implementacións do bebedor. O primeiro, mencionado anteriormente, contén tres clases: verter, beber e merenda.
O segundo está escrito a través da metodoloxía "Forward and Only Forward" e contén toda a lóxica do método Actuar:
//Не тратьте время на изучение этого класса. Лучше съешьте печеньку
сlass BrutTippler {
//...
void Act(){
// наливаем
if(!_hand.TryDischarge(from:_bottle, to:_glass, size:_glass.Capacity))
throw new OverdrunkException();
// выпиваем
if(!_hand.TryDrink(from: _glass, size: _glass.Capacity))
throw new OverdrunkException();
//Закусываем
for(int i = 0; i< 3; i++){
var food = _foodStore.TakeOrDefault();
if(food==null)
throw new FoodIsOverException();
_hand.TryEat(food);
}
}
}
Ambas clases, desde o punto de vista dun observador externo, parecen exactamente iguais e comparten a mesma responsabilidade de "beber".
Confusión!
Despois entramos en liña e descubrimos outra definición de SRP: o Principio único de cambiabilidade.
SCP afirma que "Un módulo ten un e só un motivo para cambiar". É dicir, "A responsabilidade é un motivo de cambio".
(Parece que os mozos que elaboraron a definición orixinal confiaban nas habilidades telepáticas do home mono)
Agora todo cae no seu lugar. Por separado, podemos cambiar os procedementos de vertedura, bebida e merenda, pero no propio bebedor só podemos cambiar a secuencia e composición das operacións, por exemplo, movendo a merenda antes de beber ou engadindo a lectura dun brindis.
No enfoque "Forward and Only Forward", todo o que se pode cambiar só se cambia no método Actuar. Isto pode ser lexible e efectivo cando hai pouca lóxica e raramente cambia, pero moitas veces acaba en métodos terribles de 500 liñas cada un, con máis declaracións if das necesarias para que Rusia se una á OTAN.
Definición 3. Localización de cambios.
Os bebedores moitas veces non entenden por que espertaron no apartamento doutra persoa ou onde está o seu teléfono móbil. É hora de engadir un rexistro detallado.
Comecemos a rexistrar co proceso de vertedura:
class PourOperation: IOperation{
PourOperation(ILogger log /*....*/){/*...*/}
//...
void Do(){
_log.Log($"Before pour with {_hand} and {_bottle}");
//Pour business logic ...
_log.Log($"After pour with {_hand} and {_bottle}");
}
}
Encapsulándoo Operación de vertedura, actuamos sabiamente dende o punto de vista da responsabilidade e do encapsulamento, pero agora confúndese co principio de variabilidade. Ademais da propia operación, que pode cambiar, o propio rexistro tamén se fai modificable. Terás que separar e crear un rexistrador especial para a operación de vertedura:
interface IPourLogger{
void LogBefore(IHand, IBottle){}
void LogAfter(IHand, IBottle){}
void OnError(IHand, IBottle, Exception){}
}
class PourOperation: IOperation{
PourOperation(IPourLogger log /*....*/){/*...*/}
//...
void Do(){
_log.LogBefore(_hand, _bottle);
try{
//... business logic
_log.LogAfter(_hand, _bottle");
}
catch(exception e){
_log.OnError(_hand, _bottle, e)
}
}
}
O lector meticuloso notarao LogAfter, LogBefore и OnError tamén se pode cambiar individualmente e, por analoxía cos pasos anteriores, crearase tres clases: PourLoggerBefore, PourLoggerAfter и PourErrorLogger.
E lembrando que hai tres operacións para un bebedor, temos nove clases de tala. Como resultado, todo o círculo de bebidas consta de 14 (!!!) clases.
¿Hipérbola? Dificilmente! Un home mono cunha granada de descomposición dividirá o "vertidor" nun decantador, un vaso, operadores de vertedura, un servizo de abastecemento de auga, un modelo físico de colisión de moléculas, e durante o próximo trimestre tentará desenredar as dependencias sen variables globais. E créame, non parará.
É neste momento cando moitos chegan á conclusión de que os SRP son contos de fadas dos reinos rosas, e van xogar aos fideos...
... sen coñecer nunca a existencia dunha terceira definición de Srp:
“O Principio de Responsabilidade Única establece que as cousas que son similares ao cambio deben almacenarse nun só lugar". ou "O que cambia xuntos debe manterse nun só lugar"
É dicir, se cambiamos o rexistro dunha operación, entón debemos cambiala nun só lugar.
Este é un punto moi importante, xa que todas as explicacións de SRP anteriores dicían que era necesario esmagar os tipos mentres se esmagaban, é dicir, impuxeron un "límite superior" ao tamaño do obxecto, e agora xa estamos a falar dun “límite inferior” . Noutras palabras, SRP non só require "esmagar mentres se esmaga", senón tamén non esaxere: "non esmague as cousas entrelazadas". Esta é a gran batalla entre a navalla de Occam e o home mono!
Agora o bebedor debería sentirse mellor. Ademais de que non é necesario dividir o rexistrador IPourLogger en tres clases, tamén podemos combinar todos os rexistradores nun só tipo:
class OperationLogger{
public OperationLogger(string operationName){/*..*/}
public void LogBefore(object[] args){/*...*/}
public void LogAfter(object[] args){/*..*/}
public void LogError(object[] args, exception e){/*..*/}
}
E se engadimos un cuarto tipo de operación, entón o rexistro para ela xa está listo. E o código das operacións en si está limpo e libre de ruído da infraestrutura.
Como resultado, temos 5 clases para resolver o problema da bebida:
- Operación de vertedura
- Operación de beber
- Operación de atasco
- Logger
- Fachada de bebedor
Cada un deles é responsable estritamente dunha funcionalidade e ten un motivo para cambiar. Todas as regras similares ao cambio están nas proximidades.
Exemplo da vida real
Unha vez escribimos un servizo para rexistrar automaticamente un cliente b2b. E apareceu un método GOD para 200 liñas de contido semellante:
- Vai a 1C e crea unha conta
- Con esta conta, vai ao módulo de pago e créao alí
- Comprobe que non se creou unha conta con tal conta no servidor principal
- Crea unha conta nova
- Engade os resultados do rexistro no módulo de pago e o número 1c ao servizo de resultados do rexistro
- Engade información da conta a esta táboa
- Crea un número de punto para este cliente no servizo de puntos. Pasa o teu número de conta 1c a este servizo.
E había preto de 10 operacións comerciais máis nesta lista cunha conectividade terrible. Case todos necesitaban o obxecto da conta. O ID do punto e o nome do cliente foron necesarios na metade das chamadas.
Despois dunha hora de refactorización, puidemos separar o código de infraestrutura e algúns dos matices de traballar cunha conta en métodos/clases separados. O método de Deus fíxoo máis fácil, pero quedaban 100 liñas de código que simplemente non querían ser desenredadas.
Só despois duns días quedou claro que a esencia deste método "lixeiro" é un algoritmo empresarial. E que a descrición orixinal das especificacións técnicas era bastante complexa. E é o intento de romper este método en anacos o que violará o SRP, e non viceversa.
Formalismo.
É hora de deixar só o noso borracho. Seca as túas bágoas: definitivamente volveremos a el algún día. Agora imos formalizar o coñecemento deste artigo.
Formalismo 1. Definición de SRP
- Separa os elementos para que cada un deles sexa responsable dunha cousa.
- A responsabilidade significa "razón para cambiar". É dicir, cada elemento só ten un motivo de cambio, en termos de lóxica empresarial.
- Posibles cambios na lóxica empresarial. debe ser localizado. Os elementos que cambian de forma sincronizada deben estar preto.
Formalismo 2. Criterios de autotest necesarios.
Non vin criterios suficientes para cumprir o SRP. Pero hai condicións necesarias:
1) Pregúntate que fai esta clase/método/módulo/servizo. debes contestalo cunha definición sinxela. ( Grazas
explicacións
Non obstante, ás veces é moi difícil atopar unha definición sinxela
2) Corrixir un erro ou engadir unha nova función afecta a un número mínimo de ficheiros/clases. Idealmente - un.
explicacións
Dado que a responsabilidade (por unha función ou erro) está encapsulada nun ficheiro/clase, sabe exactamente onde buscar e que editar. Por exemplo: a función de cambiar a saída das operacións de rexistro requirirá cambiar só o rexistrador. Non é necesario percorrer o resto do código.
Outro exemplo é engadir un novo control de IU, semellante aos anteriores. Se isto obriga a engadir 10 entidades diferentes e 15 conversores diferentes, parece que estás esaxerando.
3) Se varios desenvolvedores están a traballar en funcións diferentes do seu proxecto, entón a probabilidade dun conflito de combinación, é dicir, a probabilidade de que varios desenvolvedores cambien ao mesmo tempo o mesmo ficheiro/clase é mínima.
explicacións
Se, ao engadir unha nova operación "Verter vodka debaixo da mesa", cómpre afectar ao rexistrador, a operación de beber e verter, entón parece que as responsabilidades están divididas de forma torcida. Por suposto, isto non sempre é posible, pero debemos tentar reducir esta cifra.
4) Cando se lle fai unha pregunta aclaratoria sobre a lóxica empresarial (dun desenvolvedor ou xestor), entra estrictamente a unha clase/ficheiro e só recibe información desde alí.
explicacións
As características, as regras ou os algoritmos están escritos de forma compacta, cada un nun só lugar, e non están espallados con bandeiras polo espazo do código.
5) A denominación é clara.
explicacións
A nosa clase ou método é responsable dunha cousa, e a responsabilidade reflíctese no seu nome
AllManagersManagerService - moi probablemente unha clase de Deus
LocalPayment - probablemente non
Formalismo 3. Metodoloxía de desenvolvemento Occam-first.
Ao comezo do deseño, o home mono non sabe e non sente todas as sutilezas do problema que se está a resolver e pode cometer un erro. Podes cometer erros de diferentes xeitos:
- Fai que os obxectos sexan demasiado grandes combinando diferentes responsabilidades
- Reformulación dividindo unha única responsabilidade en moitos tipos diferentes
- Definir incorrectamente os límites da responsabilidade
É importante lembrar a regra: "é mellor cometer un gran erro" ou "se non estás seguro, non o dividas". Se, por exemplo, a túa clase contén dúas responsabilidades, aínda é comprensible e pódese dividir en dúas con cambios mínimos no código do cliente. Ensamblar un vaso a partir de fragmentos de vidro adoita ser máis difícil debido ao contexto que se estende por varios ficheiros e á falta de dependencias necesarias no código do cliente.
É hora de chamalo día
O alcance do SRP non se limita a OOP e SOLID. Aplícase a métodos, funcións, clases, módulos, microservizos e servizos. Aplícase tanto ao desenvolvemento "figax-figax-and-prod" como ao desenvolvemento da "ciencia do foguete", facendo o mundo un pouco mellor en todas partes. Se pensas niso, este é case o principio fundamental de toda a enxeñaría. A enxeñaría mecánica, os sistemas de control e, de feito, todos os sistemas complexos están construídos a partir de compoñentes, e a "subfragmentación" priva aos deseñadores de flexibilidade, a "sobrefragmentación" priva aos deseñadores de eficiencia e os límites incorrectos privan de razón e tranquilidade.
SRP non é inventado pola natureza e non forma parte da ciencia exacta. Rompe coas nosas limitacións biolóxicas e psicolóxicas, é só unha forma de controlar e desenvolver sistemas complexos utilizando o cerebro do home-mono. El dinos como descompoñer un sistema. A formulación orixinal requiriu unha boa cantidade de telepatía, pero espero que este artigo limpa parte da cortina de fume.
Fonte: www.habr.com