¿Cómo evitas getters y setters?

85

Me está costando mucho diseñar las clases de una manera u otra. He leído que los objetos exponen su comportamiento, no sus datos; por lo tanto, en lugar de usar getter / setters para modificar datos, los métodos de una clase dada deberían ser "verbos" o acciones que operen en el objeto. Por ejemplo, en un objeto 'Cuenta', tendríamos los métodos Withdraw()y Deposit()no setAmount()etc. Ver: ¿Por qué los métodos getter y setter son malos ?

Entonces, por ejemplo, dada una clase de cliente que mantiene mucha información sobre el cliente, por ejemplo, nombre, fecha de nacimiento, teléfono, dirección, etc., ¿cómo se evitaría obtener / establecer para obtener y establecer todos esos atributos? ¿Qué método de tipo 'Comportamiento' se puede escribir para completar todos esos datos?

IntelliData
fuente
8
Señalaría que si tiene que lidiar con la especificación de Java Beans , tendrá getters y setters. Muchas cosas usan Java Beans (lenguaje de expresión en jsps) y tratar de evitarlo probablemente será ... desafiante.
44
... y, desde la perspectiva opuesta de MichaelT: si no está utilizando la especificación JavaBeans (y personalmente creo que debe evitarla a menos que esté utilizando su objeto en un contexto donde sea necesario), entonces no hay necesidad de la verruga "get" en getters, especialmente para propiedades que no tienen ningún setter correspondiente. Creo que un método llamado name()en Customeres menos clara o más clara, de un método llamado getName().
Daniel Pryden
2
@Daniel Pryden: nombre () puede significar tanto establecer como obtener? ...
IntelliData

Respuestas:

55

Como se indicó en bastantes respuestas y comentarios, los DTO son apropiados y útiles en algunas situaciones, especialmente en la transferencia de datos a través de límites (por ejemplo, serialización a JSON para enviar a través de un servicio web). Para el resto de esta respuesta, lo ignoraré más o menos y hablaré sobre las clases de dominio y cómo se pueden diseñar para minimizar (si no eliminar) los captadores y establecedores, y seguir siendo útiles en un proyecto grande. Tampoco hablaré sobre por qué eliminar getters o setters, o cuándo hacerlo, porque esas son preguntas propias.

Como ejemplo, imagina que tu proyecto es un juego de mesa como el ajedrez o el acorazado. Puede tener varias formas de representar esto en una capa de presentación (aplicación de consola, servicio web, GUI, etc.), pero también tiene un dominio central. Una clase que puede tener es Coordinate, representando una posición en el tablero. La forma "malvada" de escribirlo sería:

public class Coordinate
{
    public int X {get; set;}
    public int Y {get; set;}
}

