¿Estoy rompiendo la práctica de OOP con esta arquitectura?

23

Tengo una aplicación web No creo que la tecnología sea importante. La estructura es una aplicación de N niveles, que se muestra en la imagen de la izquierda. Hay 3 capas

UI (patrón MVC), capa de lógica de negocios (BLL) y capa de acceso a datos (DAL)

El problema que tengo es que mi BLL es enorme, ya que tiene la lógica y las rutas a través de la llamada de eventos de la aplicación.

Un flujo típico a través de la aplicación podría ser:

Evento disparado en la interfaz de usuario, atravesar un método en el BLL, realizar la lógica (posiblemente en varias partes del BLL), eventualmente al DAL, volver al BLL (donde probablemente haya más lógica) y luego devolver algún valor a la UI.

El BLL en este ejemplo está muy ocupado y estoy pensando cómo dividir esto. También tengo la lógica y los objetos combinados que no me gustan.

ingrese la descripción de la imagen aquí

La versión de la derecha es mi esfuerzo.

La lógica sigue siendo cómo fluye la aplicación entre UI y DAL, pero es probable que no haya propiedades ... Solo métodos (la mayoría de las clases en esta capa podrían ser estáticas ya que no almacenan ningún estado). La capa Poco es donde existen clases que tienen propiedades (como una clase Persona donde habría nombre, edad, altura, etc.). Estos no tendrían nada que ver con el flujo de la aplicación, solo almacenan estado.

El flujo podría ser:

Incluso se activa desde la interfaz de usuario y pasa algunos datos al controlador de capa de interfaz de usuario (MVC). Esto traduce los datos en bruto y los convierte en el modelo poco. Luego, el modelo poco se pasa a la capa Lógica (que era el BLL) y, finalmente, a la capa de consulta de comandos, potencialmente manipulada en el camino. La capa de consulta de Comando convierte el POCO en un objeto de base de datos (que son casi lo mismo, pero uno está diseñado para la persistencia, el otro para el front-end). El elemento se almacena y se devuelve un objeto de base de datos a la capa Consulta de comando. Luego se convierte en un POCO, donde vuelve a la capa Lógica, potencialmente procesada más adelante y luego finalmente, de vuelta a la IU

La lógica y las interfaces compartidas es donde podemos tener datos persistentes, como MaxNumberOf_X y TotalAllowed_X y todas las interfaces.

Tanto la lógica / interfaces compartidas como DAL son la "base" de la arquitectura. Estos no saben nada del mundo exterior.

Todo sabe poco más que la lógica / interfaces compartidas y DAL.

El flujo sigue siendo muy similar al primer ejemplo, pero ha hecho que cada capa sea más responsable de 1 cosa (ya sea estado, flujo o cualquier otra cosa) ... pero ¿estoy rompiendo la POO con este enfoque?

Un ejemplo para demostrar la lógica y Poco podría ser:

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method1(PocoB pocoB) 
    { 
        return cmdQuery.Save(pocoB); 
    }

    /*This has no state objects, only ways to communicate with other 
    layers such as the cmdQuery. Everything else is just function 
    calls to allow flow via the program */
    public PocoA Method2(PocoB pocoB) 
    {         
        pocoB.UpdateState("world"); 
        return Method1(pocoB);
    }

}

public struct PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     public int DataC {get;set;}

    /*This simply returns something that is part of this class. 
     Everything is self-contained to this class. It doesn't call 
     trying to directly communicate with databases etc*/
     public int GetValue()
     {

         return DataB * DataC; 
     }

     /*This simply sets something that is part of this class. 
     Everything is self-contained to this class. 
     It doesn't call trying to directly communicate with databases etc*/
     public void UpdateState(string input)
     {        
         DataA += input;  
     }
}
MyDaftQuestions
fuente
No veo nada fundamentalmente incorrecto en su arquitectura como la ha descrito actualmente.
Robert Harvey
19
No hay suficientes detalles funcionales en su ejemplo de código para proporcionar más información. Los ejemplos de Foobar rara vez proporcionan una ilustración suficiente.
Robert Harvey
1
Sometido a su consideración: Baruco 2012: Deconstruyendo el marco, por Gary Bernhardt
Theraot
44
¿Podemos encontrar un mejor título para esta pregunta para que se pueda encontrar en línea más fácilmente?
Soner Gönül
1
Solo para ser pedante: un nivel y una capa no son lo mismo. Un "nivel" habla sobre la implementación, una "capa" sobre la lógica. Su capa de datos se implementará en los niveles del código del lado del servidor y de la base de datos. Su capa de IU se implementará en los niveles de código del lado del cliente web y del servidor. La arquitectura que muestra es una arquitectura de 3 capas. Sus niveles son "Cliente web", "Código del lado del servidor" y "Base de datos".
Laurent LA RIZZA

