2010-04-11 10 views
6

¿Es mala política tener muchas clases de "trabajo" (como clases de estrategia) que solo hacen una cosa?Patrón de estrategia y explosión de clases de "acción"

Supongamos que quiero hacer una clase Monster. En lugar de simplemente definir todo lo que quiero sobre el monstruo en una clase, trataré de identificar cuáles son sus características principales, para poder definirlas en las interfaces. Eso permitirá:

  1. Selle la clase si quiero. Más tarde, otros usuarios pueden simplemente crear una nueva clase y aún tener polimorfismo por medio de las interfaces que he definido. No tengo que preocuparme de cómo la gente (o yo mismo) querría cambiar/agregar características a la clase base en el futuro. Todas las clases heredan de Object e implementan herencia a través de interfaces, no de clases madre.
  2. Reutiliza las estrategias que estoy usando con este monstruo para otros miembros de mi mundo del juego.

Con: Este modelo es rígido. A veces nos gustaría definir algo que no se logra fácilmente simplemente tratando de armar estos "bloques de construcción".

public class AlienMonster : IWalk, IRun, ISwim, IGrowl { 
    IWalkStrategy _walkStrategy; 
    IRunStrategy _runStrategy; 
    ISwimStrategy _swimStrategy; 
    IGrowlStrategy _growlStrategy; 

    public Monster() { 
     _walkStrategy = new FourFootWalkStrategy(); 
     ...etc 
    } 

    public void Walk() { _walkStrategy.Walk(); } 
    ...etc 
} 

Mi idea sería la siguiente a hacer una serie de estrategias diferentes que podrían ser utilizadas por diferentes monstruos. Por otro lado, algunos de ellos también podrían usarse con fines totalmente diferentes (es decir, podría tener un tanque que también "nada"). El único problema que veo con este enfoque es que podría conducir a una explosión de clases puras de "método", es decir, clases de estrategia que tienen como único propósito hacer esta o aquella otra acción. Por otro lado, este tipo de "modularidad" permitiría una alta reutilización de estratagemas, a veces incluso en contextos totalmente diferentes.

¿Cuál es su opinión al respecto? ¿Es este un razonamiento válido? ¿Esto es sobre-ingeniería?

Además, en el supuesto que nos gustaría hacer los ajustes apropiados al ejemplo que di anteriormente, sería mejor para definir iwalk como:

interface IWalk { 
    void Walk(); 
} 

o

interface IWalk { 
    IWalkStrategy WalkStrategy { get; set; } //or something that ressembles this 
} 

es que haciendo esto no necesitaría definir los métodos en Monster, solo tendría getters públicos para IWalkStrategy (¡esto parece ir en contra de la idea de que debe encapsular todo lo que pueda!) ¿Por qué?

Gracias

Respuesta

2

Walk, Run y ​​Swim parecen ser implementaciones en lugar de interfaces. Puede tener una interfaz ILocomotion y permitir que su clase acepte una lista de implementaciones de ILocomotion.

Growl podría ser una implementación de algo así como una interfaz IAbility. Y un monstruo particular podría tener una colección de implementaciones de IAbility.

Luego tiene un par de interfaces que es la lógica de qué capacidad o locomoción usar: IMove, IAct por ejemplo.

public class AlienMonster : IMove, IAct 
{ 
    private List<IAbility> _abilities; 
    private List<ILocomotion> _locomotions; 

    public AlienMonster() 
    { 
    _abilities = new List<IAbility>() {new Growl()}; 
    _locomotion = new List<ILocomotion>() {new Walk(), new Run(), new Swim()} 
    } 

    public void Move() 
    { 
    // implementation for the IMove interface 
    } 

    public void Act() 
    { 
    // implementation for the IAct interface 
    } 

} 

Al componer su clase de esta forma evitará algo de la rigidez.

EDIT: añadió la materia sobre IMOVE y Iact

EDIT: después de algunos comentarios

Mediante la adición de iwalk, IRun y ​​iswim a un monstruo que está diciendo que cualquier cosa puede ver el objeto debe ser capaz de llame a cualquiera de los métodos implementados en cualquiera de esas interfaces y tenga sentido. Además, para que algo decida cuál de las tres interfaces debería usar, debe pasar todo el objeto. Una gran ventaja de usar una interfaz es que puede hacer referencia a ella mediante esa interfaz.

void SomeFunction(IWalk alienMonster) {...} 

La función anterior se llevará a nada que implementa iwalk pero si hay variaciones de algunaFuncion en Irun, iswim, y así sucesivamente usted tiene que escribir una función completamente nueva para cada uno de esos o pasar el objeto AlienMonster en su totalidad . Si pasa el objeto, entonces esa función puede llamar a todas y cada una de las interfaces. También significa que esa función tiene que consultar el objeto AlienMonster para ver cuáles son sus capacidades y luego decidir cuál usar. Todo esto hace que muchas funciones externas se mantengan internas a la clase. Debido a que estás externalizando todo eso y no hay nada en común entre IWalk, IRun, ISwim, algunas funciones podrían llamar inocentemente a las tres interfaces y tu monstruo podría estar corriendo, caminando y nadando al mismo tiempo.Además, dado que desearás poder llamar a IWalk, IRun, ISwim en algunas clases, todas las clases básicamente tendrán que implementar las tres interfaces y terminarás haciendo una estrategia como CannotSwim para satisfacer el requisito de interfaz para ISwim cuando sea un monstruo. no puedo nadar De lo contrario, podrías intentar llamar a una interfaz que no está implementada en un monstruo. Al final, en realidad estás empeorando el código de las interfaces adicionales, IMO.