(Voy a escribir ejemplos de código en C # en lugar de Java, por brevedad y porque estoy más familiarizado con él. Espero que eso no sea un problema. Los conceptos son los mismos y la traducción debería ser simple).

Eliminar setters: inmutabilidad

Si bien los captadores públicos y los setters son potencialmente problemáticos, los setters son los más "malvados" de los dos. También suelen ser los más fáciles de eliminar. El proceso es simple: establece el valor desde dentro del constructor. Cualquier método que previamente haya mutado el objeto debería devolver un nuevo resultado. Entonces:

public class Coordinate
{
    public int X {get; private set;}
    public int Y {get; private set;}

    public Coordinate(int x, int y)
    {
        X = x;
        Y = y;
    }
}

Tenga en cuenta que esto no protege contra otros métodos en la clase que muta X e Y. Para ser más estrictamente inmutable, puede usar readonly( finalen Java). Pero de cualquier manera, ya sea que haga que sus propiedades sean realmente inmutables o simplemente evite la mutación pública directa a través de los setters, es el truco para eliminar sus setters públicos. En la gran mayoría de las situaciones, esto funciona bien.

Eliminando Getters, Parte 1: Diseñando para el comportamiento

Lo anterior está muy bien para los setters, pero en términos de getters, en realidad nos disparamos en el pie incluso antes de comenzar. Nuestro proceso fue pensar qué es una coordenada, los datos que representa, y crear una clase en torno a eso. En cambio, deberíamos haber comenzado con qué comportamiento necesitamos de una coordenada. Este proceso, por cierto, es ayudado por TDD, donde solo extraemos clases como esta una vez que las necesitamos, por lo que comenzamos con el comportamiento deseado y trabajamos desde allí.

Entonces, digamos que el primer lugar donde necesitabas un Coordinatedetector de colisión: querías verificar si dos piezas ocupan el mismo espacio en el tablero. Aquí está la forma "malvada" (los constructores se omiten por brevedad):

public class Piece
{
    public Coordinate Position {get; private set;}
}

public class Coordinate
{
    public int X {get; private set;}
    public int Y {get; private set;}
}

    //...And then, inside some class
    public bool DoPiecesCollide(Piece one, Piece two)
    {
        return one.X == two.X && one.Y == two.Y;
    }

Y aquí está el buen camino:

public class Piece
{
    private Coordinate _position;
    public bool CollidesWith(Piece other)
    {
        return _position.Equals(other._position);
    }
}

public class Coordinate
{
    private readonly int _x;
    private readonly int _y;
    public bool Equals(Coordinate other)
    {
        return _x == other._x && _y == other._y;
    }
}

( IEquatableimplementación abreviada para simplificar). Al diseñar para el comportamiento en lugar de modelar datos, hemos logrado eliminar nuestros captadores.

Tenga en cuenta que esto también es relevante para su ejemplo. Puede estar utilizando un ORM, o mostrar información del cliente en un sitio web o algo así, en cuyo caso Customerprobablemente tendría sentido algún tipo de DTO. Pero el hecho de que su sistema incluya clientes y estén representados en el modelo de datos no significa automáticamente que deba tener una Customerclase en su dominio. Tal vez, a medida que diseñe para el comportamiento, surgirá uno, pero si desea evitar captadores, no cree uno preventivamente.

Eliminando Getters, Parte 2: Comportamiento Externo

Entonces, lo anterior es un buen comienzo, pero tarde o temprano probablemente se encontrará con una situación en la que tiene un comportamiento asociado con una clase, que de alguna manera depende del estado de la clase, pero que no pertenece a la clase. Este tipo de comportamiento es lo que generalmente vive en la capa de servicio de su aplicación.

Tomando nuestro Coordinateejemplo, eventualmente querrás representar tu juego para el usuario, y eso podría significar dibujar en la pantalla. Puede, por ejemplo, tener un proyecto de IU que Vector2representa un punto en la pantalla. Pero sería inapropiado que la Coordinateclase se hiciera cargo de la conversión de una coordenada a un punto en la pantalla, lo que llevaría todo tipo de problemas de presentación a su dominio principal. Lamentablemente, este tipo de situación es inherente al diseño OO.

La primera opción , que se elige con mucha frecuencia, es simplemente exponer a los malditos captadores y decirle al diablo con ella. Esto tiene la ventaja de la simplicidad. Pero ya que estamos hablando de evitar a los captadores, digamos por el argumento de rechazar este y ver qué otras opciones hay.

Una segunda opción es agregar algún tipo de .ToDTO()método en su clase. Esto, o similar, puede ser necesario de todos modos, por ejemplo, cuando desea guardar el juego, necesita capturar casi todo su estado. Pero la diferencia entre hacer esto por sus servicios y simplemente acceder al getter directamente es más o menos estético. Todavía tiene tanta "maldad".

Una tercera opción , que he visto defendida por Zoran Horvat en un par de videos de Pluralsight, es usar una versión modificada del patrón de visitante. Este es un uso y variación bastante inusual del patrón y creo que el kilometraje de las personas variará enormemente en cuanto a si está agregando complejidad sin ganancia real o si es un buen compromiso para la situación. La idea es esencialmente usar el patrón de visitante estándar, pero que los Visitmétodos tomen el estado que necesitan como parámetros, en lugar de la clase que visitan. Los ejemplos se pueden encontrar aquí .

Para nuestro problema, una solución usando este patrón sería:

public class Coordinate
{
    private readonly int _x;
    private readonly int _y;

    public T Transform<T>(IPositionTransformer<T> transformer)
    {
        return transformer.Transform(_x,_y);
    }
}

public interface IPositionTransformer<T>
{
    T Transform(int x, int y);
}

//This one lives in the presentation layer
public class CoordinateToVectorTransformer : IPositionTransformer<Vector2>
{
    private readonly float _tileWidth;
    private readonly float _tileHeight;
    private readonly Vector2 _topLeft;

    Vector2 Transform(int x, int y)
    {
        return _topLeft + new Vector2(_tileWidth*x + _tileHeight*y);
    }
}

Como probablemente pueda ver, _xya _yno están realmente encapsulados. Podríamos extraerlos creando uno IPositionTransformer<Tuple<int,int>>que simplemente los devuelva directamente. Dependiendo del gusto, puede sentir que esto hace que todo el ejercicio no tenga sentido.

Sin embargo, con los captadores públicos, es muy fácil hacer las cosas de manera incorrecta, simplemente extrayendo los datos directamente y usándolos en violación de Tell, Don't Ask . Mientras que usar este patrón en realidad es más simple hacerlo de la manera correcta: cuando desee crear un comportamiento, comenzará automáticamente creando un tipo asociado con él. Las violaciones de TDA serán muy evidentemente malolientes y probablemente requieran trabajar en una solución más simple y mejor. En la práctica, estos puntos hacen que sea mucho más fácil hacerlo de la manera correcta, OO, que la forma "malvada" que estimulan los captadores.

Finalmente , incluso si inicialmente no es obvio, de hecho puede haber formas de exponer suficiente de lo que necesita como comportamiento para evitar la necesidad de exponer el estado. Por ejemplo, utilizando nuestra versión anterior de Coordinatecuyo único miembro público es Equals()(en la práctica necesitaría una IEquatableimplementación completa ), podría escribir la siguiente clase en su capa de presentación:

public class CoordinateToVectorTransformer
{
    private Dictionary<Coordinate,Vector2> _coordinatePositions;

    public CoordinateToVectorTransformer(int boardWidth, int boardHeight)
    {
        for(int x=0; x<boardWidth; x++)
        {
            for(int y=0; y<boardWidth; y++)
            {
                _coordinatePositions[new Coordinate(x,y)] = GetPosition(x,y);
            }
        }
    }

    private static Vector2 GetPosition(int x, int y)
    {
        //Some implementation goes here...
    }

    public Vector2 Transform(Coordinate coordinate)
    {
        return _coordinatePositions[coordinate];
    }
}

Resulta, quizás sorprendentemente, que todo el comportamiento que realmente necesitábamos de una coordenada para lograr nuestro objetivo era la verificación de la igualdad. Por supuesto, esta solución se adapta a este problema y hace suposiciones sobre el uso / rendimiento aceptable de la memoria. Es solo un ejemplo que se ajusta a este dominio del problema en particular, en lugar de un plan para una solución general.

Y nuevamente, las opiniones variarán sobre si en la práctica esto es una complejidad innecesaria. En algunos casos, no existe una solución como esta, o puede ser prohibitivamente extraño o complejo, en cuyo caso puede volver a las tres anteriores.

Ben Aaronson
fuente
Bellamente respondida! Me gustaría aceptar, pero primero algunos comentarios: 1. Creo que toDTO () es genial, porque no estás accediendo a get / set, lo que te permite cambiar los campos dados a DTO sin romper el código existente. 2. Diga al cliente tiene un comportamiento suficiente para justificar lo que es una entidad, ¿cómo acceder u objetos para modificarlos, por ejemplo, dirección / tel cambio etc
IntelliData
@IntelliData 1. Cuando dice "cambiar los campos", ¿quiere decir cambiar la definición de clase o mutar los datos? Esto último solo puede evitarse eliminando los setters públicos pero dejando los getters, por lo que el aspecto dto es irrelevante. La primera no es realmente la razón (completa) de que los captadores públicos son "malvados". Ver programmers.stackexchange.com/questions/157526/… por ejemplo.
Ben Aaronson
@IntelliData 2. Esto es difícil de responder sin conocer el comportamiento. Pero probablemente, la respuesta es: no lo harías. ¿Qué comportamiento podría tener una Customerclase que requiera poder mutar su número de teléfono? Quizás el número de teléfono del cliente cambia y necesito persistir ese cambio en la base de datos, pero nada de eso es responsabilidad de un objeto de dominio que proporcione comportamiento. Esa es una preocupación de acceso a datos, y probablemente se manejaría con un DTO y, por ejemplo, un repositorio.
Ben Aaronson
@IntelliData Mantener los Customerdatos del objeto de dominio relativamente frescos (en sincronía con la base de datos) es una cuestión de administrar su ciclo de vida, que tampoco es su responsabilidad, y de nuevo probablemente terminaría viviendo en un repositorio o una fábrica o un contenedor de COI o cualquiera que sea la instancia Customers.
Ben Aaronson
2
Me gusta mucho el concepto de Diseño para el comportamiento . Esto informa la "estructura de datos" subyacente y ayuda a evitar las clases anémicas demasiado difíciles de usar. mas uno.
radarbob
71

La forma más sencilla de evitar los establecedores es entregar los valores al método constructor cuando newsube el objeto. Este también es el patrón habitual cuando desea hacer que un objeto sea inmutable. Dicho esto, las cosas no siempre son tan claras en el mundo real.

Es cierto que los métodos deben ser sobre el comportamiento. Sin embargo, algunos objetos, como el Cliente, existen principalmente para contener información. Esos son los tipos de objetos que más se benefician de los captadores y colocadores; Si no hubiera necesidad de tales métodos, simplemente los eliminaríamos por completo.

Lecturas adicionales
Cuándo se justifican los captadores y establecedores

Robert Harvey
fuente
3
Entonces, ¿por qué todas las exageraciones sobre 'Getter / Setters' son malas?
IntelliData
46
Solo la pontificación habitual de los desarrolladores de software que deberían saberlo mejor. Para el artículo que vinculó, el autor está usando la frase "los captadores y los colocadores son malos" para llamar su atención, pero eso no significa que la afirmación sea categóricamente cierta.
Robert Harvey
12
@IntelliData, algunas personas te dirán que Java es malo.
nulo
18
@MetaFightsetEvil(null);
44
Ciertamente eval es Evil Incarnate. O un primo cercano.
obispo
58

Está perfectamente bien tener un objeto que exponga datos en lugar de comportamiento. Simplemente lo llamamos un "objeto de datos". El patrón existe bajo nombres como Data Transfer Object u Value Object. Si el propósito del objeto es retener datos, los captadores y los establecedores son válidos para acceder a los datos.

Entonces, ¿ por qué alguien diría "los métodos getter y setter son malos"? Verá esto mucho: alguien toma una directriz que es perfectamente válida en un contexto específico y luego la elimina para obtener un titular más contundente. Por ejemplo, " favorecer la composición sobre la herencia " es un buen principio, pero pronto alguien eliminará el contexto y escribirá " ¿Por qué se extiende es malo " (oye, mismo autor, qué coincidencia!) O "la herencia es mala y debe ser destruido ".

Si nos fijamos en el contenido del artículo, en realidad tiene algunos puntos válidos, simplemente estira el punto para hacer un titular de click-baity. Por ejemplo, el artículo establece que los detalles de implementación no deben exponerse. Estos son los principios de encapsulación y ocultación de datos que son fundamentales en OO. Sin embargo, un método getter no expone por definición los detalles de implementación. En el caso de un objeto de datos del Cliente , las propiedades de Nombre , Dirección , etc., no son detalles de implementación, sino más bien el propósito completo del objeto y deberían formar parte de la interfaz pública.

Lea la continuación del artículo al que se vincula, para ver cómo sugiere realmente establecer propiedades como 'nombre' y 'salario' en un objeto 'Empleado' sin el uso de los malvados. Resulta que usa un patrón con un 'Exportador' que se completa con métodos llamados agregar Nombre, agregar Salario, que a su vez establece campos del mismo nombre ... Así que al final termina usando exactamente el patrón de establecimiento, solo con un diferente convención de nomenclatura

Esto es como pensar que evitas las trampas de los singletons al renombrarlos como únicos, pero manteniendo la misma implementación.

JacquesB
fuente
En oop, los llamados expertos parecen decir que este no es el caso.
IntelliData
8
Por otra parte, algunas personas han dicho 'Nunca use OOP': dañoso.cat
v.org
77
FTR, creo que más "expertos" todavía enseñan a crear sin sentido getters / setters para todo , que nunca crear ninguno. En mi opinión, este último es un consejo menos equivocado.
Leftaroundabout
44
@leftaroundabout: OK, pero estoy sugiriendo un punto medio entre 'siempre' y 'nunca' que es 'usar cuando sea apropiado'.
JacquesB
3
Creo que el problema es que muchos programadores convierten cada objeto (o demasiados objetos) en un DTO. A veces son necesarios, pero evítelos tanto como sea posible ya que separan los datos del comportamiento. (Asumiendo que eres un OOPer devoto)
user949300
11

Para transformar la Customerclase de un objeto de datos, podemos hacernos las siguientes preguntas sobre los campos de datos:

¿Cómo queremos usar {campo de datos}? ¿Dónde se usa {campo de datos}? ¿Se puede y se debe mover el uso de {campo de datos} a la clase?

P.ej:

  • ¿Para qué sirve Customer.Name?

    Posibles respuestas, mostrar el nombre en una página web de inicio de sesión, usar el nombre en los correos al cliente.

    Lo que lleva a métodos:

    • Customer.FillInTemplate (...)
    • Customer.IsApplicableForMailing (...)
  • ¿Para qué sirve Customer.DOB?

    Validar la edad del cliente. Descuentos en el cumpleaños del cliente. Mailings

    • Customer.IsApplicableForProduct ()
    • Customer.GetPersonalDiscount ()
    • Customer.IsApplicableForMailing ()

Dados los comentarios, el objeto de ejemplo Customer, tanto como objeto de datos como objeto "real" con sus propias responsabilidades, es demasiado amplio; es decir, tiene demasiadas propiedades / responsabilidades. Lo que conduce a muchos componentes dependiendo de Customer(leyendo sus propiedades) o Customerdependiendo de muchos componentes. Quizás existan diferentes puntos de vista del cliente, quizás cada uno debería tener su propia clase 1 distinta :

  • El cliente en el contexto de Accountlas transacciones monetarias probablemente solo se use para:

    • ayudar a los humanos a identificar que su transferencia de dinero va a la persona correcta; y
    • grupo Accounts.

    Este cliente no necesita campos como DOB, FavouriteColour, Tel, y tal vez ni siquiera Address.

  • El cliente en el contexto de un usuario que inicia sesión en un sitio web bancario.

    Los campos relevantes son:

    • FavouriteColour, que puede venir en forma de temática personalizada;
    • LanguagePreferencesy
    • GreetingName

    En lugar de propiedades con captadores y definidores, estos podrían capturarse en un solo método:

    • PersonaliseWebPage (página de plantilla);
  • El cliente en el contexto de marketing y correo personalizado.

    Aquí no confiamos en las propiedades de un objeto de datos, sino que comenzamos por las responsabilidades del objeto; p.ej:

    • IsCustomerInterestedInAction (); y
    • GetPersonalDiscounts ().

    El hecho de que este objeto de cliente tenga una FavouriteColourpropiedad y / o una Addresspropiedad se vuelve irrelevante: quizás la implementación utiliza estas propiedades; pero también podría usar algunas técnicas de aprendizaje automático y usar interacciones previas con el cliente para descubrir en qué productos podría estar interesado el cliente.


1. Por supuesto, las clases Customery Accountfueron ejemplos, y para un ejemplo simple o ejercicio de tarea, dividir a este cliente puede ser excesivo, pero con el ejemplo de división, espero demostrar que el método de convertir un objeto de datos en un objeto con Las responsabilidades funcionarán.

Kasper van den Berg
fuente
44
Vota porque realmente estás respondiendo la pregunta :) Sin embargo, también es obvio que las soluciones propuestas son mucho peores que tener gettes / setters, por ejemplo, FillInTemplate rompe claramente el principio de separación de preocupaciones. Lo que demuestra que la premisa de la pregunta es errónea.
JacquesB
@Kasper van den Berg: Y cuando tiene muchos atributos en el Cliente, como suele ser el caso, ¿cómo los establecería inicialmente?
IntelliData
2
@IntelliData es probable que sus valores provengan de una base de datos, XML, etc. El Cliente, o un CustomerBuilder, los lee, luego los establece utilizando (es decir, en Java) acceso privado / paquete / clase interna, o, (ick) reflexión. No es perfecto, pero generalmente puedes evitar a los setters públicos. (Vea mi respuesta para más detalles)
user949300
66
No creo que la clase de clientes deba saber sobre ninguna de estas cosas.
CodesInChaos
¿Qué tal algo así Customer.FavoriteColor?
Gabe
8

