📜 ⬆️ ⬇️

9 alternatives to a bad team (design pattern)

image

What is it and why?


When designing, a developer may encounter a problem: creatures and objects may have different abilities in different combinations. Frogs jump and swim, ducks swim and fly, but not with a weight, and frogs can fly with a branch and ducks. Therefore, it is convenient to move from inheritance to composition and add capabilities dynamically. The need to animate the flying frogs led to an unjustified abandonment of the ability methods and bringing their code to the teams in one of the implementations. Here she is:

class CastSpellCommand extends Command { constructor (source, target, spell) { this.source = source; this.target = target; this.spell = spell; } execute () { const spellAbility = this.source.getAbility(SpellCastAbility); // может быть много совершенно разных if (!cond) return error if (spellAbility == null) { throw new Error('NoSpellCastAbility'); } this.addChildren(new PayManaCommand(this.source, this.spell.manaCost)); this.addChildren(this.spell.getCommands(this.source, this.target)); // из другой команды: healthAbility.health = Math.max( 0, resultHealth ); } } // отрисовка: async onMeleeHit (meleeHitCommand) { await view.drawMeleeHit( meleeHitCommand.source, meleeHitCommand.target ); } async onDealDamage (dealDamageCommand) { await view.showDamageNumbers( dealDamageCommand.target, dealDamageCommand.damage ); } 

What can be done?


Consider several approaches of different nature:

Observer


 class Executor extends Observer {/* ... */} class Animator extends Observer {/* ... */} 

The classic, well-known solution for programmers. You only need to change it as little as possible to check the values ​​returned by observers:

 this.listeners.reduce((result, listener) => result && listener(action), true) 

Disadvantage: Observers must subscribe to events in the correct order.

If you do error handling, the animator will also be able to show animations of failed actions. You can pass the previous value to the observers, conceptually the solution remains the same. Whether observer methods or callback functions are called, whether a normal cycle is used instead of convolution - the details are not so significant.

Leave as is


And in fact. The current approach has both disadvantages and advantages:

  1. Verifying the ability to execute a command requires the execution of a command
  2. Arguments in varying order, conditions, method prefixes are hard-wired.
  3. Cyclic dependencies (command <spell <command)
  4. Additional entities for each action (method replaced by method, class and its constructor)
  5. Excessive knowledge and actions of a separate team: from game mechanics to timing errors and direct manipulation of other people's properties
  6. The interface is misleading (execute not only causes, but also adds commands via addChildren; which obviously does the opposite)
  7. Dubious need and implementation of recursive commands as such
  8. The dispatcher class, if any, does not perform its functions.
  9. [+] Allegedly, the only way to animate in practice, if the animation needs complete data (indicated as the main reason)
  10. [+] Probably other reasons.

Some of the shortcomings can be dealt with separately, but the rest require more fundamental changes.

ad hoc



Turn


To show the animation of the action after the execution of the action, it is enough to add them to the queue and run something like in solution 1.

 [ [ walkRequirements, walkAction, walkAnimation ], [ castRequirements, castAction, castAnimation ], // ... ] 

It does not matter which entities lie in the array: functions booted with the necessary parameters, instances of user-defined classes or ordinary objects.
The value of such a decision is simplicity and transparency; it is easy to make a sliding window for viewing the last N commands.

Good for prototyping and debugging.

Doubler class


Making an animation class for ability.

 class MovementAbility { walk (...args) { // action } } class AnimatedMovementAbility { walk (...args) { // animation } } 

If you cannot make changes to the calling class, inherit from it or decorate the desired method so that it invokes an animation. Or we transfer animation instead of ability, they have the same interface.

Well suited when you actually need the same set of methods, you can automatically check and test them.

Combinations of methods


 const AnimatedMovementAbility = combinedClass(MovementAbility, { ['*:before'] (method, ...args) { // call requirements }, ['*:after'] (method, ...args) { // call animations } }) 

It would be an interesting opportunity with native language support.
It is good to use if this option turns out more productive, although a proxy is actually needed.

Proxy


Wrapping abilities in proxies, catching methods in the getter.

 new Proxy(new MovementAbility, {/* handler */}) 

Disadvantage: many times slower than normal calls, which is not so significant for animation. On a server processing millions of objects, a slowdown would be noticeable, but no animation is needed on the server.

Promise


You can construct chains from Promise, but there is another option (ES2018):

 for await (const action of actionDispatcher.getActions()) { // } 

getActions returns an asynchronous iterator over an action. The next iterator method returns the Deferred Promise of the next action. After processing the events from the user and the server, call resolve (), create a new promise.

Better team


Create objects like this:

 {actor, ability, method, options} 

The code comes down to checking and calling the ability method with parameters. The easiest and most productive option.

Note


Source: https://habr.com/ru/post/439506/