Skip to main content
Minor syntactic correction.
Source Link
Vector Zita
  • 2.5k
  • 11
  • 21

In short, you don't need to change your entire model just to accommodate the fact that some of your classes share part of their behavior. The reason for this is that, in your case, the common code is most likely a coincidence and not an actual design detail decision. You need characters that move on a 2D-grid, each in its own way, based on the circumstances. Your other proposals, implying characters that can move in two different ways (at most), will also necessitate the actual support of two ways at most and, suddenly, you will find yourself making additional checks throughout the main game logic, to find out how your characters should move (i.e. which method to call, move() or alternativeMove()). The decision about how characters should move is best contained (encapsulated) within the character classes, potentially aided by supplying additional details (preferably through the constructor of each character). The only model that supports this design choice is your "Proposal 1" and you should keep this and try to solve the rest of your problems without sacrificing its high abstraction level.

In short, you don't need to change your entire model just to accommodate the fact that some of your classes share part of their behavior. The reason for this is that, in your case, the common code is most likely a coincidence and not an actual design detail decision. You need characters that move on a 2D-grid, each in its own way, based on the circumstances. Your other proposals, implying characters that can move in two different ways (at most), will also necessitate the actual support of two ways at most and, suddenly, you will find yourself making additional checks throughout the main game logic, to find out how your characters should move (i.e. which method to call, move() or alternativeMove(). The decision about how characters should move is best contained (encapsulated) within the character classes, potentially aided by supplying additional details (preferably through the constructor of each character). The only model that supports this design choice is your "Proposal 1" and you should keep this and try to solve the rest of your problems without sacrificing its high abstraction level.

In short, you don't need to change your entire model just to accommodate the fact that some of your classes share part of their behavior. The reason for this is that, in your case, the common code is most likely a coincidence and not an actual design detail decision. You need characters that move on a 2D-grid, each in its own way, based on the circumstances. Your other proposals, implying characters that can move in two different ways (at most), will also necessitate the actual support of two ways at most and, suddenly, you will find yourself making additional checks throughout the main game logic, to find out how your characters should move (i.e. which method to call, move() or alternativeMove()). The decision about how characters should move is best contained (encapsulated) within the character classes, potentially aided by supplying additional details (preferably through the constructor of each character). The only model that supports this design choice is your "Proposal 1" and you should keep this and try to solve the rest of your problems without sacrificing its high abstraction level.

Source Link
Vector Zita
  • 2.5k
  • 11
  • 21

The answer to your main question:

in general, when is it appropriate to introduce a new layer of abstraction at the cost of complicating the program architecture?

is relatively simple and straightforward:

When the benefit outweighs the cost

You need to be certain about the benefits and costs involved. An additional abstraction layer will not complicate a project with 3 classes too much, but it might be a deal-breaker in a project where dozens of classes would be affected. Refactoring logical abstractions into a model is very much nontrivial and needs to be carefully weighed against the benefits.

As far as the benefits are concerned, the two broad aspects I would be looking after are high expressive power, as well as high adaptability. In short, if it makes your code more expressive, that's a plus. If it makes it flexible to easily adapt to forthcoming unforeseen requirements, that's an even bigger plus!

Do not underestimate the expressive power, because that's where adaptability also hides. The better you understand and "mimic" your modeled domain, the better you can "foresee" potential future requirements.

Also

Depending on your perspective, you might be confusing the true meaning of abstraction. Abstractions are powerful, because they hide details. You state:

Proposal 1 is simple but has a lower level of abstraction. Proposal 3 is complex but has a higher level of abstraction

No, it's the other way around. Your "Proposal 1" has a higher level of abstraction compared to "Proposal 3". Take a step outside of where you stand. You know nothing about your design, and you present it to others.

From your "Proposal 1", I immediately know you have some kind of entity termed Enemy which you can show, update and move. Plus you have some specific types of Enemy.

From your "Proposal 3", I know, additionally, that there is a special type of Enemy, called however you like, e.g. MovementPlanEnemy. Some of your types implement this instead of the basic Enemy type, so now, I know more about your types. You are becoming more specific, you offer two types of movement, plain and alternative.

Think about where you are going to use these types. Within your game, you will, eventually, have to declare the general base types, so that you can compose your logic. Wherever you declare your types as MovementPlanEnemy, you are "leaking" more details than if you would declare them as Enemy. When I know more about your design, you go down the ladder of abstraction, towards "concretion". When you do that, you usually lose expressive power because you have less flexibility. You now offer more information and this complicates things more as you will have to support these specific provisions of yours in future versions of your code (or force everyone using them, including you, to change their code to adapt to your new decisions).

Also (part II)

For your "Proposal 1", you state:

Violates DRY since code isn't shared between Drunkard and Mummy.

You might be being somewhat too strict with your assessment there. Of course code will not be shared between Drunkard and Mummy. DRY (Do not Repeat Yourself) does not mean do not write identical lines of code. It means, strive to reuse your concepts. Type 1 Movement is a concept and you can avoid repeating yourself regarding that quite easily by, for example, using static helper classes, or, as others have stated, through composition and interfaces (strategy pattern and whatnot). But tomorrow, you may be required to add another character that moves like two already existing characters. Would you change the abstraction again, in order to separate movement types into more groups?

In short, you don't need to change your entire model just to accommodate the fact that some of your classes share part of their behavior. The reason for this is that, in your case, the common code is most likely a coincidence and not an actual design detail decision. You need characters that move on a 2D-grid, each in its own way, based on the circumstances. Your other proposals, implying characters that can move in two different ways (at most), will also necessitate the actual support of two ways at most and, suddenly, you will find yourself making additional checks throughout the main game logic, to find out how your characters should move (i.e. which method to call, move() or alternativeMove(). The decision about how characters should move is best contained (encapsulated) within the character classes, potentially aided by supplying additional details (preferably through the constructor of each character). The only model that supports this design choice is your "Proposal 1" and you should keep this and try to solve the rest of your problems without sacrificing its high abstraction level.