A resposta simples é: sempre que uma operação for impossível (por causa de um aplicativo OU por violar a lógica de negócios). Se um método for chamado e for impossível fazer o que o método foi escrito, execute uma exceção. Um bom exemplo é que os construtores sempre lançam ArgumentExceptions se uma instância não puder ser criada usando os parâmetros fornecidos. Outro exemplo é InvalidOperationException, lançado quando uma operação não pode ser executada devido ao estado de outro membro ou membros da classe.
No seu caso, se um método como Login (nome de usuário, senha) for chamado, se o nome de usuário não for válido, é realmente correto lançar uma UserNameNotValidException ou PasswordNotCorrectException se a senha estiver incorreta. O usuário não pode fazer login usando o (s) parâmetro (s) fornecido (s) (ou seja, é impossível porque violaria a autenticação), então lance uma exceção. Embora eu possa ter suas duas exceções herdadas de ArgumentException.
Dito isto, se você NÃO deseja lançar uma exceção porque uma falha de logon pode ser muito comum, uma estratégia é criar um método que retorne tipos que representam falhas diferentes. Aqui está um exemplo:
{ // class
...
public LoginResult Login(string user, string password)
{
if (IsInvalidUser(user))
{
return new UserInvalidLoginResult(user);
}
else if (IsInvalidPassword(user, password))
{
return new PasswordInvalidLoginResult(user, password);
}
else
{
return new SuccessfulLoginResult();
}
}
...
}
public abstract class LoginResult
{
public readonly string Message;
protected LoginResult(string message)
{
this.Message = message;
}
}
public class SuccessfulLoginResult : LoginResult
{
public SucccessfulLogin(string user)
: base(string.Format("Login for user '{0}' was successful.", user))
{ }
}
public class UserInvalidLoginResult : LoginResult
{
public UserInvalidLoginResult(string user)
: base(string.Format("The username '{0}' is invalid.", user))
{ }
}
public class PasswordInvalidLoginResult : LoginResult
{
public PasswordInvalidLoginResult(string password, string user)
: base(string.Format("The password '{0}' for username '{0}' is invalid.", password, user))
{ }
}
A maioria dos desenvolvedores é ensinada a evitar exceções devido à sobrecarga causada por lançá-las. É ótimo ter consciência de recursos, mas geralmente não às custas do design do aplicativo. Essa é provavelmente a razão pela qual você foi instruído a não lançar suas duas exceções. O uso ou não de exceções geralmente se resume à frequência com que a exceção ocorrerá. Se é um resultado bastante comum ou bastante esperado, é nesse momento que a maioria dos desenvolvedores evita Exceções e cria outro método para indicar falha, devido ao suposto consumo de recursos.
Aqui está um exemplo de como evitar exceções em um cenário como o descrito, usando o padrão Try ():
public class ValidatedLogin
{
public readonly string User;
public readonly string Password;
public ValidatedLogin(string user, string password)
{
if (IsInvalidUser(user))
{
throw new UserInvalidException(user);
}
else if (IsInvalidPassword(user, password))
{
throw new PasswordInvalidException(password);
}
this.User = user;
this.Password = password;
}
public static bool TryCreate(string user, string password, out ValidatedLogin validatedLogin)
{
if (IsInvalidUser(user) ||
IsInvalidPassword(user, password))
{
return false;
}
validatedLogin = new ValidatedLogin(user, password);
return true;
}
}