TL; DR

  • Modelar para el comportamiento es bueno.

  • Modelar para buenas (!) Abstracciones es mejor.

  • A veces se requieren objetos de datos.


Comportamiento y abstracción

Hay varias razones para evitar captadores y colocadores. Una es, como notó, evitar los datos de modelado. Esta es en realidad la razón menor. La razón más grande es proporcionar abstracción.

En su ejemplo con la cuenta bancaria que está clara: un setBalance()método sería realmente malo porque establecer un saldo no es para lo que debe usarse una cuenta. El comportamiento de la cuenta debe abstraerse de su saldo actual tanto como sea posible. Puede tener en cuenta el saldo al decidir si se debe suspender un retiro, puede dar acceso al saldo actual, pero modificar la interacción con una cuenta bancaria no debería requerir que el usuario calcule el nuevo saldo. Eso es lo que la cuenta debería hacer por sí misma.

Incluso un par de deposit()y withdraw()métodos no es ideal para modelar una cuenta bancaria. Una mejor manera sería proporcionar un solo transfer()método que tenga en cuenta otra y una cantidad como argumentos. Esto permitiría que la clase de cuenta se asegure trivialmente de que no cree / destruya accidentalmente dinero en su sistema, proporcionaría una abstracción muy útil y en realidad proporcionaría a los usuarios más información porque forzaría el uso de cuentas especiales para dinero ganado / invertido / perdido (ver doble contabilidad ). Por supuesto, no todos los usos de una cuenta necesitan este nivel de abstracción, pero definitivamente vale la pena considerar cuánta abstracción pueden proporcionar sus clases.