Respuestas:

54

Sí, es muy probable que rompas los conceptos básicos de OOP. Sin embargo, no se sienta mal, la gente hace esto todo el tiempo, no significa que su arquitectura esté "equivocada". Diría que probablemente sea menos mantenible que un diseño OO adecuado, pero esto es bastante subjetivo y no es su pregunta de todos modos. ( Aquí hay un artículo mío criticando la arquitectura n-tier en general).

Razonamiento : El concepto más básico de OOP es que los datos y la lógica forman una sola unidad (un objeto). Aunque esta es una declaración muy simple y mecánica, aun así, no se sigue realmente en su diseño (si lo entiendo correctamente). Está separando claramente la mayoría de los datos de la mayoría de la lógica. Tener métodos sin estado (como estáticos), por ejemplo, se llama "procedimientos" y, por lo general, son antitéticos a la POO.

Por supuesto, siempre hay excepciones, pero este diseño viola estas cosas como regla.

Nuevamente, me gustaría enfatizar que "viola OOP"! = "Incorrecto", por lo que esto no es necesariamente un juicio de valor. Todo depende de las limitaciones de su arquitectura, casos de uso de mantenibilidad, requisitos, etc.

Robert Bräutigam
fuente
99
Tenga un voto positivo, esta es una buena respuesta, si estuviera escribiendo la mía propia, copiaría y pegaría esto, pero también agregaría que, si encuentra que no escribe código OOP, tal vez debería considerar un lenguaje que no sea OOP. viene con una gran cantidad de gastos adicionales que puede prescindir si no lo está utilizando
TheCatWhisperer
2
@TheCatWhisperer: las arquitecturas empresariales modernas no descartan OOP por completo, solo de forma selectiva (por ejemplo, para DTO).
Robert Harvey
@RobertHarvey De acuerdo, quiero decir que si no usas OOP en casi ningún lugar de tu diseño
TheCatWhisperer
@TheCatWhisperer muchas de las ventajas en un oop como c # no están necesariamente en la parte oop del lenguaje sino en el soporte disponible como bibliotecas, estudio visual, administración de memoria, etc.
@Orangesandlemons Estoy seguro de que hay muchos otros idiomas compatibles ...
TheCatWhisperer
31

Uno de los principios centrales de la programación funcional son las funciones puras.

Uno de los principios centrales de la Programación Orientada a Objetos es unir las funciones con los datos sobre los que actúan.

Ambos principios básicos desaparecen cuando su aplicación tiene que comunicarse con el mundo exterior. De hecho, solo puede ser fiel a estos ideales en un espacio especialmente preparado en su sistema. No todas las líneas de su código deben cumplir con estos ideales. Pero si ninguna línea de su código cumple con estos ideales, no puede afirmar que está usando OOP o FP.

Por lo tanto, está bien tener solo "objetos" de datos a los que les da vueltas porque los necesita para cruzar un límite que simplemente no puede refactorizar para mover el código interesado. Solo sé que eso no es POO. Esa es la realidad OOP es cuando, una vez dentro de ese límite, reúnes toda la lógica que actúa sobre esos datos en un solo lugar.

No es que tengas que hacer eso tampoco. OOP no es todo para todas las personas. Es lo que es. Simplemente no afirmes que algo sigue a OOP cuando no lo hace o vas a confundir a las personas que intentan mantener tu código.

Sus POCO parecen tener lógica de negocios en ellos, así que no me preocuparía demasiado por estar anémico. Lo que me preocupa es que todos parecen mutables. Recuerde que los captadores y establecedores no proporcionan encapsulación real. Si su POCO se dirige a ese límite, entonces está bien. Solo comprenda que esto no le brinda todos los beneficios de un objeto OOP encapsulado real. Algunos llaman a esto un objeto de transferencia de datos o DTO.