+1

Otra cosa buena de hacerlo de esta manera es que, cuando quiera pasar de A a B, puede iterar sobre su lista de interfaces de locomoción y preguntarle a cada uno el costo en lugar de hardcoding "get walk cost; get run cost" ; obtener el costo de nadar, obtener el costo de volar, obtener el costo de teletransportación ... "y incurrir en la deuda técnica de tener que mantener esa lista actualizada a medida que se implementan nuevos tipos de locomoción. –

+0

¿Podría explicar por qué Walk, Run y ​​Swim le parecen más como implementaciones que como interfaces? En general, ¿no debería una interfaz modelar algo que una determinada clase "puede hacer"? ISwim significaría que mi monstruo (o tanque, o cualquier clase que trate de implementar) puede nadar. Luego podría implementar varias clases de estrategia diferentes (por ejemplo) como SwimInWater SwimInAcid, etc. ¿Qué es lo que me falta aquí? –

+1

Permítanme reformular las "Interfaces que no describen qué puede hacer algo per se". Es una buena idea usar interfaces de una manera lógica y consistente. Usar el ILocomotion para Growl sería una mala elección. Pero caminar, nadar, correr, volar, teletransportarse, etc., son solo formas de movimiento. Al abstraerlos a una interfaz de movimiento, puede simplificar mucho su código. Puede tener 100 implementaciones de ILocomotion y estar mejor que 20 implementaciones IWalk, 20 IRun, 20 ISwim, 20 ISkip, 2 ITeleport, 18 IJump. Si algo no encaja en una interfaz de ILocomotion, crea una nueva interfaz. – Felan

2

En lenguajes que soportan herencia múltiple, que podrían de hecho crear su Monster heredando de clases que representan las cosas que puede hacer. En estas clases, debe escribir el código para que no se copie ningún código entre las clases implementadas.

En C#, siendo un lenguaje de herencia único, no veo otro camino que crear interfaces para ellos. ¿Hay mucho código compartido entre las clases, entonces su enfoque IWalkStrategy funcionaría bien para reducir el código redundante. En el caso de su ejemplo, también puede combinar varias acciones relacionadas (como caminar, nadar y correr) en una sola clase.

La reutilización y la modularidad son cosas buenas, y tener muchas interfaces con solo unos pocos métodos no es en mi opinión un problema real en este caso. Especialmente porque las acciones que define con las interfaces son en realidad cosas diferentes que pueden usarse de manera diferente.Por ejemplo, un método puede querer un objeto que pueda saltar, por lo que debe implementar esa interfaz. De esta forma, fuerza esta restricción por tipo en tiempo de compilación en lugar de lanzar excepciones en el tiempo de ejecución cuando no cumple con las expectativas del método.

Así que en resumen: Lo haría de la misma manera que usted propuso, utilizando un enfoque adicional I...Strategy cuando reduce el código copiado entre las clases.

1

"Encuentra lo que varía y encapsulalo".

Si varía la forma en que un monstruo camina, entonces encapsule esa variación detrás de una abstracción. Si necesita cambiar la forma en que camina un monstruo, es probable que tenga un patrón de estado en su problema. Si necesita asegurarse de que las estrategias Walk and Growl concuerden, entonces probablemente tenga un patrón abstracto de fábrica.

En general: no, definitivamente no es una sobreingeniería para encapsular varios conceptos en sus propias clases. Tampoco hay nada de malo en hacer clases concretas sealed o final, tampoco. Obliga a las personas a romper la encapsulación de forma consciente antes de heredar de algo que probablemente debería no heredarse.

2

Desde el punto de vista del mantenimiento, la sobre-abstracción puede ser tan mala como el código rígido y monolítico. La mayoría de las abstracciones añaden complicaciones y, por lo tanto, debes decidir si la complejidad añadida te compra algo valioso. Tener muchas clases de trabajo muy pequeñas puede ser un signo de este tipo de abstracción excesiva.

Con las herramientas de refactorización modernas, generalmente no es demasiado difícil crear abstracción adicional donde y cuando la necesite, en lugar de diseñar completamente un gran diseño por adelantado. En los proyectos en los que comencé de manera muy abstracta, descubrí que, a medida que desarrollaba la implementación concreta, descubría casos que no había considerado y, a menudo, me encontraba tratando de contorsionar la implementación para que coincida con el patrón, en lugar de ir volver y reconsiderar la abstracción. Cuando empiezo de manera más concreta, identifico (más de) los casos de esquina por adelantado y puedo determinar mejor dónde tiene sentido abstraer.

Cuestiones relacionadas