Tenga en cuenta que proporcionar abstracción y ocultar datos internos no siempre es lo mismo. Casi cualquier aplicación contiene clases que efectivamente son solo datos. Las tuplas, los diccionarios y las matrices son ejemplos frecuentes. No desea ocultar al usuario la coordenada x de un punto. Hay muy poca abstracción que pueda / deba hacer con un punto.


La clase del cliente

Un cliente es ciertamente una entidad en su sistema que debería intentar proporcionar abstracciones útiles. Por ejemplo, probablemente debería estar asociado con un carrito de compras, y la combinación del carrito y el cliente debería permitir comprometer una compra, lo que podría iniciar acciones como enviarle los productos solicitados, cobrarle dinero (teniendo en cuenta su pago seleccionado método), etc.

El problema es que todos los datos que mencionó no solo están asociados con un cliente, sino que todos esos datos también son mutables. El cliente puede mudarse. Pueden cambiar su compañía de tarjeta de crédito. Pueden cambiar su dirección de correo electrónico y número de teléfono. ¡Diablos, incluso pueden cambiar su nombre y / o sexo! Por lo tanto, una clase de cliente con todas las funciones tiene que proporcionar un acceso de modificación total a todos estos elementos de datos.

Aún así, los emisores pueden / deberían proporcionar servicios no triviales: pueden garantizar el formato correcto de direcciones de correo electrónico, verificación de direcciones postales, etc. Asimismo, los "captadores" pueden proporcionar servicios de alto nivel como proporcionar direcciones de correo electrónico en el Name <[email protected]>formato utilizando los campos de nombre y la dirección de correo electrónico depositada, o proporcione una dirección postal formateada correctamente, etc. Por supuesto, lo que tiene sentido de esta funcionalidad de alto nivel depende en gran medida de su caso de uso. Puede ser una exageración completa, o puede requerir que otra clase lo haga bien. La elección del nivel de abstracción no es fácil.