Un truco que he usado con éxito es crear objetos de POO que coman DTO. Yo uso el DTO como un objeto de parámetro . Mi constructor lee el estado de él (leído como copia defensiva ) y lo arroja a un lado. Ahora tengo una versión totalmente encapsulada e inmutable del DTO. Todos los métodos relacionados con estos datos se pueden mover aquí siempre que estén en este lado de ese límite.

No proporciono getters o setters. Sigo diciendo, no preguntes . Llamas a mis métodos y hacen lo que hay que hacer. Es probable que ni siquiera te digan lo que hicieron. Solo lo hacen.

Ahora, eventualmente, algo, en algún lugar, se encontrará con otro límite y todo esto se desmorona nuevamente. Esta bien. Gira otro DTO y tíralo sobre la pared.

Esta es la esencia de lo que se trata la arquitectura de puertos y adaptadores. Lo he estado leyendo desde una perspectiva funcional . Quizás también te interese.

naranja confitada
fuente
55
" getters y setters no proporcionan encapsulación real " - ¡sí!
Boris the Spider
3
@BoristheSpider: los captadores y establecedores proporcionan encapsulación, simplemente no se ajustan a su definición limitada de encapsulación.
Davor Ždralo
44
@ Davor®dralo: ocasionalmente son útiles como una solución alternativa, pero por su propia naturaleza, los captadores y los colocadores rompen la encapsulación. Proporcionar una forma de obtener y establecer alguna variable interna es lo opuesto a ser responsable de su propio estado y actuar en consecuencia.
cHao
55
@ cHao: no entiendes lo que es un captador. No significa un método que devuelve un valor de una propiedad de objeto. Es una implementación común, pero puede devolver un valor de una base de datos, solicitarlo a través de http, calcularlo sobre la marcha, lo que sea. Como dije, los captadores y establecedores rompen la encapsulación solo cuando las personas usan sus propias definiciones estrechas (e incorrectas).
Davor Ždralo
44
@cHao: la encapsulación significa que está ocultando la implementación. Eso es lo que se encapsula. Si tiene getter "getSurfaceArea ()" en una clase Square, no sabe si el área de superficie es un campo, si se calcula sobre la marcha (altura de retorno * ancho) o algún tercer método, por lo que puede cambiar la implementación interna cuando quieras, porque está encapsulado.
Davor Ždralo
1

Si leo su explicación correctamente, sus objetos se parecen un poco a esto: (complicado sin contexto)

public class LogicClass
{
    private ICommandQueryObject cmdQuery;
    public PocoA Method(PocoB pocoB) { ... }
}

public class PocoX
{
     public string DataA {get;set;}
     public int DataB {get;set;}
     ... etc
}

En eso sus clases de Poco contienen solo datos y sus clases de Lógica contienen los métodos que actúan sobre esos datos; sí, has roto los principios de "Classic OOP"

De nuevo, es difícil distinguirlo de su descripción generalizada, pero me arriesgaría a que lo que ha escrito se pueda clasificar como Modelo de dominio anémico.

No creo que este sea un enfoque particularmente malo, ni, si consideras tus Poco como estructuras, nescarly rompe OOP en el sentido más específico. En eso sus objetos son ahora las clases lógicas. De hecho, si hace que su Pocos sea inmutable, el diseño podría considerarse bastante funcional.

Sin embargo, cuando hace referencia a Shared Logic, Pocos que son casi iguales y estáticos, empiezo a preocuparme por los detalles de su diseño.

Ewan
fuente
He agregado a mi publicación, esencialmente copiando su ejemplo. Lo siento, no estaba claro para empezar
MyDaftQuestions
1
Lo que quiero decir es que si nos dijeras qué hace la aplicación, sería más fácil escribir ejemplos. En lugar de LogicClass, podría tener PaymentProvider o lo que sea
Ewan
1

