
Eu tenho pesquisado OOP dia e noite por mais de dois anos. Leia uma grande pilha de livros, passei meses refatorando código procedural para orientado a objetos e vice-versa. Um amigo diz que ganhei OOP do cérebro. Mas tenho confiança de que posso resolver problemas complexos e escrever um código claro?
Eu invejo as pessoas que conseguem passar com confiança suas opiniões delirantes. Principalmente quando se trata de desenvolvimento, arquitetura. Em geral, o que aspiro com paixão, mas sobre o que tenho dúvidas intermináveis. Porque eu não sou um gênio e não sou um FP, não tenho uma história de sucesso. Mas deixe-me colocar 5 copeques.
Encapsulamento, polimorfismo, pensamento de objeto ...?
Você gosta quando está carregado de termos? Eu li o suficiente, mas as palavras acima ainda não me dizem nada em particular. Estou acostumado a explicar as coisas em uma linguagem que entendo. Um nível de abstração, se você quiser. E há muito tempo queria saber a resposta a uma pergunta simples: "O que o OOP oferece?" De preferência com exemplos de código. E hoje vou tentar responder sozinho. Mas primeiro, uma pequena abstração.
Complexidade da tarefa
O desenvolvedor está de uma forma ou de outra empenhado em resolver problemas. Cada tarefa possui muitos detalhes. A partir das especificidades da API de interação com um computador, terminando com os detalhes da lógica de negócios.
Outro dia coletei um mosaico com minha filha. Costumávamos colecionar quebra-cabeças de grande tamanho, literalmente de 9 peças. E agora ela pode manipular pequenos mosaicos para crianças a partir dos 3 anos. É interessante! Como o cérebro encontra seu lugar entre os quebra-cabeças espalhados. E o que determina a complexidade?
A julgar pelos mosaicos infantis, a complexidade é determinada principalmente pelo número de detalhes. Não tenho certeza se a analogia do quebra-cabeça abrangerá todo o processo de desenvolvimento. Mas o que mais você pode comparar o nascimento de um algoritmo no momento de escrever um corpo de função? E me parece que reduzir a quantidade de detalhes é uma das simplificações mais significativas.
Para mostrar mais claramente a principal característica da OOP, vamos falar sobre tarefas, a quantidade de detalhes que não nos permite montar um quebra-cabeça em um tempo razoável. Nesses casos, precisamos de decomposição.
Decomposição
Como você sabe da escola, um problema complexo pode ser dividido em problemas mais simples para resolvê-los separadamente. A essência da abordagem é limitar o número de peças.
Acontece que enquanto aprendemos a programar, nos acostumamos a trabalhar com uma abordagem procedimental. Quando há um dado na entrada, o qual transformamos, o lançamos em subfunções e o mapeamos para o resultado. E, por fim, decompomos durante a refatoração quando a solução já está lá.
Qual é o problema com a decomposição procedural? Por hábito, precisamos de dados iniciais, de preferência com uma estrutura finalmente formada. Além disso, quanto maior a tarefa, mais complexa é a estrutura desses dados iniciais e mais detalhes você precisa ter em mente. Mas como ter certeza de que haverá dados iniciais suficientes para resolver subtarefas e, ao mesmo tempo, se livrar da soma de todos os detalhes no nível superior?
Vejamos um exemplo. Não faz muito tempo, escrevi um script que faz montagens de projetos e os joga nas pastas necessárias.
interface BuildConfig {
id: string;
deployPath: string;
options: BuildOptions;
// ...
}
interface TestService {
runTests(buildConfigs: BuildConfig[]): Promise<void>;
}
interface DeployService {
publish(buildConfigs: BuildConfig[]): Promise<void>;
}
class Builder {
constructor(
private testService: TestService,
private deployService: DeployService
) // ...
{}
async build(buildConfigs: BuildConfig[]): Promise<void> {
await this.testService.runTests(buildConfigs);
await this.build(buildConfigs);
await this.deployService.publish(buildConfigs);
// ...
}
// ...
}
Pode parecer que apliquei OOP nesta solução. Você pode substituir implementações de serviço, você pode até mesmo testar algo. Mas, na verdade, este é um excelente exemplo de abordagem procedimental.
Dê uma olhada na interface BuildConfig. Esta é uma estrutura que criei no início da escrita do código. Percebi de antemão que não poderia prever todos os parâmetros com antecedência e simplesmente adicionei campos a essa estrutura conforme necessário. No meio do trabalho, a configuração foi tomada por um monte de campos que foram usados em diferentes partes do sistema. Fiquei incomodado com a presença de um "objeto" que precisa ser finalizado a cada mudança. É difícil navegar nele e é fácil quebrar algo confundindo os nomes dos campos. E ainda, todas as partes do sistema de compilação dependem de BuildConfig. Como essa tarefa não é tão volumosa e crítica, não houve desastre. Mas é claro que se o sistema fosse mais complicado, eu teria bagunçado o projeto.
Um objeto
O principal problema da abordagem procedimental são os dados, sua estrutura e quantidade. A complexa estrutura de dados apresenta detalhes que tornam a tarefa difícil de entender. Agora, observe suas mãos, não há engano aqui.
Vamos lembrar, por que precisamos de dados? Para realizar operações neles e obter o resultado. Freqüentemente, sabemos quais subtarefas precisam ser resolvidas, mas não entendemos que tipo de dados são necessários para isso.
Atenção! Podemos manipular as operações sabendo que eles possuem os dados com antecedência para executá-las.
Objeto permite substituir um conjunto de dados por um conjunto de operações. E se reduzir o número de peças, simplifica parte da tarefa!
// , /
interface BuildConfig {
id: string;
deployPath: string;
options: BuildOptions;
// ...
}
// vs
// ,
interface Project {
test(): Promise<void>;
build(): Promise<void>;
publish(): Promise<void>;
}
A transformação é muito simples: f (x) -> of (), onde o é menor que x . O secundário se escondeu dentro do objeto. Ao que parece, qual é o efeito de transferir o código com a configuração de um lugar para outro? Mas essa transformação tem implicações de longo alcance. Podemos fazer o mesmo para o resto do programa.
// project.ts
// , Project .
class Project {
constructor(
private buildTester: BuildTester,
private builder: Builder,
private buildPublisher: BuildPublisher
) {}
async test(): Promise<void> {
await this.buildTester.runTests();
}
async build(): Promise<void> {
await this.builder.build();
}
async publish(): Promise<void> {
await this.buildPublisher.publish();
}
}
// builder.ts
export interface BuildOptions {
baseHref: string;
outputPath: string;
configuration?: string;
}
export class Builder {
constructor(private options: BuildOptions) {}
async build(): Promise<void> {
// ...
}
}
Agora, o Builder recebe apenas os dados de que precisa, como outras partes do sistema. Ao mesmo tempo, as classes que recebem o Builder por meio do construtor não dependem dos parâmetros necessários para inicializá-lo. Quando os detalhes estão definidos, é mais fácil entender o programa. Mas também há um ponto fraco.
export interface ProjectParams {
id: string;
deployPath: Path | string;
configuration?: string;
buildRelevance?: BuildRelevance;
}
const distDir = new Directory(Path.fromRoot("dist"));
const buildRecordsDir = new Directory(Path.fromRoot("tmp/builds-manifest"));
export function createProject(params: ProjectParams): Project {
return new ProjectFactory(params).create();
}
class ProjectFactory {
private buildDir: Directory = distDir.getSubDir(this.params.id);
private deployDir: Directory = new Directory(
Path.from(this.params.deployPath)
);
constructor(private params: ProjectParams) {}
create(): Project {
const builder = this.createBuilder();
const buildPublisher = this.createPublisher();
return new Project(this.params.id, builder, buildPublisher);
}
private createBuilder(): NgBuilder {
return new NgBuilder({
baseHref: "/clientapp/",
outputPath: this.buildDir.path.toAbsolute(),
configuration: this.params.configuration,
});
}
private createPublisher(): BuildPublisher {
const buildHistory = this.getBuildsHistory();
return new BuildPublisher(this.buildDir, this.deployDir, buildHistory);
}
private getBuildsHistory(): BuildsHistory {
const buildRecordsFile = this.getBuildRecordsFile();
const buildRelevance = this.params.buildRelevance ?? BuildRelevance.Default;
return new BuildsHistory(buildRecordsFile, buildRelevance);
}
private getBuildRecordsFile(): BuildRecordsFile {
const buildRecordsPath = buildRecordsDir.path.join(
`${this.params.id}.json`
);
return new BuildRecordsFile(buildRecordsPath);
}
}
Todos os detalhes associados à estrutura complexa da configuração original entraram no processo de criação do objeto Projeto e suas dependências. Você tem que pagar por tudo. Mas às vezes esta é uma oferta lucrativa - livrar-se de peças menores em todo o módulo e concentrá-las dentro de uma fábrica.
Assim, OOP possibilita ocultar detalhes, deslocando-os no momento da criação do objeto. Do ponto de vista do design, essa é uma superpotência - a capacidade de se livrar de detalhes desnecessários. Isso faz sentido se a soma dos detalhes na interface do objeto for menor do que na estrutura que ele encapsula. E se você pode separar a criação do objeto e seu uso na maior parte do sistema.
SÓLIDO, abstração, encapsulamento ...
Existem toneladas de livros sobre OOP. Eles conduzem estudos aprofundados que refletem a experiência de escrever programas orientados a objetos. Mas minha visão de desenvolvimento foi virada de cabeça para baixo quando percebi que OOP simplifica o código principalmente por limitar os detalhes. E eu serei polar ... mas a menos que você se livre de detalhes com objetos, você não está usando OOP.
Você pode tentar cumprir o SOLID, mas não faz muito sentido se você não escondeu os detalhes menores. É possível fazer interfaces parecerem objetos no mundo real, mas isso não faz muito sentido se você não escondeu os pequenos detalhes. Você pode melhorar a semântica usando substantivos em seu código, mas ... essa é a ideia.
Acho que SOLID, padrões e outras diretrizes de escrita de objetos são excelentes diretrizes de refatoração. Depois de completar o quebra-cabeça, você pode ver a imagem inteira e destacar as partes mais simples. Em geral, essas são ferramentas e métricas importantes que requerem atenção, mas geralmente os desenvolvedores passam a aprendê-las e usá-las antes de converter o programa para a forma de objeto.
Quando você souber a verdade
OOP é uma ferramenta para resolver problemas complexos. Tarefas difíceis são ganhas dividindo-se em tarefas simples, limitando os detalhes. Uma forma de reduzir o número de peças é substituir os dados por um conjunto de operações.
Agora que você sabe a verdade, tente se livrar de coisas desnecessárias em seu projeto. Combine os objetos resultantes com SOLID. Em seguida, tente trazê-los para objetos do mundo real. Não o contrário. O principal está nos detalhes.
Recentemente escreveu uma extensão VSCode para refatoração da classe Extract . Acho que este é um bom exemplo de código orientado a objetos. O melhor que tenho. Eu ficaria feliz em receber comentários sobre a implementação ou sugestões para melhorar o código / funcionalidade. Eu quero emitir um PR em Abracadabra em um futuro próximo