Em resumo, não projete seu software para reutilização, porque nenhum usuário final se importa se suas funções podem ser reutilizadas. Em vez disso, engenheiro para a compreensão do design - é fácil meu código para outra pessoa ou meu futuro esquecido? - e flexibilidade de design- quando inevitavelmente precisar corrigir bugs, adicionar recursos ou modificar funcionalidades, quanto meu código resistirá às alterações? A única coisa com a qual seu cliente se importa é a rapidez com que você pode responder quando ele relata um erro ou pede uma alteração. Fazer essas perguntas sobre o seu design, incidentalmente, tende a resultar em um código reutilizável, mas essa abordagem mantém você focado em evitar os problemas reais que você enfrentará ao longo da vida útil desse código, para que você possa servir melhor o usuário final em vez de buscar noções elevadas e impraticáveis. ideais de "engenharia" para agradar as barbas do pescoço.
Para algo tão simples quanto o exemplo que você forneceu, sua implementação inicial é boa por causa de quão pequena é, mas esse design simples se tornará difícil de entender e quebradiço se você tentar colocar muita flexibilidade funcional (em oposição à flexibilidade de design) em um procedimento. Abaixo está minha explicação de minha abordagem preferida para projetar sistemas complexos para compreensibilidade e flexibilidade, que espero demonstrem o que quero dizer com eles. Eu não empregaria essa estratégia para algo que pudesse ser escrito em menos de 20 linhas em um único procedimento, porque algo tão pequeno já atende aos meus critérios de compreensibilidade e flexibilidade.
Objetos, não procedimentos
Em vez de usar classes como módulos da velha escola com várias rotinas que você chama para executar as coisas que seu software deve fazer, considere modelar o domínio como objetos que interagem e cooperam para realizar a tarefa em questão. Os métodos em um paradigma orientado a objetos foram originalmente criados para serem sinais entre objetos, de modo que eles Object1
pudessem dizer Object2
o que quer que fosse, e receber um sinal de retorno. Isso ocorre porque o paradigma Orientado a Objeto é inerentemente sobre modelagem dos objetos de domínio e suas interações, em vez de uma maneira sofisticada de organizar as mesmas funções e procedimentos antigos do paradigma Imperativo. No caso dovoid destroyBaghdad
Por exemplo, em vez de tentar escrever um método genérico sem contexto para lidar com a destruição de Bagdá ou qualquer outra coisa (que pode se tornar rapidamente complexa, difícil de entender e quebradiça), tudo que puder ser destruído deve ser responsável por entender como destruir a si mesmo. Por exemplo, você tem uma interface que descreve o comportamento de coisas que podem ser destruídas:
interface Destroyable {
void destroy();
}
Então você tem uma cidade que implementa essa interface:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Nada que exija a destruição de uma instância de City
alguma vez se importará com isso, portanto, não há razão para que esse código exista em qualquer lugar fora de City::destroy
, e de fato, um conhecimento íntimo do funcionamento interno de City
fora de si seria um acoplamento rígido que reduz felxibility, já que você precisa considerar esses elementos externos, caso precise modificar o comportamento de City
. Esse é o verdadeiro objetivo por trás do encapsulamento. Pense nisso como se cada objeto tivesse sua própria API, o que lhe permitirá fazer o que for necessário para que você possa se preocupar em atender às suas solicitações.
Delegação, não "Controle"
Agora, se sua classe de implementação é City
ouBaghdad
depende de quão genérico é o processo de destruição da cidade. Com toda a probabilidade, a City
será composta de peças menores que precisarão ser destruídas individualmente para realizar a destruição total da cidade; portanto, nesse caso, cada uma dessas peças também será implementada Destroyable
e elas serão instruídas pelo City
destruidor. da mesma maneira que alguém de fora pediu City
que se destruísse.
interface Part extends Destroyable {
...part-specific methods
}
class Building implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class Street implements Part {
...part-specific methods
@Override
public void destroy() {
...code to destroy a building
}
}
class City implements Destroyable {
public List<Part> parts() {...}
@Override
public void destroy() {
parts().forEach(Destroyable::destroy);
}
}
Se você quiser ficar realmente doido e implementar a ideia de uma Bomb
que é descartada em um local e destrói tudo dentro de um determinado raio, pode ser algo como isto:
class Bomb {
private final Integer radius;
public Bomb(final Integer radius) {
this.radius = radius;
}
public void drop(final Grid grid, final Coordinate target) {
new ObjectsByRadius(
grid,
target,
this.radius
).forEach(Destroyable::destroy);
}
}
ObjectsByRadius
representa um conjunto de objetos que é calculado para o Bomb
partir das entradas, porque Bomb
não se importa como esse cálculo é feito desde que possa trabalhar com os objetos. Isso é reutilizável incidentalmente, mas o objetivo principal é isolar o cálculo dos processos de descartar Bomb
e destruir os objetos, para que você possa compreender cada peça e como eles se encaixam e mudar o comportamento de uma peça individual sem precisar remodelar o algoritmo inteiro. .
Interações, não algoritmos
Em vez de tentar adivinhar o número certo de parâmetros para um algoritmo complexo, faz mais sentido modelar o processo como um conjunto de objetos em interação, cada um com funções extremamente estreitas, pois permitirá modelar a complexidade do seu processo através das interações entre esses objetos bem definidos, fáceis de compreender e quase imutáveis. Quando feito corretamente, isso faz com que algumas das modificações mais complexas sejam tão triviais quanto implementar uma interface ou duas e refazer quais objetos são instanciados no seu main()
método.
Eu daria algo para o seu exemplo original, mas sinceramente não consigo entender o que significa "imprimir ... o horário de verão". O que posso dizer sobre essa categoria de problema é que, sempre que você estiver executando um cálculo, cujo resultado pode ser formatado de várias maneiras, minha maneira preferida de decompor isso é a seguinte:
interface Result {
String print();
}
class Caclulation {
private final Parameter paramater1;
private final Parameter parameter2;
public Calculation(final Parameter parameter1, final Parameter parameter2) {
this.parameter1 = parameter1;
this.parameter2 = parameter2;
}
public Result calculate() {
...calculate the result
}
}
class FormattedResult {
private final Result result;
public FormattedResult(final Result result) {
this.result = result;
}
@Override
public String print() {
...interact with this.result to format it and return the formatted String
}
}
Como seu exemplo usa classes da biblioteca Java que não suportam esse design, você pode simplesmente usar a API ZonedDateTime
diretamente. A idéia aqui é que cada cálculo seja encapsulado dentro de seu próprio objeto. Não faz suposições sobre quantas vezes deve ser executado ou como deve formatar o resultado. Trata-se exclusivamente de executar a forma mais simples do cálculo. Isso facilita o entendimento e é flexível para alterar. Da mesma forma, o Result
item se preocupa exclusivamente em encapsular o resultado do cálculo, e o FormattedResult
item se preocupa exclusivamente em interagir com o Result
para formatá-lo de acordo com as regras que definimos. Nesse caminho,podemos encontrar o número perfeito de argumentos para cada um dos nossos métodos, pois cada um deles possui uma tarefa bem definida . Também é muito mais simples modificar o avanço, desde que as interfaces não sejam alteradas (o que provavelmente não ocorrerão se você tiver minimizado adequadamente as responsabilidades de seus objetos). Nossomain()
método pode ficar assim:
class App {
public static void main(String[] args) {
final List<Set<Paramater>> parameters = ...instantiated from args
parameters.forEach(set -> {
System.out.println(
new FormattedResult(
new Calculation(
set.get(0),
set.get(1)
).calculate()
).print()
);
});
}
}
De fato, a Programação Orientada a Objetos foi inventada especificamente como uma solução para o problema de complexidade / flexibilidade do paradigma Imperativo, porque simplesmente não há uma boa resposta (com a qual todos possam concordar ou chegar de forma independente) de como otimizar. especifique funções e procedimentos imperativos dentro do idioma.