Eu tenho uma interface chamada IContext
. Para isso, não importa realmente o que faz, exceto o seguinte:
T GetService<T>();
O que esse método faz é examinar o contêiner DI atual do aplicativo e tentar resolver a dependência. Bastante padrão, eu acho.
No meu aplicativo ASP.NET MVC, meu construtor se parece com isso.
protected MyControllerBase(IContext ctx)
{
TheContext = ctx;
SomeService = ctx.GetService<ISomeService>();
AnotherService = ctx.GetService<IAnotherService>();
}
Portanto, em vez de adicionar vários parâmetros no construtor para cada serviço (porque isso será realmente irritante e demorado para os desenvolvedores que estendem o aplicativo), estou usando esse método para obter serviços.
Agora, parece errado . No entanto, a maneira como atualmente estou justificando isso é isso - eu posso zombar .
Eu posso. Não seria difícil zombar IContext
para testar o Controlador. Eu teria que de qualquer maneira:
public class MyMockContext : IContext
{
public T GetService<T>()
{
if (typeof(T) == typeof(ISomeService))
{
// return another mock, or concrete etc etc
}
// etc etc
}
}
Mas como eu disse, parece errado. Quaisquer pensamentos / abuso são bem-vindos.
fonte
public SomeClass(Context c)
. Este código é bastante claro, não é? Afirma,that SomeClass
depende de aContext
. Err, mas espere, isso não acontece! Ele depende apenas da dependênciaX
obtida do Contexto. Isso significa que toda vez que você fizer uma alteraçãoContext
, poderá quebrarSomeObject
, mesmo que você tenha mudado apenasContext
sY
. Mas sim, você sabe que mudou apenasY
nãoX
, então tudoSomeClass
bem. Mas escrever um bom código não é sobre o que você sabe, mas sobre o que o novo funcionário sabe quando olha para o seu código pela primeira vez.Respostas:
Ter um em vez de muitos parâmetros no construtor não é a parte problemática desse design . Desde que sua
IContext
classe não seja senão uma fachada de serviço , especificamente para fornecer as dependências usadas noMyControllerBase
e não um localizador de serviço geral usado em todo o código, essa parte do código está IMHO ok.Seu primeiro exemplo pode ser alterado para
isso não seria uma mudança substancial no design
MyControllerBase
. Se esse design for bom ou ruim, depende apenas do fato de você quererTheContext
,SomeService
eAnotherService
são sempre tudo inicializado com objetos fictícios, ou todas elas com objetos reaisPortanto, o uso de apenas um parâmetro em vez de 3 no construtor pode ser totalmente razoável.
O que é problemático é
IContext
expor oGetService
método em público. IMHO você deve evitar isso, em vez disso, mantenha os "métodos de fábrica" explícitos. Então, tudo bem implementar os métodosGetSomeService
eGetAnotherService
do meu exemplo usando um localizador de serviço? IMHO que depende. Desde que aIContext
classe continue sendo apenas uma fábrica abstrata simples com o objetivo específico de fornecer uma lista explícita de objetos de serviço, isso é aceitável pelo IMHO. Fábricas abstratas são tipicamente apenas códigos de "cola", que não precisam ser testadas por unidade. No entanto, você deve se perguntar se, no contexto de métodos comoGetSomeService
, se realmente precisa de um localizador de serviço ou se uma chamada explícita ao construtor não seria mais simples.Portanto, tenha cuidado, quando você se apega a um design em que a
IContext
implementação é apenas um invólucro em torno de umGetService
método público genérico , permitindo resolver quaisquer dependências arbitrárias por classes arbitrárias, tudo se aplica ao que @BenjaminHodgson escreveu em sua resposta.fonte
GetService
método genérico . Refatorar para métodos explicitamente nomeados e digitados é melhor. Melhor ainda seria explicitar explicitamente as dependências daIContext
implementação no construtor.IValidatableObject
?Esse design é conhecido como Service Locator * e eu não gosto. Existem muitos argumentos contra:
O Localizador de serviços o acopla ao seu contêiner . Usando injeção de dependência regular (onde o construtor explicita explicitamente as dependências), você pode substituir diretamente seu contêiner por outro diferente ou voltar para
new
expressões. Com o seu,IContext
isso não é realmente possível.O Localizador de serviço oculta dependências . Como cliente, é muito difícil dizer o que você precisa para construir uma instância de uma classe. Você precisa de algum tipo de
IContext
, mas também precisa configurar o contexto para retornar os objetos corretos para fazer oMyControllerBase
trabalho. Isso não é de todo óbvio a partir da assinatura do construtor. Com DI regular, o compilador informa exatamente o que você precisa. Se a sua turma tem muitas dependências, você deve sentir essa dor, porque isso o estimulará a refatorar. O Localizador de serviço oculta os problemas com designs ruins.O Localizador de serviço causa erros em tempo de execução . Se você chamar
GetService
com um parâmetro de tipo ruim, receberá uma exceção. Em outras palavras, suaGetService
função não é uma função total. (Funções totais são uma ideia do mundo do FP, mas basicamente significa que as funções devem sempre retornar um valor.) Melhor deixar o compilador ajudar e informar quando você tem as dependências erradas.O Localizador de serviço viola o princípio de substituição de Liskov . Como seu comportamento varia de acordo com o argumento de tipo, o Service Locator pode ser exibido como se tivesse efetivamente um número infinito de métodos na interface! Este argumento é explicado em detalhes aqui .
É difícil testar o Localizador de Serviço . Você deu um exemplo de falsificação
IContext
para testes, o que é bom, mas certamente é melhor não ter que escrever esse código em primeiro lugar. Basta injetar suas dependências falsas diretamente, sem passar pelo localizador de serviços.Em suma, apenas não faça isso . Parece uma solução sedutora para o problema de classes com muitas dependências, mas a longo prazo você apenas tornará sua vida miserável.
* Estou definindo o Service Locator como um objeto com um
Resolve<T>
método genérico capaz de resolver dependências arbitrárias e usado em toda a base de código (não apenas na raiz da composição). Não é o mesmo que Service Facade (um objeto que agrupa um pequeno conjunto conhecido de dependências) ou Abstract Factory (um objeto que cria instâncias de um único tipo - o tipo de Abstract Factory pode ser genérico, mas o método não é) .fonte
MyControllerBase
não é acoplado a um contêiner de DI específico, nem esse "realmente" é um exemplo para o antipadrão do Localizador de Serviço.GetService<T>
método genérico . Resolver dependências arbitrárias é o cheiro real, presente e correto no exemplo do OP.GetService<T>()
método permite que classes arbitrárias sejam solicitadas: "O que esse método faz é olhar para o contêiner DI atual do aplicativo e tentar resolver a dependência. Penso que é bastante padrão". . Eu estava respondendo ao seu comentário na parte superior desta resposta. Isso é 100% um localizador de serviçoOs melhores argumentos contra o antipadrão do Localizador de Serviço são claramente declarados por Mark Seemann, por isso não vou falar muito sobre por que essa é uma péssima idéia - é uma jornada de aprendizado que você precisa dedicar um tempo para entender por si mesmo (I também recomende o livro de Mark ).
OK, então, para responder à pergunta - vamos repor o seu problema real :
Há uma pergunta que resolve esse problema no StackOverflow . Como mencionado em um dos comentários lá:
Você está procurando no lugar errado a solução para o seu problema. É importante saber quando uma aula está fazendo muito. No seu caso, eu suspeito fortemente que não há necessidade de um "Controlador de Base". De fato, no POO quase sempre não há necessidade de herança . Variações no comportamento e na funcionalidade compartilhada podem ser obtidas inteiramente através do uso apropriado de interfaces, o que geralmente resulta em um código melhorado e encapsulado - e não é necessário passar dependências para os construtores da superclasse.
Em todos os projetos em que trabalhei onde existe um Controlador Base, isso foi feito exclusivamente para o compartilhamento de propriedades e métodos convenientes, como
IsUserLoggedIn()
eGetCurrentUserId()
. PARAR . Isso é horrível mau uso da herança. Em vez disso, crie um componente que exponha esses métodos e dependa dele onde for necessário. Dessa forma, seus componentes permanecerão testáveis e suas dependências serão óbvias.Além de qualquer outra coisa, ao usar o padrão MVC, eu sempre recomendaria controladores magros . Você pode ler mais sobre isso aqui, mas a essência do padrão é simples: os controladores no MVC devem fazer apenas uma coisa: manipular argumentos transmitidos pela estrutura do MVC, delegando outras preocupações a algum outro componente. Esse é novamente o princípio de responsabilidade única em ação.
Seria realmente útil conhecer seu caso de uso para fazer um julgamento mais preciso, mas honestamente, não consigo pensar em nenhum cenário em que uma classe base seja preferível a dependências bem fatoradas.
fonte
protected
delegações de uma linha para os novos componentes. Em seguida, você pode perder BaseController e substituir osprotected
métodos com chamadas diretas para dependências - o que irá simplificar o seu modelo e fazer todas as suas dependências explícita (que é uma coisa muito boa em um projeto com 100s de controladores!)Estou adicionando uma resposta a isso com base nas contribuições de todos os outros. Muito obrigado a todos. Primeiro, aqui está a minha resposta: "Não, não há nada errado com isso".
Resposta do Doc Brown "Service Facade" Aceitei essa resposta porque o que estava procurando (se a resposta fosse "não") eram alguns exemplos ou alguma expansão do que estava fazendo. Ele forneceu isso sugerindo que A) tem um nome e B) provavelmente existem maneiras melhores de fazer isso.
Resposta do "Localizador de serviço" de Benjamin Hodgson Por mais que eu aprecie o conhecimento que adquiri aqui, o que tenho não é um "Localizador de serviço". É uma "Fachada de Serviço". Tudo nesta resposta está correto, mas não para as minhas circunstâncias.
Respostas da USR
Vou abordar isso com mais detalhes:
Não perco nenhuma ferramenta e não estou perdendo nenhuma digitação "estática". A fachada do serviço retornará o que eu configurei no meu contêiner de DI ou
default(T)
. E o que ele retorna é "digitado". A reflexão é encapsulada.Certamente não é "raro". Como estou usando um controlador de base , toda vez que precisar alterar seu construtor, talvez seja necessário alterar 10, 100, 1000 outros controladores.
Estou usando injeção de dependência. Essa é a questão.
E, finalmente, no comentário de Jules sobre a resposta de Benjamin, não estou perdendo nenhuma flexibilidade. É a minha fachada de serviço. Posso adicionar quantos parâmetros
GetService<T>
desejar para distinguir entre diferentes implementações, da mesma maneira que faria ao configurar um contêiner de DI. Por exemplo, eu poderia mudarGetService<T>()
paraGetService<T>(string extraInfo = null)
contornar esse "problema em potencial".De qualquer forma, obrigado novamente a todos. Tem sido realmente útil. Felicidades.
fonte
GetService<T>()
método é capaz de (tentar) resolver dependências arbitrárias . Isso o torna um localizador de serviço, não uma fachada de serviço, como expliquei na nota de rodapé da minha resposta. Se você o substituísse por um pequeno conjunto deGetServiceX()
/GetServiceY()
métodos, como sugere o @DocBrown, seria uma Fachada.IContext
e injetar isso, e crucialmente: se você adicionar uma nova dependência, o código ainda irá compilar - mesmo que os testes irão falhar em tempo de execução