Recuerdo haber tenido una discusión similar con mi profesor cuando aprendí C ++ en la universidad. Simplemente no podía ver el punto de usar getters y setters cuando podía hacer pública una variable. Ahora entiendo mejor con años de experiencia y he aprendido una razón mejor que simplemente decir "mantener la encapsulación".
Al definir los captadores y establecedores, proporcionará una interfaz coherente para que si desea cambiar su implementación, sea menos probable que rompa el código dependiente. Esto es especialmente importante cuando sus clases están expuestas a través de una API y se utilizan en otras aplicaciones o por terceros. Entonces, ¿qué pasa con las cosas que entran en el getter o setter?
Los captadores generalmente están mejor implementados como un simple paso hacia abajo para acceder a un valor porque esto hace que su comportamiento sea predecible. Digo en general, porque he visto casos en los que los captadores se han utilizado para acceder a valores manipulados por cálculo o incluso por código condicional. En general, no es tan bueno si está creando componentes visuales para usar en tiempo de diseño, pero aparentemente es útil en tiempo de ejecución. Sin embargo, no existe una diferencia real entre esto y el uso de un método simple, excepto que cuando usa un método, generalmente es más probable que nombre un método de manera más apropiada para que la funcionalidad del "getter" sea más evidente al leer el código.
Compare lo siguiente:
int aValue = MyClass.Value;
y
int aValue = MyClass.CalculateValue();
La segunda opción deja en claro que el valor se está calculando, mientras que el primer ejemplo le dice que simplemente está devolviendo un valor sin saber nada sobre el valor en sí.
Quizás podría argumentar que lo siguiente sería más claro:
int aValue = MyClass.CalculatedValue;
Sin embargo, el problema es que está asumiendo que el valor ya ha sido manipulado en otro lugar. Por lo tanto, en el caso de un captador, si bien es posible que desee suponer que algo más podría estar sucediendo cuando devuelve un valor, es difícil aclarar esas cosas en el contexto de una propiedad, y los nombres de las propiedades nunca deben contener verbos de lo contrario, es difícil entender de un vistazo si el nombre utilizado debe estar decorado con paréntesis cuando se accede.
Sin embargo, los setters son un caso ligeramente diferente. Es completamente apropiado que un establecedor proporcione un procesamiento adicional para validar los datos que se envían a una propiedad, lanzando una excepción si establecer un valor violaría los límites definidos de la propiedad. Sin embargo, el problema que tienen algunos desarrolladores al agregar procesamiento a los establecedores es que siempre existe la tentación de que el colocador haga un poco más, como realizar un cálculo o una manipulación de los datos de alguna manera. Aquí es donde puede obtener efectos secundarios que en algunos casos pueden ser impredecibles o indeseables.
En el caso de los setters, siempre aplico una regla general simple, que es hacer lo menos posible a los datos. Por ejemplo, generalmente permitiré la prueba de límites y el redondeo para que pueda plantear excepciones si es apropiado, o evitar excepciones innecesarias donde puedan evitarse de manera sensata. Las propiedades de punto flotante son un buen ejemplo en el que es posible que desee redondear los decimales excesivos para evitar generar una excepción, al tiempo que permite ingresar valores de rango con algunos decimales adicionales.
Si aplica algún tipo de manipulación de la entrada del setter, tiene el mismo problema que con el getter, que es difícil permitir que otros sepan lo que está haciendo el setter simplemente nombrándolo. Por ejemplo:
MyClass.Value = 12345;
¿Le dice esto algo sobre lo que va a pasar con el valor cuando se le da al setter?
Qué tal si:
MyClass.RoundValueToNearestThousand(12345);
El segundo ejemplo le dice exactamente qué va a pasar con sus datos, mientras que el primero no le permitirá saber si su valor se modificará arbitrariamente. Al leer el código, el segundo ejemplo será mucho más claro en su propósito y función.
¿Estoy en lo cierto de que esto derrotaría por completo el propósito de tener captadores y establecedores en primer lugar y debería permitirse la validación y otra lógica (sin efectos secundarios extraños, por supuesto)?
Tener getters y setters no se trata de encapsular en aras de la "pureza", sino de encapsular para permitir que el código se refactorice fácilmente sin arriesgarse a un cambio en la interfaz de la clase que de lo contrario rompería la compatibilidad de la clase con el código de llamada. La validación es totalmente apropiada en un setter, sin embargo, existe un pequeño riesgo de que un cambio en la validación pueda romper la compatibilidad con el código de llamada si el código de llamada se basa en la validación que ocurre de una manera particular. Esta es una situación generalmente rara y de riesgo relativamente bajo, pero debe tenerse en cuenta en aras de la integridad.
¿Cuándo debe ocurrir la validación?
La validación debe realizarse dentro del contexto del establecedor antes de establecer realmente el valor. Esto garantiza que si se produce una excepción, el estado de su objeto no cambiará y potencialmente invalidará sus datos. En general, me parece mejor delegar la validación a un método separado que sería lo primero que se llama dentro del configurador, para mantener el código del configurador relativamente ordenado.
¿Se permite a un setter cambiar el valor (tal vez convertir un valor válido en alguna representación interna canónica)?
En casos muy raros, tal vez. En general, probablemente sea mejor no hacerlo. Este es el tipo de cosas que mejor se deja a otro método.