¿Debería pasar siempre los datos mínimos necesarios a una función en casos como este?

81

Digamos que tengo una función IsAdminque verifica si un usuario es administrador. Supongamos también que la comprobación del administrador se realiza haciendo coincidir el ID de usuario, el nombre y la contraseña con algún tipo de regla (no importante).

En mi cabeza, hay dos posibles firmas de funciones para esto:

public bool IsAdmin(User user);
public bool IsAdmin(int id, string name, string password);

A menudo busco el segundo tipo de firma, pensando que:

  • La firma de la función le da al lector mucha más información
  • La lógica contenida dentro de la función no tiene que saber sobre la Userclase
  • Por lo general, resulta un poco menos de código dentro de la función

Sin embargo, a veces cuestiono este enfoque y también me doy cuenta de que en algún momento sería difícil de manejar. Si, por ejemplo, una función mapeara entre diez campos de objetos diferentes en un bool resultante, obviamente enviaría todo el objeto. Pero aparte de un claro ejemplo como ese, no puedo ver una razón para pasar el objeto real.

Agradecería cualquier argumento para cualquier estilo, así como cualquier observación general que pueda ofrecer.

Programo tanto en estilos orientados a objetos como en estilos funcionales, por lo que la pregunta debe considerarse en relación con todos y cada uno de los modismos.

Anders Arpi
fuente
40
Fuera de tema: Por curiosidad, ¿por qué el estado "es administrador" de un usuario depende de su contraseña?
stakx
43
@ syb0rg Incorrecto. En C # esa es la convención de nomenclatura .
Magnético el
68
@ syb0rg Correcto, no especificó el idioma, así que creo que es bastante extraño decirle que está violando la convención de un determinado idioma.
Magnético el
31
@ syb0rg La afirmación general de que "los nombres de funciones no deberían comenzar con una letra mayúscula" es incorrecta, porque hay idiomas donde deberían hacerlo. De todos modos, no quise ofenderte, solo estaba tratando de aclarar esto. Esto se está volviendo bastante fuera de tema a la pregunta en sí. No veo la necesidad, pero si quieres continuar, pasemos el debate al chat .
Magnético el
66
Como punto general, poner en marcha su propia seguridad suele ser algo malo (tm) . La mayoría de los lenguajes y marcos tienen sistemas de seguridad / permisos disponibles y hay buenas razones para usarlos incluso cuando desea algo más simple.
James Snell el

Respuestas:

123

Personalmente prefiero el primer método de solo IsAdmin(User user)

Es mucho más fácil de usar, y si sus criterios para IsAdmin cambian en una fecha posterior (tal vez en función de los roles o isActive), no necesita volver a escribir la firma de su método en todas partes.

Probablemente también sea más seguro, ya que no anuncia qué propiedades determinan si un usuario es administrador o no, o si transfiere la propiedad de contraseña a todas partes. Y syrion señala que lo que sucede cuando tu idno coincide con el name/ password?

La longitud del código dentro de una función realmente no debería importar si el método hace su trabajo, y preferiría tener un código de aplicación más corto y simple que un código de método auxiliar.

Rachel
fuente
3
Creo que el problema de refactorización es un muy buen punto, gracias.
Anders Arpi el
1
Iba a hacer el argumento de firma del método si no lo hicieras. Esto es especialmente útil cuando hay muchas dependencias en su método que son mantenidas por otros programadores. Esto ayuda a mantener a las personas fuera del camino de los demás. +1
jmort253
1
Estoy de acuerdo con esta respuesta, pero permítanme señalar dos situaciones en las que el otro enfoque ("datos mínimos") podría ser preferible: (1) Desea almacenar en caché los resultados del método. Es mejor no usar un objeto completo como clave de caché, o tener que verificar las propiedades de un objeto en cada invocación. (2) Desea que el método / función se ejecute de forma asíncrona, lo que podría implicar serializar los argumentos y, por ejemplo, almacenarlos en una tabla. Es más fácil y sencillo buscar un número entero que un objeto blob al administrar trabajos pendientes.
Gladstone
La obsesión primitiva antipatrón podría ser horrible para las pruebas unitarias.
Samuel
82