Un problema potencial que vi en su diseño (y es muy común): algunos de los códigos "OO" absolutamente peores que he encontrado fueron causados ​​por una arquitectura que separó los objetos "Data" de los objetos "Code". ¡Esto es algo de pesadilla! El problema es que en todas partes de su código de negocio cuando desea acceder a sus objetos de datos, TIENE QUE codificarlo allí mismo en línea (no es necesario, podría crear una clase de utilidad u otra función para manejarlo, pero esto es lo que He visto suceder repetidamente con el tiempo).

El código de acceso / actualización generalmente no se recopila, por lo que terminas con una funcionalidad duplicada en todas partes.

Por otro lado, esos objetos de datos son útiles, por ejemplo, como persistencia de la base de datos. He intentado tres soluciones:

Copiar valores dentro y fuera de objetos "reales" y tirar su objeto de datos es tedioso (pero puede ser una solución válida si quiere seguir ese camino).

Agregar métodos de disputa de datos a los objetos de datos puede funcionar, pero puede convertirse en un gran objeto de datos desordenado que está haciendo más de una cosa. También puede hacer que la encapsulación sea más difícil ya que muchos mecanismos de persistencia quieren accesores públicos ... No me ha encantado cuando lo hice, pero es una solución válida

La solución que mejor funcionó para mí es el concepto de una clase "Wrapper" que encapsula la clase "Data" y contiene toda la funcionalidad de disputa de datos, entonces no expongo la clase de datos en absoluto (ni siquiera los setters y getters a menos que sean absolutamente necesarios). Esto elimina la tentación de manipular el objeto directamente y te obliga a agregar funcionalidades compartidas al contenedor.

La otra ventaja es que puede asegurarse de que su clase de datos esté siempre en un estado válido. Aquí hay un ejemplo rápido de psuedocode:

// Data Class
Class User {
    String name;
    Date birthday;
}

Class UserHolder {
    final private User myUser // Cannot be null or invalid

    // Quickly wrap an object after getting it from the DB
    public UserHolder(User me)
    {
        if(me == null ||me.name == null || me.age < 0)
            throw Exception
        myUser=me
    }

    // Create a new instance in code
    public UserHolder(String name, Date birthday) {
        User me=new User()
        me.name=name
        me.birthday=birthday        
        this(me)
    }
    // Methods access attributes, they try not to return them directly.
    public boolean canDrink(State state) {
        return myUser.birthday.year < Date.yearsAgo(state.drinkingAge) 
    }
}

Tenga en cuenta que no tiene el control de edad extendido a lo largo de su código en diferentes áreas y también que no está tentado a usarlo porque ni siquiera puede averiguar cuál es el cumpleaños (a menos que lo necesite para otra cosa, en en cuyo caso puedes agregarlo).

Tiendo a no solo extender el objeto de datos porque pierdes esta encapsulación y la garantía de seguridad, en ese punto también podrías agregar los métodos a la clase de datos.

De esa manera, su lógica de negocios no tiene un montón de basura / iteradores de acceso a datos distribuidos por todo, se vuelve mucho más legible y menos redundante. También recomiendo adquirir el hábito de envolver siempre las colecciones por la misma razón: mantener las construcciones de bucle / búsqueda fuera de la lógica de su negocio y asegurarse de que siempre estén en buen estado.

Bill K
fuente
1

Nunca cambie su código porque piensa o alguien le dice que no es esto o no eso. Cambie su código si le da problemas y descubrió una manera de evitar estos problemas sin crear otros.

Entonces, aparte de que no le gustan las cosas, desea invertir mucho tiempo para hacer un cambio. Escribe los problemas que tienes ahora. Escriba cómo su nuevo diseño resolvería los problemas. Calcule el valor de la mejora y el costo de realizar sus cambios. Luego, y esto es lo más importante, asegúrese de tener tiempo para completar esos cambios, o terminará la mitad en este estado, la mitad en ese estado, y esa es la peor situación posible. (Una vez trabajé en un proyecto con 13 tipos diferentes de cadenas y tres esfuerzos identificables a medias para estandarizar un tipo)

gnasher729
fuente
0

La categoría "OOP" es mucho más grande y más abstracta que lo que está describiendo. No le importa todo esto. Se preocupa por la responsabilidad clara, la cohesión, el acoplamiento. Entonces, en el nivel que está preguntando, no tiene mucho sentido preguntar sobre la "práctica de OOPS".

Dicho esto, a tu ejemplo:

