Artigos

Aberto / Pechado, segundo o principio SÓLIDO

As entidades de software (clases, módulos, funcións, etc.) deberían estar abertas para a extensión, pero pechadas para a súa edición.

Deseñando o software: módulos, clases e funcións de tal xeito que cando se necesita unha nova funcionalidade, non debemos modificar o código existente senón escribir un novo código que será usado polo código existente. Isto pode parecer estraño, especialmente con linguaxes como Java, C, C ++ ou C # onde se aplica non só ao código fonte en si, senón tamén ao binario. Queremos crear novas funcións de xeito que non requiran redistribución de binarios, executables ou DLL existentes.
OCP no contexto SÓLIDO

 

SRP e OCP complementarios

Xa vimos o principio de responsabilidade única do SRP que establece que un módulo só debería ter unha razón para cambiar. Os principios OCP e SRP son complementarios. O código deseñado seguindo o principio SRP tamén respectará os principios OCP. Cando temos un código que só ten un motivo para cambiar, a introdución dunha nova característica creará un motivo secundario para ese cambio. Polo tanto, tanto SRP como OCP serían incumpridos. Do mesmo xeito, se temos un código que só debería cambiar cando cambia a súa función principal e debe permanecer inalterado cando se engade nova funcionalidade, respectando así o OCP, tamén respectará o SRP.
Isto non significa que o SRP sempre leve a OCP ou viceversa, pero na maioría dos casos se se respecta un deles, lograr o segundo é bastante sinxelo.

 

Exemplo de violación do principio OCP

Desde un punto de vista puramente técnico, o principio aberto / pechado é moi sinxelo. Unha simple relación entre dúas clases, como a seguinte, viola o principio OCP.

A clase Usuario usa a clase Lóxica directamente. Se necesitamos implementar unha segunda clase de lóxica dun xeito que nos permita usar tanto a actual como a nova, haberá que cambiar a clase de lóxica existente. O usuario está directamente ligado á implementación da lóxica, non hai ningunha forma de proporcionar unha nova lóxica sen afectar á actual. E cando falamos de idiomas de tipo estático, é moi probable que a clase User tamén precise modificacións. Se falamos de linguaxes compiladas, seguramente tanto o executable de usuario como o executable de Logic ou a biblioteca dinámica requirirán recompilación e entrega, preferible para evitar cando sexa posible.

Con referencia ao esquema anterior, podemos deducir que calquera clase que empregue directamente outra clase podería levar á violación do principio Aberto / Pechado. 
Supoñamos que queremos escribir unha clase capaz de proporcionar o progreso "en porcentaxe" dun ficheiro descargado, a través da nosa aplicación. Teremos dúas clases principais, un Progreso e un Arquivo, e supoño que nos gustaría usalos do seguinte xeito:

 

función testItCanGetTheProgressOfAFileAsAPercent () {
     $ file = new File ();
     $ ficheiro-> lonxitude = 200;
     $ ficheiro-> enviado = 100;
     $ progreso = novo progreso ($ ficheiro);
     $ this-> assertEquals (50, $ progress-> getAsPercent ());
}

Neste código somos usuarios de Progress. Queremos obter un valor como porcentaxe, independentemente do tamaño real do ficheiro. Usamos Ficheiro como fonte de información. Un ficheiro ten unha lonxitude en bytes e un campo chamado enviado que representa a cantidade de datos enviados ao descargador. Non nos importa como se actualizan estes valores na aplicación. Podemos supor que hai algunha lóxica máxica que nos fai isto, polo que nunha proba podemos establecelos explícitamente.

 

ficheiro de clase {
     lonxitude pública $;
     $ público enviado;
}

 

A clase File é só un simple obxecto de datos que contén os dous campos. Por suposto, tamén debe conter outra información e comportamentos, como nome de ficheiro, ruta, ruta relativa, directorio actual, tipo, permisos, etc.

 

progreso da clase {

     $ file privado;

     function __construct (File $ file) {
          $ this-> file = $ file;
     }

     función getAsPercent () {
          devolver $ this-> file-> sent * 100 / $ this-> file-> length;
     }

}

Progress é simplemente unha clase que acepta un ficheiro no seu constructor. Para maior claridade, especificamos o tipo de variable nos parámetros do constructor. Hai un único método útil en Progress, getAsPercent (), que tomará os valores e a lonxitude enviados de Ficheiro e os converterá nunha porcentaxe. Sinxelo e funciona.

Este código parece ser correcto, con todo viola o principio Aberto / Pechado.

Pero por qué?

E como?

 

Tentemos cambiar os requisitos

Cada aplicación para evolucionar co paso do tempo necesitará novas funcións. Unha nova característica para a nosa aplicación podería ser permitir a transmisión de música en lugar de só descargar ficheiros. A lonxitude do ficheiro represéntase en bytes, a duración da música en segundos. Queremos ofrecer unha barra de progreso aos nosos oíntes, pero podemos reutilizar a clase escrita anteriormente?