La primera firma es superior porque permite la encapsulación de la lógica relacionada con el usuario en el Userobjeto mismo. No es beneficioso tener lógica para la construcción de la tupla de "id, nombre, contraseña" esparcida sobre su base de código. Además, complica la isAdminfunción: ¿qué sucede si alguien pasa en una idque no coincide con name, por ejemplo? ¿Qué sucede si desea verificar si un usuario determinado es un administrador de un contexto en el que no debe conocer su contraseña?

Además, como una nota sobre su tercer punto a favor del estilo con argumentos adicionales; puede resultar en "menos código dentro de la función", pero ¿a dónde va esa lógica? ¡No puede desaparecer! En cambio, se extiende por todos los lugares donde se llama la función. Para guardar cinco líneas de código en un solo lugar, ¡ha pagado cinco líneas por uso de la función!

asthasr
fuente
45
Siguiendo tu lógica, ¿no user.IsAdmin()sería mucho mejor?
Anders Arpi el
13
Si, absolutamente.
asthasr
66
@ AndersHolmström Depende, ¿está probando si el usuario es un administrador en general, o simplemente un administrador de la página / formulario actual en el que se encuentra? Si en general, entonces sí, eso sería mejor, pero si solo está probando IsAdminuna forma o función específica, eso no debería estar relacionado con el usuario
Rachel
40
@ Ansders: no, no debería. Un usuario no debe decidir si es un administrador o no. El sistema de privilegios o seguridad debería. Algo que está estrechamente relacionado con una clase no significa que sea una buena idea hacer que forme parte de esa clase
Marjan Venema
11
@MarjanVenema: la idea de que la clase Usuario no debe "decidir" si una instancia es un administrador me parece un poco culto a la carga. Realmente no puedes hacer esa generalización con tanta fuerza. Si sus necesidades son complejas, entonces quizás sea beneficioso encapsularlas en un "sistema" separado, pero citando a KISS + YAGNI, me gustaría tomar esa decisión frente a esa complejidad, y no como una regla general :-). Y tenga en cuenta que incluso si la lógica no es "parte" del Usuario, puede ser útil, por cuestiones prácticas, tener un traspaso en el Usuario que simplemente delegue en un sistema más complejo.
Eamon Nerbonne
65

El primero, pero no (solo) por las razones que otros han dado.

public bool IsAdmin(User user);

Esto es de tipo seguro. Especialmente porque el usuario es un tipo que usted definió, hay poca o ninguna oportunidad de transponer o cambiar argumentos. Claramente opera en los usuarios y no es una función genérica que acepte cualquier int y dos cadenas. Esta es una gran parte del objetivo de usar un lenguaje de tipo seguro como Java o C #, como se ve su código. Si está preguntando sobre un idioma específico, es posible que desee agregar una etiqueta para ese idioma a su pregunta.

public bool IsAdmin(int id, string name, string password);

Aquí, cualquier int y dos cadenas servirá. ¿Qué le impide transponer el nombre y la contraseña? El segundo es más trabajo para usar y permite más oportunidades de error.

Presumiblemente, ambas funciones requieren que se llame a user.getId (), user.getName () y user.getPassword (), ya sea dentro de la función (en el primer caso) o antes de llamar a la función (en el segundo), entonces La cantidad de acoplamiento es la misma en ambos sentidos. En realidad, dado que esta función solo es válida en Usuarios, en Java eliminaría todos los argumentos y haría de este un método de instancia en objetos de Usuario:

user.isAdmin();

Dado que esta función ya está estrechamente vinculada a los usuarios, tiene sentido que sea parte de lo que es un usuario.