cmaster
fuente
suena bien, aunque no estoy de acuerdo en la parte del sexo ...;)
IntelliData
6

Intentando ampliar la respuesta de Kasper, es más fácil despotricar contra y eliminar a los setters. En un argumento bastante vago, de saludo (y con suerte humorístico):

¿Cuándo cambiaría Customer.Name alguna vez?

Raramente. Quizás se casaron. O entró en protección de testigos. Pero en ese caso, también querrá verificar y posiblemente cambiar su residencia, sus familiares y otra información.

¿Cuándo cambiaría DOB alguna vez?

Solo en la creación inicial o en un atornillado de entrada de datos. O si son jugadores de béisbol dominicanos. :-)

Estos campos no deberían ser accesibles con los setters normales y de rutina. Quizás tenga un Customer.initialEntry()método, o un Customer.screwedUpHaveToChange()método que requiera permisos especiales. Pero no tenga un Customer.setDOB()método público .

Por lo general, un Cliente se lee desde una base de datos, una API REST, algún XML, lo que sea. Tenga un método Customer.readFromDB()o, si es más estricto con respecto a SRP / separación de preocupaciones, tendría un constructor separado, por ejemplo, un CustomerPersisterobjeto con un read()método. Internamente, de alguna manera establecen los campos (prefiero usar el acceso a paquetes o una clase interna, YMMV). Pero, de nuevo, evite los setters públicos.

