¿Cómo cambia el pensamiento sobre patrones de diseño y prácticas de OOP en lenguajes dinámicos y de tipo débil?

11

Ya hay una pregunta bastante útil en este sentido (" ¿Patrones de diseño que no son OOP? "), Pero tengo más curiosidad acerca de un punto de vista de transición para alguien que acaba de comenzar con lenguajes dinámicos y de tipo débil.

Es decir: digamos que he estado programando en C ++, C # o Java durante muchos años, y absorbí mucha sabiduría en la línea de los patrones de diseño de GoF, los patrones de arquitectura de aplicaciones empresariales de Fowler , los principios SOLID , etc. Ahora ' Me meto en Ruby, Python, JavaScript, etc., y me pregunto cómo se aplica mi conocimiento. Presumiblemente podría hacer traducciones directas en muchos casos, pero casi con certeza eso no sería aprovechar al máximo mi nueva configuración. El tipeo de patos solo da vuelta a mi pensamiento basado en la interfaz.

¿Qué queda igual? ¿Que cambios? ¿Existen principios rectores como SOLID, o patrones canónicos (quizás completamente nuevos) que un principiante de lenguaje dinámico debería conocer?

Domenic
fuente

Respuestas:

7

¿Qué queda igual? ¿Que cambios?

Los patrones son los mismos. Las técnicas del lenguaje cambian.

¿Existen principios rectores como SOLID,

Si. De hecho, siguen siendo los principios rectores. Nada cambia.

o patrones canónicos (quizás completamente nuevos) que un principiante de lenguaje dinámico debería saber?

Algunas cosas son únicas. Principalmente el impacto es que las técnicas de implementación cambian.

Un patrón es, bueno, un patrón . No es una ley No es una subrutina. No es una macro Es solo una buena idea que se repite porque es una buena idea.

Las buenas ideas no pasan de moda ni cambian dramáticamente.

Otras notas. Python no está "débilmente escrito". Está más fuertemente tipado que Java o C ++ porque no hay operación de conversión. [Sí, hay una manera de eludir la clase asociada con un objeto, pero no es el tipo de cosa que se hace excepto para demostrar un punto exigente y legalista.]

También. La mayoría de los patrones de diseño se basan en diferentes formas de explotar el polimorfismo.

Mire el estado o comando o recuerdo como ejemplos. Tienen jerarquías de clase para crear estados polimórficos, comandos o recuerdos de cambios de estado. Nada cambia significativamente cuando haces esto en Python. Los cambios menores incluyen la relajación de la jerarquía de clases precisa porque el polimorfismo en Python depende de métodos comunes, no de antepasados ​​comunes.

Además, algunos patrones son simplemente un intento de lograr una unión tardía. La mayoría de los patrones relacionados con la fábrica son un intento de permitir un cambio fácil a una jerarquía de clases sin recompilar todos los módulos de C ++ en la aplicación. Esta no es una optimización tan interesante en un lenguaje dinámico. Sin embargo, una Fábrica como una forma de ocultar detalles de implementación todavía tiene un gran valor.

Algunos patrones son un intento de conducir el compilador y el enlazador. Singleton , por ejemplo, existe para crear globales confusos pero al menos encapsularlos. Las clases de Python Singleton no son una perspectiva agradable. Pero los módulos de Python ya son singletons, por lo que muchos de nosotros solo usamos un módulo y evitamos tratar de meternos con una clase Singleton .

S.Lott
fuente
No diría que "nada cambia" con SOLID. Dependiendo del lenguaje y su modelo de objeto, el Principio Abierto-Cerrado y el Principio de Sustitución de Liskov pueden no tener sentido. (JavaScript y Go me vienen a la mente.)
Mason Wheeler
@Mason Wheeler. Open-Closed es un lenguaje independiente en mi experiencia. Tendrá que proporcionar algunos ejemplos más concretos de cómo el diseño abierto-cerrado "no tiene sentido" con JavaScript o Go. La sustitución de Liskov, tal vez, no se aplica a JavaScript, pero el patrón esencial, el polimorfismo, todavía parece aplicarse.
S.Lott
@ S.Lott: Buenas actualizaciones en la edición; fueron mucho más interesantes que la respuesta original: P. Gracias por corregir mi error de Python. En general, los ejemplos de patrones específicos y cómo se relacionan con lenguajes dinámicos, polimorfismo, enlace tardío, etc. son perfectos.
Domenic
@ S.Lott: Porque Open / Closed es sobre herencia, que esos idiomas no tienen. (Además, la idea de que un objeto esté "cerrado por modificación" no se sentiría bien con muchos codificadores de Ruby ...)
Mason Wheeler
@Mason Wheeler: Gracias por la aclaración sobre Abierto / Cerrado. Creo que la excepción de JavaScript es importante, pero como la pregunta es muy abierta (enumerando JavaScript, Python y Ruby, así como un lenguaje llamado ETC), no estoy seguro de cómo abordar el caso especial.
S.Lott
8

Peter Norvig asumió esta misma pregunta en 1998, lea http://norvig.com/design-patterns/ppframe.htm para obtener un conjunto de cosas detalladas que notó, y http://c2.com/cgi/wiki?AreDesignPatternsMissingLanguageFeatures para discusión adicional sobre el punto.