PD: Estoy seguro de que esto es solo un ejemplo, pero parece que está almacenando una contraseña. En su lugar, solo debe almacenar un hash de contraseña criptográficamente seguro. Durante el inicio de sesión, la contraseña suministrada se debe cifrar de la misma manera y comparar con el hash almacenado. Se debe evitar enviar contraseñas en texto plano. Si viola esta regla e isAdmin (int Str Str) registrara el nombre de usuario, entonces si transpuso el nombre y la contraseña en su código, la contraseña podría registrarse en su lugar, lo que crea un problema de seguridad (no escriba contraseñas en los registros) ) y es otro argumento a favor de pasar un objeto en lugar de sus partes componentes (o usar un método de clase).

GlenPeterson
fuente
11
Un usuario no debe decidir si es un administrador o no. El sistema de privilegios o seguridad debería. Algo que esté estrechamente vinculado a una clase no significa que sea una buena idea hacer que forme parte de esa clase.
Marjan Venema el
66
@MarjanVenema El usuario no está decidiendo nada. El objeto / tabla Usuario almacena datos sobre cada usuario. Los usuarios reales no pueden cambiar todo sobre ellos mismos. Incluso cuando el usuario cambia algo que se le permite, puede activar alertas de seguridad como un correo electrónico si cambian su dirección de correo electrónico o su ID de usuario o contraseña. ¿Está utilizando un sistema en el que los usuarios pueden cambiar mágicamente todo sobre ciertos tipos de objetos? No estoy familiarizado con tal sistema.
GlenPeterson el
2
@GlenPeterson: ¿me estás malinterpretando deliberadamente? Cuando dije usuario, por supuesto, me refería a la clase de usuario.
Marjan Venema el
2
@GlenPeterson Totalmente fuera de tema, pero múltiples rondas de hashing y funciones criptográficas debilitarán los datos.
Gusdor el
3
@MarjanVenema: no estoy siendo deliberadamente difícil. Creo que tenemos perspectivas muy diferentes y me gustaría entender mejor la suya. He abierto una nueva pregunta donde esto se puede discutir mejor: programmers.stackexchange.com/questions/216443/…
GlenPeterson
6

Si su lenguaje de elección no pasa estructuras por valor, entonces la (User user)variante parece mejor. ¿Qué sucede si algún día decide eliminar el nombre de usuario y solo usa la ID / contraseña para identificar al usuario?

Además, este enfoque inevitablemente conduce a llamadas de método largas y exageradas, o inconsistencias. ¿Está bien pasar 3 argumentos? ¿Qué tal 5? O 6? ¿Por qué pensar en eso, si pasar un objeto es barato en términos de recursos (incluso más barato que pasar 3 argumentos, probablemente)?

Y estoy (en cierto modo) en desacuerdo con que el segundo enfoque le da al lector más información: intuitivamente, la llamada al método pregunta "si el usuario tiene derechos de administrador", no "si la combinación de identificación / nombre / contraseña dada tiene derechos de administrador".

Entonces, a menos que pasar el Userobjeto resulte en copiar una estructura grande, el primer enfoque es más limpio y más lógico para mí.

Maciej Stachowski
fuente
3

Probablemente tendría ambos. El primero como API externo, con alguna esperanza de estabilidad en el tiempo. El segundo como una implementación privada llamada por la API externa.

Si en algún momento posterior tuviera que cambiar la regla de verificación, simplemente escribiría una nueva función privada con una nueva firma llamada por la externa.

Esto tiene la ventaja de la facilidad de cambiar la implementación interna. También es muy habitual que en algún momento pueda necesitar ambas funciones disponibles al mismo tiempo y llamar a una u otra dependiendo de algún cambio de contexto externo (por ejemplo, puede tener usuarios antiguos coexistentes y usuarios nuevos con diferentes campos).

Con respecto al título de la pregunta, de alguna manera, en ambos casos, está proporcionando la información mínima necesaria. Un usuario parece ser la estructura de datos relacionados con la aplicación más pequeña que contiene la información. Los tres campos id, contraseña y nombre parecen ser los datos de implementación más pequeños realmente necesarios, pero en realidad no son objetos de nivel de aplicación.