Me parece que hay un malentendido sobre lo que significa MVC. Está llamando a su UI "MVC", por separado de su lógica empresarial y control "backend". Pero para mí, MVC incluye toda la aplicación web:

  • Modelo: contiene los datos empresariales + lógica
    • Capa de datos como detalle de implementación del modelo
  • Ver: código de interfaz de usuario, plantillas HTML, CSS, etc.
    • Incluye aspectos del lado del cliente como JavaScript o las bibliotecas para aplicaciones web de "una página", etc.
  • Control: el pegamento del lado del servidor entre todas las otras partes
  • (Hay extensiones como ViewModel, Batch, etc., a las que no entraré aquí)

Aquí hay algunos supuestos básicos extremadamente importantes:

  • Una clase / objeto Modelo nunca tiene ningún conocimiento sobre ninguna de las otras partes (Ver, Control, ...). Nunca los llama, no supone ser llamado por ellos, no obtiene atributos / parámetros de sesión ni nada más a lo largo de esta línea. Está completamente solo. En los idiomas que admiten esto (por ejemplo, Ruby), puede activar una línea de comando manual, crear instancias de clases de Modelo, trabajar con ellas a su gusto y puede hacer todo lo que hacen sin ninguna instancia de Control o Vista o cualquier otra categoría. No tiene conocimiento sobre sesiones, usuarios, etc., lo más importante.
  • Nada toca la capa de datos excepto a través de un modelo.
  • La vista solo tiene un ligero toque en el modelo (muestra cosas, etc.) y nada más. (Tenga en cuenta que una buena extensión es "ViewModel", que son clases especiales que realizan un procesamiento más sustancial para procesar datos de una manera complicada, que no encajaría bien en Model o View; este es un buen candidato para eliminar / evitar la hinchazón en el Modelo puro).
  • El control es lo más liviano posible, pero es responsable de reunir a todos los demás jugadores y transferir cosas entre ellos (es decir, extraer las entradas de los usuarios de un formulario y reenviarlas al modelo, reenviar las excepciones de la lógica comercial a una útil). mensajes de error para el usuario, etc.). Para las API web / HTTP / REST, etc., toda la autorización, seguridad, administración de sesiones, administración de usuarios, etc. suceden aquí (y solo aquí).

Importante: la interfaz de usuario es parte de MVC. No al revés (como en su diagrama). Si acepta eso, entonces los modelos gordos son bastante buenos, siempre que no contengan cosas que no deberían.

Tenga en cuenta que "modelos gordos" significa que toda la lógica de negocios está en la categoría Modelo (paquete, módulo, cualquiera que sea el nombre en el idioma de su elección). Obviamente, las clases individuales deben estar estructuradas en OOP de una buena manera de acuerdo con las pautas de codificación que usted mismo le dé (es decir, algunas líneas de código máximas por clase o por método, etc.).

También tenga en cuenta que la forma en que se implementa la capa de datos tiene consecuencias muy importantes; especialmente si la capa del modelo puede funcionar sin una capa de datos (por ejemplo, para pruebas unitarias o para bases de datos en memoria baratas en la computadora portátil del desarrollador en lugar de las caras bases de datos Oracle o lo que sea que tenga). Pero esto realmente es un detalle de implementación en el nivel de arquitectura que estamos viendo en este momento. Obviamente, aquí todavía desea tener una separación, es decir, no me gustaría ver código que tenga una lógica de dominio pura directamente entrelazada con el acceso a datos, acoplando esto intensamente. Un tema para otra pregunta.

Para volver a su pregunta: me parece que hay una gran superposición entre su nueva arquitectura y el esquema MVC que he descrito, por lo que no está en un camino completamente equivocado, pero parece que está reinventando algunas cosas, o usarlo porque su entorno / bibliotecas de programación actual lo sugiere. Difícil de decir para mí. Por lo tanto, no puedo darle una respuesta exacta sobre si lo que pretende es particularmente bueno o malo. Puede averiguarlo verificando si cada "cosa" tiene exactamente una clase responsable de ello; si todo es altamente cohesivo y poco acoplado. Eso le da una buena indicación y, en mi opinión, es suficiente para un buen diseño de OOP (o un buen punto de referencia del mismo, si lo desea).

AnoE
fuente