Non, non podemos. A nosa progresión está obrigada a Arquivo. Só pode xestionar información de ficheiros, aínda que tamén se pode aplicar a contido musical. Pero para iso temos que modificalo, temos que facer que Progress coñeza a música e os arquivos. Se o noso deseño cumpra o OCP, non teríamos que tocar Ficheiro ou Progreso. Poderiamos reutilizar o progreso existente e aplicalo á música.

 

Boletín de innovación
Non te perdas as novidades máis importantes sobre innovación. Rexístrese para recibilos por correo electrónico.

Posible solución

As linguaxes escritas dinámicamente teñen a vantaxe de xestionar tipos de obxectos en tempo de execución. Isto permítenos eliminar o tipohint do constructor Progress e o código seguirá funcionando.

progreso da clase {

     $ file privado;

     función __construct ($ file) {
         $ this-> file = $ file;
     }

    función getAsPercent () {
         devolver $ this-> file-> sent * 100 / $ this-> file-> length;
     }

}

Agora podemos lanzar calquera cousa en Progress. E con calquera cousa, quero dicir literalmente calquera cousa:

clase de música {

lonxitude pública $;
$ público enviado;

$ artista público;
$ álbum público;
$ $ releaseDate;

función getAlbumCoverFile () {
devolver "Imaxes / Portadas /". $ this-> artista. '/'. $ este-> álbum. '.png';
}
}

E a clase de Música como a anterior funcionará perfectamente. Podemos probalo facilmente cunha proba moi similar a File.
function testItCanGetTheProgressOfAMusicStreamAsAPercent () {
$ music = new Music ();
$ música-> lonxitude = 200;
$ música-> enviado = 100;

$ progreso = novo progreso ($ música);

$ this-> assertEquals (50, $ progress-> getAsPercent ());
}

Polo tanto, basicamente calquera contido medible pode usarse coa clase Progress. Quizais deberiamos expresalo en código cambiando tamén o nome da variable:

progreso da clase {

privado $ medibleContido;

función __construír ($ medibleContido) {
$ isto-> medibleContento = $ medibleContido;
}

función getAsPercent () {
devolver $ this-> MeasurableContent-> sent * 100 / $ this-> MeasurableContent-> length;
}

}

Cando especificamos Ficheiro como o tipo de indicación, fomos optimistas sobre o que pode tratar a nosa clase. Era explícito e se chegaba algo máis, un gran erro diríanos.

Uclase na que anula un método dunha clase base de xeito que a clase derivada non cumpre o contrato de clase base. 

Non queremos acabar intentando chamar a métodos ou acceder a campos en obxectos que non se axusten ao noso contrato. Cando tivemos un tipo de indicación, o contrato foi especificado por el. Os campos e métodos da clase File. Agora que non temos nada, podemos enviar calquera cousa, incluso unha cadea e resultaría nun mal erro.

Aínda que o resultado final é o mesmo en ambos os casos, o que significa que o código rompe, o primeiro produciu unha boa mensaxe. Isto, porén, é moi escuro. Non hai forma de saber cal é a variable (unha cadea no noso caso) e que propiedades se buscaron e non se atoparon. É difícil depurar e solucionar o problema. Un programador debe abrir a clase Progress, lela e entendela. O contrato, neste caso, cando non especificas explícitamente o tipo de indicación, é defirematada polo comportamento do Progreso. É un contrato implícito que só coñece Progress. No noso exemplo, é defiAccedendo aos dous campos, enviado e lonxitude, no método getAsPercent(). Na vida real o contrato implícito pode ser moi complexo e difícil de descubrir con só buscar uns segundos na clase.

Esta solución só se recomenda se ningún dos outros consellos a continuación pode implementarse facilmente ou se causarían cambios arquitectónicos graves que non xustifican o esforzo.

Ercole Palmeri

Boletín de innovación
Non te perdas as novidades máis importantes sobre innovación. Rexístrese para recibilos por correo electrónico.

Artigos recentes

Os beneficios das páxinas para colorear para nenos: un mundo de maxia para todas as idades

O desenvolvemento da motricidade fina a través da cor prepara aos nenos para habilidades máis complexas como escribir. Para colorear…

2 maio 2024

O futuro está aquí: como a industria do transporte marítimo está revolucionando a economía global

O sector naval é unha verdadeira potencia económica mundial, que navega cara a un mercado de 150 millóns...

1 maio 2024

Editores e OpenAI asinan acordos para regular o fluxo de información procesada pola Intelixencia Artificial

O pasado luns, o Financial Times anunciou un acordo con OpenAI. FT licencia o seu xornalismo de clase mundial...

Abril 30 2024

Pagos en liña: aquí tes como os servizos de streaming che fan pagar para sempre

Millóns de persoas pagan por servizos de streaming, pagando taxas de subscrición mensuais. É unha opinión común que vostede...

Abril 29 2024