En otras palabras, si se tratara de una base de datos, un Usuario sería un registro, mientras que el ID, la contraseña y el inicio de sesión serían campos en ese registro.

kriss
fuente
No veo la razón del método privado. ¿Por qué mantener ambos? Si el método privado necesita argumentos diferentes, aún tendrá que modificar uno público. Y lo probarás a través de uno público. Y no, no es habitual que necesites ambas en algún momento posterior. Estás adivinando aquí - YAGNI.
Piotr Perak
Asume, por ejemplo, que la estructura de datos del usuario no tiene ningún otro campo desde el principio. Por lo general, no es cierto. La segunda función deja en claro qué parte interna del usuario se verifica para ver si es un administrador. Los casos de uso típicos pueden ser del tipo: si el tiempo de creación del usuario es anterior a alguna fecha, llame a la regla anterior, si no se llama a la nueva regla (que puede depender de otras partes del Usuario, o depende de la misma pero usando otra regla). Al dividir el código de esta manera obtendrá dos funciones mínimas: una API conceptual mínima y una función de implementación mínima.
kriss
en otras palabras, de esta manera será inmediatamente visible desde la firma de la función interna qué partes del Usuario se usan o no. En el código de producción eso no siempre es obvio. Actualmente estoy trabajando en una gran base de código con varios años de historial de mantenimiento y este tipo de división es muy útil para el mantenimiento a largo plazo. Si no tiene la intención de mantener el código, de hecho es inútil (pero incluso proporcionar una API conceptual estable también es probablemente inútil).
kriss
Otra palabra: ciertamente no haré pruebas unitarias del método interno a través del externo. Esa es una mala práctica de prueba.
kriss
1
Esta es una buena práctica y tiene un nombre. Se llama el "principio de responsabilidad única". Se puede aplicar tanto a clases como a métodos. Tener métodos con una sola responsabilidad es una buena manera de crear códigos más modulares y mantenibles. En este caso, una tarea es elegir un conjunto básico de datos del objeto de usuario, y otra tarea es verificar ese conjunto básico de datos para ver si el usuario es un administrador. Siguiendo este principio, obtienes la ventaja de poder componer métodos y evitar la duplicación de código. Esto también significa, según lo indicado por @kriss, que obtienes mejores pruebas unitarias.
diegoreymendez
3

Hay un punto negativo para enviar clases (tipos de referencia) a los métodos, y es la vulnerabilidad de asignación masiva . Imagina que en lugar de IsAdmin(User user)método, tengo UpdateUser(User user)método.

Si la Userclase tiene una propiedad booleana llamada IsAdmin, y si no la verifico durante la ejecución de mi método, entonces soy vulnerable a la asignación masiva. GitHub fue atacado por esta misma firma en 2012.

Saeed Neamati
fuente
2

Yo iría con este enfoque:

public bool IsAdmin(this IUser user) { /* ... */ }

public class User: IUser { /* ... */ }

public interface IUser
{
    string Username {get;}
    string Password {get;}
    IID Id {get;}
}
  • El uso del tipo de interfaz como parámetro para esta función le permite pasar objetos de usuario, definir objetos de usuario simulados para probar, y le brinda más flexibilidad en cuanto al uso y mantenimiento de esta función.

  • También agregué la palabra clave thispara hacer que la función sea un método de extensión de todas las clases de IUser; de esta manera, puede escribir código de una manera más amigable para OO y usar esta función en consultas de Linq. Aún más simple sería definir esta función en IUser y User, pero supongo que hay alguna razón por la que decidió poner este método fuera de esa clase.

  • Utilizo la interfaz personalizada en IIDlugar de, intya que esto le permite redefinir las propiedades de su ID; por ejemplo, en caso de que alguna vez tiene que cambiar de inta long, Guido algo más. Probablemente sea una exageración, pero siempre es bueno tratar de flexibilizar las cosas para que no te quedes atrapado en decisiones tempranas.

  • NB: su objeto IUser puede estar en un espacio de nombres y / o ensamblado diferente a la clase de usuario, por lo que tiene la opción de mantener su código de usuario completamente separado de su código IsAdmin, con ellos solo compartiendo una biblioteca referenciada. Exactamente lo que haces es una llamada sobre cómo sospechas que se usarán estos artículos.