La versión corta es que cuando su idioma tiene más funciones, los patrones de diseño repetitivos tienden a ser más simples, a menudo hasta el punto de ser invisibles. Descubrió que esto era cierto para la mayoría de los patrones de diseño que el GoF había identificado.

btilly
fuente
8

La programación en un lenguaje dinámico orientado a objetos utiliza muchos de los mismos patrones y principios, pero existen ciertos ajustes y diferencias debido al entorno:

Reemplace las interfaces con Duck Typing : donde Gang of Four le indicaría que use una clase base abstracta con funciones virtuales puras, y usaría una interfaz en Java, en un lenguaje dinámico, solo necesita una comprensión. Dado que puede usar cualquier objeto en cualquier lugar, y funcionará bien si implementa los métodos que realmente se llaman, no necesita definir una interfaz formal. Puede valer la pena documentar uno, para que quede claro lo que realmente se requiere.

Las funciones también son objetos : hay muchos patrones que tratan de separar la decisión de la acción; Comando, estrategia, cadena de responsabilidad, etc. En un lenguaje con funciones de primera clase, a menudo es razonable simplemente pasar una función en lugar de crear objetos con .doIt()métodos. Estos patrones se transforman en "usar una función de orden superior".

VENDIDO : el principio de segregación de interfaz tiene el mayor éxito aquí, debido a que no hay interfaces. Aún debe considerar el principio, pero no puede volver a clasificarlo en su código. Solo la vigilancia personal lo protegerá aquí. En el lado positivo, el dolor causado por violar este principio se reduce mucho en entornos dinámicos comunes.

"... en mi propio Particular ... ¡Idioma!" - Cada idioma tiene buenas prácticas y malas prácticas, y tendrá que aprenderlas y seguirlas, si desea el mejor código en esos idiomas. Un patrón iterador perfectamente escrito puede reírse en un lenguaje con comprensiones de listas incorporadas, por ejemplo.

Sean McMillan
fuente
3

En mi experiencia, algunos patrones siguen siendo útiles en Python, e incluso más fáciles de configurar que en lenguajes más estáticos. Algunos patrones OTOH simplemente no son necesarios, o incluso mal vistos, como el patrón Singleton. Utilice una variable o función de nivel de módulo en su lugar. O usa el patrón Borg.

En lugar de configurar un patrón de creación, a menudo es suficiente pasar un invocable que crea objetos. Esa podría ser una función, un objeto con un __call__método o incluso una clase, ya que no hay new()en Python, solo una invocación de la clase en sí:

def make_da_thing(maker, other, stuff):
    da_thing = maker(other + 1, stuff + 2)
    # ... do sth
    return da_thing

def maker_func(x, y):
     return x * y

class MakerClass(object):
    def __init__(self, x, y):
        self.x = x
        self.y = y
...
a = make_da_thing(maker_func, 5, 8)
b = make_da_thing(MakerClass, 6, 7)

El patrón de estado y estrategia comparte una estructura muy similar en lenguajes como C ++ y Java. Menos en Python. El patrón de estrategia permanece más o menos igual, pero el patrón de estado se vuelve innecesario. El patrón de estado en lenguajes estáticos simula el cambio de clase en tiempo de ejecución. En Python, puede hacer exactamente eso: cambiar la clase de un objeto en tiempo de ejecución. Mientras lo haga de forma controlada y encapsulada, debería estar bien:

class On(object):
    is_on = True
    def switch(self):
        self.__class__ = Off

class Off(object):
    is_on = False
    def switch(self):
        self.__class__ = On
...

my_switch = On()
assert my_switch.is_on
my_switch.switch()
assert not my_switch.is_on

Los patrones que dependen del Despacho de tipo estático no funcionarán, o funcionarán de manera muy diferente. No tiene que escribir tanto código de placa de caldera, por ejemplo, Patrón de visitante: en Java y C ++ debe escribir un método de aceptación en cada clase visitable, mientras que en Python puede heredar esa funcionalidad a través de una clase mixin, como Visitable:

class Visitable(object):
    def accept(self, visitor):
        visit = getattr(visitor, 'visit' + self.__class__.__name__)
        return visit(self)
...

class On(Visitable):
    ''' exactly like above '''

class Off(Visitable):
    ''' exactly like above '''

class SwitchStatePrinter(object): # Visitor
    def visitOn(self, switch):
         print 'the switch is on'
    def visitOff(self, switch):
         print 'the switch is off'

class SwitchAllOff(object): # Visitor
    def visitOn(self, switch):
         switch.switch()
    def visitOff(self, switch):
         pass
...
print_state = SwitchStatePrinter()
turn_em_off = SwitchAllOff()
for each in my_switches:
    each.accept(print_state)
    each.accept(turn_em_off)

Muchas situaciones que requieren la aplicación de un Patrón en un lenguaje estático no lo hacen tanto en Python. Muchas cosas se pueden resolver con otras técnicas, como funciones de orden superior (decoradores, fábricas de funciones) o metaclases.

Pillmuncher
fuente
Ahora me doy cuenta de que su respuesta casi cubre la pregunta que acabo de hacer: ¿Es buena idea sobrescribir __class__para implementar una fábrica en Python ?
rds