En bref, ne concevez pas votre logiciel pour qu'il soit réutilisable, car aucun utilisateur final ne se soucie de savoir si vos fonctions peuvent être réutilisées. À la place, ingénieur pour la compréhensibilité de la conception - mon code est-il facile à comprendre pour quelqu'un d'autre ou mon futur moi oublieux à comprendre? - et flexibilité de conception- lorsque je dois inévitablement corriger des bogues, ajouter des fonctionnalités ou modifier d'autres fonctionnalités, dans quelle mesure mon code résistera-t-il aux modifications? La seule chose qui compte pour votre client est la rapidité avec laquelle vous pouvez répondre quand elle signale un bogue ou demande un changement. Le fait de poser ces questions sur votre conception a tendance à générer un code réutilisable, mais cette approche vous permet d'éviter les véritables problèmes que vous rencontrerez tout au long de la vie de ce code afin de pouvoir mieux servir l'utilisateur final plutôt que de rechercher des solutions fastidieuses et peu pratiques. "ingénierie" idéaux pour plaire à la barbe.
Pour quelque chose d'aussi simple que l'exemple que vous avez fourni, votre implémentation initiale est bonne à cause de sa petite taille, mais cette conception simple deviendra difficile à comprendre et fragile si vous essayez d'incorporer trop de flexibilité fonctionnelle (par opposition à une flexibilité de conception) dans une procédure. Vous trouverez ci-dessous une explication de l’approche que je privilégie pour la conception de systèmes complexes aux fins de la compréhensibilité et de la flexibilité, ce qui, j’espère, démontrera ce que j’entends par eux. Je n’utiliserais pas cette stratégie pour quelque chose qui pourrait être écrit en moins de 20 lignes en une seule procédure car quelque chose de si petit répond déjà à mes critères de compréhensibilité et de flexibilité.
Des objets, pas des procédures
Plutôt que d'utiliser des classes telles que des modules de la vieille école avec un ensemble de routines que vous appelez pour exécuter les tâches que votre logiciel doit faire, envisagez de modéliser le domaine en tant qu'objets qui interagissent et coopèrent pour accomplir la tâche à accomplir. Les méthodes d'un paradigme orienté objet ont été créées à l'origine pour être des signaux entre objets afin que Object1
chacun Object2
puisse faire son travail, quel qu'il soit, et éventuellement recevoir un signal de retour. En effet, le paradigme orienté objet concerne intrinsèquement la modélisation des objets de votre domaine et de leurs interactions plutôt qu'un moyen sophistiqué d’organiser les mêmes fonctions et procédures anciennes du paradigme Imperative. Dans le cas duvoid destroyBaghdad
Par exemple, au lieu d’essayer d’écrire une méthode générique sans contexte pour gérer la destruction de Bagdad ou de toute autre chose (qui pourrait devenir rapidement complexe, difficile à comprendre et cassante), tout ce qui peut être détruit devrait permettre de comprendre comment se détruire. Par exemple, vous avez une interface qui décrit le comportement de choses pouvant être détruites:
interface Destroyable {
void destroy();
}
Ensuite, vous avez une ville qui implémente cette interface:
class City implements Destroyable {
@Override
public void destroy() {
...code that destroys the city
}
}
Rien qui appelle à la destruction d'une instance de City
ne sera jamais soin comment cela se passe, donc il n'y a aucune raison pour que le code d'exister partout en dehors de City::destroy
, et en effet, la connaissance intime des rouages de l' City
extérieur de lui - même serait un couplage étroit qui réduit félicité puisque vous devez tenir compte de ces éléments extérieurs au cas où vous auriez besoin de modifier le comportement de City
. C'est le but véritable de l'encapsulation. Pensez-y comme si chaque objet avait sa propre API, ce qui devrait vous permettre de faire tout ce dont vous avez besoin pour pouvoir le laisser vous soucier de répondre à vos demandes.
Délégation, pas "contrôle"
Maintenant, que votre classe d'implémentation City
soit Baghdad
dépendante ou non du degré de générique du processus de destruction de la ville. Selon toute probabilité, un City
sera composé de plus petites pièces qui devront être détruites individuellement pour accomplir la destruction totale de la ville. Ainsi, dans ce cas, chacune de ces pièces serait également mise en œuvre Destroyable
et chacune d'elles aurait pour instruction City
de détruire de la même manière que quelqu'un de l'extérieur a demandé City
à se détruire.
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);
}
}
Si vous voulez devenir vraiment fou et mettre en œuvre l'idée d'un objet Bomb
déposé sur un emplacement et détruisant tout dans un rayon donné, cela pourrait ressembler à ceci:
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
représente un ensemble d'objets qui est calculé pour les Bomb
entrées depuis, car la Bomb
manière dont ce calcul est effectué ne se soucie pas tant qu'il peut fonctionner avec les objets. Ceci est d'ailleurs réutilisable, mais l'objectif principal est d'isoler le calcul des processus de suppression Bomb
et de destruction des objets afin que vous puissiez comprendre chaque élément et son agencement et modifier le comportement d'un élément individuel sans avoir à remodeler l'ensemble de l'algorithme. .
Des interactions, pas des algorithmes
Plutôt que d'essayer de deviner le bon nombre de paramètres pour un algorithme complexe, il est plus judicieux de modéliser le processus en tant qu'ensemble d'objets en interaction, chacun avec des rôles extrêmement étroits, car il vous permettra de modéliser la complexité de votre processus à travers les interactions entre ces objets bien définis, faciles à comprendre et presque immuables. Lorsque cela est fait correctement, cela rend même certaines des modifications les plus complexes aussi triviales que l'implémentation d'une interface ou deux et la modification des objets instanciés dans votre main()
méthode.
Je donnerais quelque chose à votre exemple initial, mais honnêtement, je ne peux pas comprendre ce que signifie "imprimer ... Day Light Savings". Ce que je peux dire à propos de cette catégorie de problème, c'est que chaque fois que vous effectuez un calcul, le résultat de celui-ci peut être formaté de différentes manières, la méthode que je préfère pour décomposer cela est la suivante:
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
}
}
Étant donné que votre exemple utilise des classes de la bibliothèque Java qui ne prennent pas en charge cette conception, vous pouvez simplement utiliser l'API ZonedDateTime
directement. L'idée ici est que chaque calcul est encapsulé dans son propre objet. Il ne fait aucune hypothèse sur le nombre de fois qu'il devrait être exécuté ou comment il devrait formater le résultat. Il s’agit exclusivement de réaliser la forme la plus simple du calcul. Cela rend le changement facile à comprendre et flexible. De même, le Result
est exclusivement concerné par l’encapsulation du résultat du calcul, et le FormattedResult
exclusivement sur l’interaction avec le Result
pour le formater selon les règles que nous définissons. De cette façon,nous pouvons trouver le nombre parfait d'arguments pour chacune de nos méthodes, car elles ont chacune une tâche bien définie . Il est également beaucoup plus simple de modifier l’avenir tant que les interfaces ne changent pas (ce qui est moins le cas si vous avez correctement minimisé les responsabilités de vos objets). Notremain()
méthode pourrait ressembler à ceci:
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()
);
});
}
}
En fait, la programmation orientée objet a été spécialement inventée pour résoudre le problème de complexité / flexibilité du paradigme de l’impératif, car il n’ya pas de bonne réponse (que tout le monde peut s’entendre ou arriver à un accord de façon indépendante) sur la manière optimale de fonctionner. spécifier les fonctions et les procédures impératives dans l'idiome.