JohnLBevan
fuente
3
IUser es inútil aquí y solo agrega complejidad. No veo por qué no puedes usar un usuario real en las pruebas.
Piotr Perak
1
Agrega flexibilidad futura para muy poco esfuerzo, por lo que si bien no agrega valor actualmente, no hace daño. Personalmente, prefiero hacer esto siempre, ya que evita tener que pensar en las posibilidades futuras para todos los escenarios (lo cual es prácticamente imposible dada la naturaleza impredecible de los requisitos, y tomará mucho más tiempo incluso obtener una respuesta parcialmente buena de lo que lo haría). pegarse en una interfaz).
JohnLBevan
@Peri una clase de usuario real puede ser compleja de crear, tomar una interfaz le permite simularla para que pueda mantener sus pruebas simples
jk.
@JohnLBevan: ¡YAGNI!
Piotr Perak
@jk .: si es difícil crear un Usuario, entonces algo está mal
Piotr Perak
1

Descargos de responsabilidad:

Para mi respuesta, me centraré en el tema del estilo y olvidaré si un ID, inicio de sesión y contraseña simple es un buen conjunto de parámetros para usar, desde el punto de vista de la seguridad. Debería poder extrapolar mi respuesta a cualquier conjunto básico de datos que esté utilizando para descubrir los privilegios del usuario ...

También considero user.isAdmin()que es equivalente a IsAdmin(User user). Esa es una decisión que debes tomar.

Respuesta:

Mi recomendación es para cualquier usuario solamente:

public bool IsAdmin(User user);

O use ambos, a lo largo de las líneas de:

public bool IsAdmin(User user); // calls the private method below
private bool IsAdmin(int id, string name, string password);

Razones para el método público:

Al escribir métodos públicos, generalmente es una buena idea pensar cómo se utilizarán.

Para este caso en particular, la razón por la que generalmente llamaría a este método es para averiguar si cierto usuario es un administrador. Ese es un método que necesitas. Realmente no desea que cada persona que llama tenga que elegir los parámetros correctos para enviar (y corre el riesgo de errores debido a la duplicación de código).

Razones para el método privado:

El método privado que recibe solo el conjunto mínimo de parámetros requeridos puede ser algo bueno a veces. A veces no tanto.

Una de las ventajas de dividir estos métodos es que puede llamar a la versión privada desde varios métodos públicos en la misma clase. Por ejemplo, si desea (por cualquier motivo) tener una lógica ligeramente diferente Localusery RemoteUser(¿tal vez hay una configuración para activar / desactivar los administradores remotos?):

public bool IsAdmin(Localuser user); // Just call private method
public bool IsAdmin(RemoteUser user); // Check if setting is on, then call private method
private bool IsAdmin(int id, string name, string password);

Además, si por alguna razón necesita hacer público el método privado ... puede hacerlo. Es tan fácil como cambiar privatea public. Puede no parecer una gran ventaja para este caso, pero a veces lo es realmente.

Además, cuando realice pruebas unitarias, podrá realizar muchas más pruebas atómicas. Por lo general, es muy bueno no tener que crear un objeto de usuario completo para ejecutar pruebas en este método. Y si desea una cobertura completa, puede probar ambas llamadas.

diegoreymendez
fuente
0
public bool IsAdmin(int id, string name, string password);

es malo por los motivos enumerados. Eso no quiere decir

public bool IsAdmin(User user);

es automáticamente bueno En particular, si el usuario es un objeto que tiene métodos como: getOrders(), getFriends(), getAdresses(), ...

Puede considerar refactorizar el dominio con un tipo de credenciales de usuario que contenga solo ID, nombre, contraseña y pasarlo a IsAdmin en lugar de todos los datos de usuario innecesarios.

tkruse
fuente