(Addendum as Question ha cambiado algo ...)

Digamos que su aplicación hace un uso intensivo de las bases de datos relacionales. Sería tonto tener Customer.saveToMYSQL()o Customer.readFromMYSQL()métodos. Eso crea un acoplamiento indeseable a una entidad concreta, no estándar y que probablemente cambie . Por ejemplo, cuando cambia el esquema o cambia a Postgress u Oracle.

Sin embargo, la OMI, es perfectamente aceptable para acoplar al cliente a un nivel abstracto , ResultSet. Un objeto auxiliar separado (lo llamaré CustomerDBHelper, que probablemente sea una subclase de AbstractMySQLHelper) conoce todas las conexiones complejas a su base de datos, conoce los detalles difíciles de optimización, conoce las tablas, consultas, uniones, etc. (o usa un ORM como Hibernate) para generar el ResultSet. Su objeto habla con el ResultSet, que es un estándar abstracto , que es poco probable que cambie. Cuando cambia la base de datos subyacente, o cambia el esquema, el Cliente no cambia , pero el CustomerDBHelper sí. Si tiene suerte, solo cambia AbstractMySQLHelper, que realiza automáticamente los cambios para Cliente, Comerciante, Envío, etc.

De esta manera puede (quizás) evitar o disminuir la necesidad de captadores y establecedores.

Y, el punto principal del artículo de Holub, compara y contrasta lo anterior con lo que sería si usaras getters y setters para todo y cambiaras la base de datos.

Del mismo modo, digamos que usa mucho XML. En mi opinión, está bien acoplar a su Cliente a un estándar abstracto, como Python xml.etree.ElementTree o Java org.w3c.dom.Element . El cliente obtiene y se establece a partir de eso. De nuevo, puede (tal vez) disminuir la necesidad de captadores y establecedores.

user949300
fuente
¿Diría que es aconsejable utilizar un patrón de construcción?
IntelliData
Builder es útil para hacer que la construcción de un objeto sea más fácil y más robusta y, si lo desea, permitir que el objeto sea inmutable. Sin embargo, todavía (parcialmente) expone el hecho de que el objeto subyacente tiene un campo DOB, por lo que no es el final de todo.
user949300
1

El problema de tener captadores y establecedores puede deberse al hecho de que una clase puede usarse en la lógica de negocios de una manera, pero también puede tener clases auxiliares para serializar / deserializar los datos de una base de datos o archivo u otro almacenamiento persistente.

Debido al hecho de que hay muchas formas de almacenar / recuperar sus datos y desea desacoplar los objetos de datos de la forma en que están almacenados, la encapsulación puede verse "comprometida" haciendo públicos estos miembros o haciéndolos accesibles a través de getters y setters que es casi tan malo como hacerlos públicos.

Hay varias formas de evitar esto. Una forma es hacer que los datos estén disponibles para un "amigo". Aunque la amistad no se hereda, esto puede ser superado por cualquier serializador que solicite la información del amigo, es decir, el serializador base "reenvía" la información.

Su clase podría tener un método genérico "fromMetadata" o "toMetadata". From-metadata construye un objeto, por lo que bien puede ser un constructor. Si es un lenguaje de tipo dinámico, los metadatos son bastante estándar para dicho lenguaje y probablemente es la forma principal de construir tales objetos.

Si su lenguaje es C ++ específicamente, una forma de evitar esto es tener una "estructura" pública de datos y luego que su clase tenga una instancia de esta "estructura" como miembro y, de hecho, todos los datos que va a almacenar / recuperar para ser almacenado en él. Luego puede escribir fácilmente "envoltorios" para leer / escribir sus datos en múltiples formatos.

Si su lenguaje es C # o Java que no tiene "estructuras", puede hacerlo de manera similar, pero su estructura ahora es una clase secundaria. No existe un concepto real de "propiedad" de los datos o constancia, por lo que si proporciona una instancia de la clase que contiene sus datos y todo es público, lo que sea que se retenga puede modificarlo. Podrías "clonarlo" aunque esto puede ser costoso. Alternativamente, podría hacer que esta clase tenga datos privados pero use accesores. Eso brinda a los usuarios de su clase una forma indirecta de acceder a los datos, pero no es la interfaz directa con su clase y es realmente un detalle en el almacenamiento de los datos de la clase, que también es un caso de uso.

CashCow
fuente
0

OOP se trata de encapsular y ocultar el comportamiento dentro de los objetos. Los objetos son cajas negras. Esta es una forma de diseñar cosas. El activo es, en muchos casos, uno no necesita conocer el estado interno de otro componente y es mejor no tener que saberlo. Puede aplicar esa idea principalmente con interfaces o dentro de un objeto con visibilidad y teniendo cuidado de que solo los verbos / acciones permitidos estén disponibles para la persona que llama.

Esto funciona bien para algún tipo de problema. Por ejemplo, en las interfaces de usuario para modelar componentes de IU individuales. Cuando interactúa con un cuadro de texto, solo le interesa configurar el texto, obtenerlo o escuchar el evento de cambio de texto. Por lo general, no le interesa saber dónde está el cursor, la fuente utilizada para dibujar el texto o cómo se usa el teclado. La encapsulación proporciona mucho aquí.

Por el contrario, cuando llama a un servicio de red, proporciona una entrada explícita. Por lo general, hay una gramática (como en JSON o XML) y todas las opciones de llamar al servicio no tienen por qué ocultarse. La idea es que puede llamar al servicio de la forma que desee y el formato de datos es público y publicado.

En este caso, o en muchos otros (como el acceso a una base de datos), realmente trabaja con datos compartidos. Como tal, no hay razón para ocultarlo, por el contrario, desea que esté disponible. Puede haber preocupación por el acceso de lectura / escritura o la coherencia de la comprobación de datos, pero en este núcleo, el concepto central si es público.

Para tal requisito de diseño en el que desea evitar la encapsulación y hacer que las cosas sean públicas y, de forma clara, desea evitar los objetos. Lo que realmente necesita son tuplas, estructuras C o su equivalente, no objetos.

Pero también ocurre en lenguajes como Java, lo único que puede modelar es objetos o matrices de objetos. Los objetos pueden contener algunos tipos nativos (int, float ...) pero eso es todo. Pero los objetos también pueden comportarse como una estructura simple con solo campos públicos y todo eso.

Entonces, si modela datos, puede hacerlo solo con campos públicos dentro de objetos porque no necesita más. No utiliza la encapsulación porque no la necesita. Esto se hace de esta manera en muchos idiomas. En Java, históricamente, una rosa estándar donde con getter / setter al menos podría tener control de lectura / escritura (al no agregar setter, por ejemplo) y que las herramientas y el marco utilizando la API de instrospección buscarían métodos getter / setter y los usarían para autocompletar el contenido o mostrar tesis como campos modificables en la interfaz de usuario generada automáticamente.

También existe el argumento de que podría agregar algo de lógica / verificación en el método setter.

En realidad, casi no hay justificación para getter / setters, ya que se usan con mayor frecuencia para modelar datos puros. Los marcos y desarrolladores que usan sus objetos esperan que el captador / definidor no haga nada más que establecer / obtener los campos de todos modos. Efectivamente, no está haciendo más con getter / setter de lo que podría hacerse con los campos públicos.

Pero esos viejos hábitos y viejos hábitos son difíciles de eliminar ... Incluso podría ser amenazado por sus colegas o maestros si no coloca a los captadores / colocadores a ciegas en todas partes si carecen de los antecedentes para comprender mejor qué son y cuáles son no.

Es probable que deba cambiar el idioma para obtener el código repetitivo de getters / setters de estas tesis. (Como C # o lisp). Para mí, los captadores / establecedores son solo otro error de mil millones de dólares ...

Nicolas Bousquet
fuente
66
Las propiedades de C # realmente no implementan la encapsulación más que los captadores y establecedores ...
IntelliData
1
La ventaja de [gs] etters es que puede hacer las cosas que normalmente no hace (verificar valores, notificar a los observadores), simular campos no existentes, etc., y principalmente que puede cambiarlo más tarde . Con Lombok , el texto modelo se ha ido: @Getter @Setter class MutablePoint3D {private int x, y, z;}.
maaartinus
1
@maaartinus Seguro que [gs] etter puede hacer cualquier cosa como cualquier otro método. Esto significa que cualquier código de la persona que llama debe tener en cuenta que cualquier valor que establezca puede generar una excepción, modificarse o enviar notificaciones de cambios ... o cualquier otra cosa. Que más o menos [gs] etter no proporcionan acceso a un campo sino que hacen código arbitrar.
Nicolas Bousquet
Las propiedades de @IntelliData C # permiten no escribir 1 carácter único inútil de repetitivo y no preocuparse por todas estas cosas getter / setter ... Esto ya es un logro mejor que el proyecto lombok. También para mí, un POJO con solo getters / setter no está aquí para proporcionar la encapsulación, sino para publicar un formato de datos que uno puede leer o escribir como intercambio con un servicio. La encapsulación es entonces lo opuesto al requisito de diseño.
Nicolas Bousquet
Realmente no creo que las propiedades sean realmente buenas. Claro, guarda el prefijo y los paréntesis, es decir, 5 caracteres por llamada, pero 1. se ven como campos, lo cual es confuso. 2. son una cosa adicional para la que necesita apoyo en la reflexión. No es gran cosa, pero tampoco es una gran ventaja (en comparación con Java + Lombok; Java puro está en clara pérdida).
maaartinus
0

Entonces, por ejemplo, dada una clase de cliente que mantiene mucha información sobre el cliente, por ejemplo, nombre, fecha de nacimiento, teléfono, dirección, etc., ¿cómo se evitaría obtener / establecer para obtener y establecer todos esos atributos? ¿Qué método de tipo 'Comportamiento' se puede escribir para completar todos esos datos?

Creo que esta pregunta es espinosa porque le preocupan los métodos de comportamiento para poblar datos, pero no veo ninguna indicación de qué comportamiento Customerpretende encapsular la clase de objetos.

No confunda Customercomo una clase de objetos con 'Cliente' como usuario / actor que realiza diferentes tareas utilizando su software.

Cuando dice que se le da una clase de Cliente que mantiene mucha información sobre el cliente, entonces, en lo que respecta al comportamiento, parece que su clase de Cliente tiene poco que distinguirlo de una roca. A Rockpuede tener un color, podría darle un nombre, podría tener un campo para almacenar su dirección actual pero no esperamos ningún tipo de comportamiento inteligente de una roca.

Del artículo vinculado sobre getters / setters como malvados:

El proceso de diseño de OO se centra en casos de uso: un usuario realiza tareas independientes que tienen algún resultado útil. (Iniciar sesión no es un caso de uso porque carece de un resultado útil en el dominio del problema. Dibujar un cheque de pago es un caso de uso). Luego, un sistema OO implementa las actividades necesarias para desarrollar los diversos escenarios que comprenden un caso de uso.

Sin ningún comportamiento definido, referirse a una roca como a Customerno cambia el hecho de que es solo un objeto con algunas propiedades que le gustaría rastrear y no importa qué trucos quiera jugar para alejarse de los atrapadores y setters A una roca no le importa si tiene un nombre válido y no se esperaría que una roca supiera si una dirección es válida o no.

Su sistema de pedidos podría asociar un Rockpedido de compra y, siempre que Rocktenga una dirección definida, alguna parte del sistema puede asegurarse de que un artículo se entregue a una roca.

En todos estos casos, Rockes solo un objeto de datos y seguirá siendo uno hasta que definamos comportamientos específicos con resultados útiles en lugar de hipotéticos.


Prueba esto:

Cuando evita sobrecargar la palabra 'Cliente' con 2 significados potencialmente diferentes, debería facilitar la conceptualización de las cosas.

¿Un Rockobjeto coloca una Orden o es algo que hace un ser humano haciendo clic en los elementos de la interfaz de usuario para activar acciones en su sistema?

nvuono
fuente
El cliente es un actor que hace muchas cosas, pero también tiene mucha información asociada; ¿eso justifica la creación de 2 clases separadas, 1 como actor y 1 como objeto de datos?
IntelliData
@IntelliData pasando objetos ricos a través de capas es donde las cosas se ponen difíciles. Si envía el objeto desde un controlador a una vista, la vista debe comprender el contrato general del objeto (por ejemplo, el estándar JavaBeans). Si está enviando el objeto a través del cable, JaxB o similar debe poder reinstalarlo en un objeto tonto (porque no les dio el objeto rico completo). Los objetos ricos son maravillosos para manipular datos; son pobres para transferir el estado. Por el contrario, los objetos tontos son pobres para manipular datos y están bien para transferir estados.
0

Agrego mis 2 centavos aquí mencionando el enfoque de objetos que hablan SQL .

Este enfoque se basa en la noción de objeto autocontenido. Tiene todos los recursos que necesita para implementar su comportamiento. No es necesario que le digan cómo hacer su trabajo: la solicitud declarativa es suficiente. Y un objeto definitivamente no tiene que contener todos sus datos como propiedades de clase. Realmente no importa, y no debería importar, de dónde provienen.

Hablando de un agregado , la inmutabilidad tampoco es un problema. Digamos que tiene una secuencia de estados que el agregado puede contener: Raíz agregada como saga está totalmente bien implementar cada estado como objeto independiente. Probablemente podría ir aún más lejos: chatee con su experto en dominios. Lo más probable es que él o ella no vea este agregado como una entidad unificada. Probablemente cada estado tiene su propio significado, mereciendo su propio objeto.

Finalmente, me gustaría señalar que el proceso de búsqueda de objetos es muy similar con la descomposición del sistema en subsistemas . Ambos se basan en el comportamiento, no en otra cosa.

Zapadlo
fuente