style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">

OOP, Refactoring, y Android

Este articulo fue escrito con el fin de explicar la oop y varios conceptos relacionados con ella, como el polimorfismo, y la la asignacion de responsabilidades aplicando alta cohesion y bajo acoplamiento y el patron strategy. Para hacer este articulo me baso en el codigo que es explicado en otros dos articulos, los cuales les recomiendo que lean, para que puedan entender a la perfeccion lo que voy a explicar, las ligas son las siguientes:

http://blog.vidasconcurrentes.com/android/dibujando-figuras-en-un-canvas...

http://blog.vidasconcurrentes.com/android/detectando-drag-drop-en-un-can...

El codigo es hecho para la plataforma Android, pero creo que a la mayoria le ayudara, ya que esta bien explicado.

Primero se analizara el codigo(recuerden que este codigo fue para enseñar algo en particular, asi que me imagino, omitieron muchas cosas) y se mencionaran los errores, posteriormente en el diseño se implementaran algunas buenas practicas, tratando de explicar el porque, entonces empezamos:

public class DragAndDropView extends SurfaceView implements SurfaceHolder.Callback {

        private DragAndDropThread thread;
        private ArrayList<Figura> figuras;
        private int figuraActiva;
       
        public DragAndDropView(Context context) {
                super(context);
                getHolder().addCallback(this);
        }

        public void surfaceChanged(SurfaceHolder arg0, int arg1, int arg2, int arg3) {}

        public void surfaceCreated(SurfaceHolder arg0) {
                int id = 0;
                figuras = new ArrayList<Figura>();
                figuras.add(new Rectangulo(id++,200,500,200,200));
                figuras.add(new Circulo(id++,200,200,100));

                figuraActiva = -1;
               
                thread = new DragAndDropThread(getHolder(), this);
                thread.setRunning(true);
                thread.start();
        }

        public void surfaceDestroyed(SurfaceHolder arg0) {
                boolean retry = true;
                thread.setRunning(false);
                while (retry) {
                        try {
                                thread.join();
                                retry = false;
                        } catch (InterruptedException e) {
                               
                        }
                }
        }
       
        @Override
        public void onDraw(Canvas canvas) {
                Paint p = new Paint();
                p.setAntiAlias(true);
               
                canvas.drawColor(Color.WHITE);
               
                Circulo c = (Circulo) figuras.get(1);
                p.setColor(Color.BLACK);
                canvas.drawCircle(c.getX(), c.getY(), c.getRadio(), p);
               
                Rectangulo r = (Rectangulo) figuras.get(0);
                p.setColor(Color.RED);
                canvas.drawRect(r.getX(), r.getY(), r.getX()+r.getAncho(), r.getY()+r.getAlto(), p);
        }
       
        @Override
        public boolean onTouchEvent(MotionEvent event) {
                int x = (int) event.getX();
                int y = (int) event.getY();
               
                switch(event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                        for(Figura f : figuras) {
                                if(f instanceof Circulo) {
                                        Circulo c = (Circulo) f;
                                        int distanciaX = x - c.getX();
                                        int distanciaY = y - c.getY();
                                        if(Math.pow(c.getRadio(), 2) > (Math.pow(distanciaX, 2) + Math.pow(distanciaY, 2))) {
                                                figuraActiva = c.getId();

                                        }
                                } else {
                                        Rectangulo r = (Rectangulo) f;
                                        if(x > r.getX() && x < r.getX()+r.getAncho() && y > r.getY() && y < r.getY()+r.getAlto()) {
                                                figuraActiva = r.getId();
                                        }
                                }
                        }
                        break;
                case MotionEvent.ACTION_MOVE:
                        if(figuraActiva != -1) {
                                if(figuras.get(figuraActiva) instanceof Circulo) {
                                        figuras.get(figuraActiva).setX(x);
                                        figuras.get(figuraActiva).setY(y);
                                } else {
                                        Rectangulo r = (Rectangulo) figuras.get(figuraActiva);
                                        r.setX(x - r.getAncho()/2);
                                        r.setY(y - r.getAlto()/2);
                                }
                        }
                        break;
                case MotionEvent.ACTION_UP:
                        figuraActiva = -1;
                        break;
                }
               
                return true;
        }
}

Analisis

En los siguientes metodos existe mucha dependencia, fallara el codigo si se cambia el orden del tipo de las figuras que se van agregando al array, no se pueden agregar mas figuras, ademas el color de la figura se maneja en el metodo onDraw, ¿Que pasara si quieres manejar 100 figuras con diferente color?.

        public void surfaceCreated(SurfaceHolder arg0) {
                int id = 0;
                figuras = new ArrayList<Figura>();
                figuras.add(new Rectangulo(id++,200,500,200,200));
                figuras.add(new Circulo(id++,200,200,100));

                figuraActiva = -1;
               
                thread = new DragAndDropThread(getHolder(), this);
                thread.setRunning(true);
                thread.start();
        }

        public void onDraw(Canvas canvas) {
                Paint p = new Paint();
                p.setAntiAlias(true);
               
                canvas.drawColor(Color.WHITE);
               
                Circulo c = (Circulo) figuras.get(1);
                p.setColor(Color.BLACK);
                canvas.drawCircle(c.getX(), c.getY(), c.getRadio(), p);
               
                Rectangulo r = (Rectangulo) figuras.get(0);
                p.setColor(Color.RED);
                canvas.drawRect(r.getX(), r.getY(), r.getX()+r.getAncho(), r.getY()+r.getAlto(), p);
        }

Este codigo calcula sobre que figura el usuario oprime la patalla. Como se puede observar solo se esta considerando el manejo de dos tipos de figuras, sin embargo, si crece el numero de figuras, este algoritmo se hara mas dificil de manejar.

case MotionEvent.ACTION_DOWN:
        for(Figura f : figuras) {
                if(f instanceof Circulo) {
                        Circulo c = (Circulo) f;
                        int distanciaX = x - c.getX();
                        int distanciaY = y - c.getY();
                        if(Math.pow(c.getRadio(), 2) > (Math.pow(distanciaX, 2) + Math.pow(distanciaY, 2))) {
                        figuraActiva = c.getId();
                        }
                } else {
                        Rectangulo r = (Rectangulo) f;
                        if(x > r.getX() && x < r.getX()+r.getAncho() && y > r.getY() && y < r.getY()+r.getAlto()) {
                        figuraActiva = r.getId();
                        }
                }
        }
        break;

Este codigo cambia las coordenadas del centro de la figura seleccionada cuando el usuario mueve el dedo sobre la superficie de la pantalla del dispositivo, al igual que el codigo anterior con el tiempo no se podra dar mantenimiento, ademas de que esta muy enfocado a solo dos figuras.

case MotionEvent.ACTION_MOVE:
        if(figuraActiva != -1) {
                if(figuras.get(figuraActiva) instanceof Circulo) {
                        figuras.get(figuraActiva).setX(x);
                        figuras.get(figuraActiva).setY(y);
                } else {        // in this context, only instanceof Rectangulo
                        Rectangulo r = (Rectangulo) figuras.get(figuraActiva);
                        r.setX(x - r.getAncho()/2);
                        r.setY(y - r.getAlto()/2);
                }
        }
        break;

Si se desea implementar cualquier otra funcionalidad en este codigo, sera una proeza hacerlo, por ejemplo, cuando se selecciona una figura, esta no se muestra encima de la otra, ¿cuanto trabajo sera necesario para implementar que la figura seleccionada se muestre sobre la otra?, ¿que coste tendra que la aplicacion pueda manejar un numero indeterminado de las figuras que ahorita tiene?, al menos, sera necesario agregarle varias sentencias de control, lo que lo hara mas...

Analisis

Asignacion de responsabilidades

Generalmente se piensa en el codigo como algo inanimado, es decir sin vida, bueno, asi lo pensamos muchos pero la OOP se creo con el fin de simular el comportamiento de las cosas del mundo real en la computadora. Consideremos los siguiente: al oir nuestro nombre generalmente volteamos, o cuando nos señalan, sabemos que refieren a nosotros, y sabemos que nos llamaron o señalaron y actuamos como resultado de esa accion, tambien tu sabes si estas de malas o buenas.

Si homologamos todo la anterior, ¿la figura tendria que saber que a ella la seleccionaron?, ¿la misma figura tendria que saber de que color es?, ¿la misma figura tendria la obligacion de pintarse?, ¿Que necesita saber la figura en este contexto de aplicacion para que sea funcional?.

La figura necesita saber: su color, las coordenadas del centro de la figura, si fue seleccionada, y como se va a pintar. Considerando que como se va a pintar es un metodo, entonces la clase inicial que va a representar la figura quedaria, en una forma inicial, asi:

public abstract class Shape {

        protected int color;
        protected int x;
        protected int y;
        protected boolean active;

        public abstract void draw();
}

Implementado diseño, polimorfismo y el patron strategy

Analicemos mas a fondo el metodo onDraw:

public void onDraw(Canvas canvas) {
        Paint p = new Paint();
        p.setAntiAlias(true);
       
        canvas.drawColor(Color.WHITE);
       
        Circulo c = (Circulo) figuras.get(1);
        p.setColor(Color.BLACK);
        canvas.drawCircle(c.getX(), c.getY(), c.getRadio(), p);
       
        Rectangulo r = (Rectangulo) figuras.get(0);
        p.setColor(Color.RED);
        canvas.drawRect(r.getX(), r.getY(), r.getX()+r.getAncho(), r.getY()+r.getAlto(), p);
}

Se obtiene del array figuras cada una de las figuras, se obtiene de cada figura las coordenadas y se pinta la figura muy explicitamente. Imaginemos que vamos a utilizar muchos tipos de figuras y una gran cantidad de ellas, entonces lo que necesitariamos es que todas las clases que hereden de Shape tengan el mismo metodo, con los mismos parametros, con el fin de que con un solo bucle se puedan pintar solas.
Entonces el metodo quedaria asi en la clase Shape (se esta omitiendo lo innecesario para el ejemplo):

public abstract class Shape {
       
        public abstract void draw(Canvas canvas, Paint paint);
}

Y quedaria asi implementado en dos clases (recuerda que Shape tiene las coordenadas del centro de la figura):

public class Square extends Shape{
        @Override
        public void draw(Canvas canvas, Paint paint) {

                paint.setColor(color);
                canvas.drawRect(x, y, x + width, y + height, paint);
        }
}

public class Circle extends Shape{
        @Override
        public void draw(Canvas canvas, Paint paint) {
                paint.setColor(color);
                canvas.drawCircle(x, y, radius, paint);
        }
}

El metodo onDraw se transformaria y quedaria asi:

@Override
public void onDraw(Canvas canvas) {

        canvas.drawColor(Color.WHITE);
               
        int size = shapes.size();
               
        for(int i = size - 1; i >= 0; i--){
                       
                Shape shape = (Shape)shapes.get(i);
                shape.draw(canvas, paint);
        }
}

Algunas observaciones:
El for va de mas a cero, con el fin de que se dibuje hasta el ultimo la figura seleccionada.

El bajo acoplamiento de Shape esta aumentado, ya que debe de manejar las clases Canvas, Paint.

Estamos usando el polimorfismo en toda la expresion de la palabra, ya que lo que contiene el array shapes son objetos de la clase Circle y Square.

Lo importante de el algoritmo mostrado abajo son los if que se escribieron despues del comentario, hacen el calculo para saber si el toque de la pantalla queda dentro del area de cada figura, en caso de que sea asi, figuraActiva obtiene el id de la figura, para hacer ese calculo es necesario obtener las coordenadas del toque y las coordenadas del centro de la figura.

case MotionEvent.ACTION_DOWN:
        for(Figura f : figuras) {
                if(f instanceof Circulo) {
                        Circulo c = (Circulo) f;
                        int distanciaX = x - c.getX();
                        int distanciaY = y - c.getY();
                        if(Math.pow(c.getRadio(), 2) > (Math.pow(distanciaX, 2) + Math.pow(distanciaY, 2))) {
                                figuraActiva = c.getId();
                        }
                } else {        // in this context, only instanceof Rectangulo
                        Rectangulo r = (Rectangulo) f;
                        if(x > r.getX() && x < r.getX()+r.getAncho() && y > r.getY() && y < r.getY()+r.getAlto()) {
                                figuraActiva = r.getId();
                        }
                }
        }
        break;

//-- Comentario ------------------------------------

if(Math.pow(c.getRadio(), 2) > (Math.pow(distanciaX, 2) + Math.pow(distanciaY, 2))) {
        figuraActiva = c.getId();
}

if(x > r.getX() && x < r.getX()+r.getAncho() && y > r.getY() && y < r.getY()+r.getAlto()) {
        figuraActiva = r.getId();
}

Entonces si la figura tiene las coordenadas del centro, solo es necesario que sepa las coordenadas del toque:

public abstract class Shape {
       
        public abstract boolean isEventOverShape(int x, int y);
}

Asi estaria implementadas en las clases que heredan de Shape:

public class Square extends Shape{
@Override
        public boolean isEventOverShape(int x, int y) {

                if(x > this.x && x < this.x + this.width && y > this.y && y < this.y + this.height) {

                        active = true;
                } else {
                       
                        active = false;
                }
               
                return active;
        }
}

public class Circle extends Shape{
        @Override
        public boolean isEventOverShape(int x, int y) {
               
                int distanceX = x - this.x;
                int distanceY = y - this.y;
                if(Math.pow(radius, 2) > (Math.pow(distanceX, 2) + Math.pow(distanceY, 2))) {

                        active = true;
                } else {
                       
                        active = false;
                }
               
                return active;
        }
}

Y asi se reduciria el codigo:

case MotionEvent.ACTION_DOWN:
        for(Shape shape : shapes) {
                               
                shape.isEventOverShape(x, y);
        }
                       
        for(Shape shape : shapes) {
                               
                if (shape.isActive()){
                        shapes.remove(shape);
                        shapes.add(0, shape);
                        break;
                }
        }
        break;

El segundo for es para pasar al inicio del array la primera figura que fue activada, con el fin de mostrarse hasta el ultimo en el metodo onDraw.

Continuamos con el siguiente codigo, a traves del cual se obtiene las coordenadas del centro de la figura, las cuales sirven para indicar en donde se pintara la figura mientras el usuario recorre la pantalla con el dedo, al analizar el algoritmo nos podemos dar cuenta que lo unico necesario es pasar las coordenadas en donde el dedo del usuario hace contacto con la pantalla:

case MotionEvent.ACTION_MOVE:
        if(figuraActiva != -1) {
                if(figuras.get(figuraActiva) instanceof Circulo) {
                        figuras.get(figuraActiva).setX(x);
                        figuras.get(figuraActiva).setY(y);
                } else {
                        Rectangulo r = (Rectangulo) figuras.get(figuraActiva);
                        r.setX(x - r.getAncho()/2);
                        r.setY(y - r.getAlto()/2);
                }
        }
        break;

Esta funcionalidad la vamos a implementar en las clases de la siguiente manera: en la clase Shape, el metodo quedara asi:

public abstract class Shape {

        public abstract void setPosition(int x, int y);
}

Ya implementado en Circle y Square:

public class Circle extends Shape{
        @Override
        public void setPosition(int x, int y) {

                this.x = x;
                this.y = y;
        }
}

public class Square extends Shape{

        @Override
        public void setPosition(int x, int y) {

                this.x = x - this.width/2;
                this.y = y - this.height/2;
        }

}

Y el codigo dentro del case:

case MotionEvent.ACTION_MOVE:
                       
        for(Shape shape : shapes) {
                               
                if (shape.isActive()){
                        shape.setPosition(x, y);
                        break;
                }
        }
        break;

Asi se agregarian las figuras que se quieran mostrar dentro de la aplicacion:

        public void surfaceCreated(SurfaceHolder arg0) {

                shapes = new ArrayList<Shape>();
                shapes.add(new Square(Color.BLACK,200,500,200,200));
                shapes.add(new Circle(Color.BLUE,200,200,100));
                shapes.add(new Square(Color.CYAN,200,500,200,200));
                shapes.add(new Circle(Color.DKGRAY,200,200,100));
                shapes.add(new Square(Color.GRAY,200,500,200,200));
                shapes.add(new Circle(Color.GREEN,200,200,100));
                shapes.add(new Square(Color.LTGRAY,200,500,200,200));
                shapes.add(new Circle(Color.MAGENTA,200,200,100));
                               
                thread = new DragAndDropThread(getHolder(), this);
                thread.setRunning(true);
                thread.start();
        }

Lo unico que se hizo fue asignar responsabilidades correctamente y la calidad de la aplicacion aumento considerablemente.

Comentarios

Opciones de visualización de comentarios

Seleccione la forma que prefiera para mostrar los comentarios y haga clic en «Guardar las opciones» para activar los cambios.
Imagen de ezamudio

Bien!

Creo hace falta más contenido relacionado con Android, buen aporte

Imagen de beto.bateria

Falta subir codigo.

Falta subir el codigo, en un rato lo subo.

Imagen de paranoid_android

Esta bueno el ejercicio. Comento solo por teorizar.

Hola.
Aclaro que tu refactoring está bastante bien. (Me quede por dudas por eso de que el código no esta completo)
Y solo por disfrutar del gusto, jajaja.
En estas líneas:

// public class Square extends Shape{
if(x > this.x && x < this.x + this.width  && y > this.y && y < this.y + this.height) {

// public class Circle extends Shape{
if(Math.pow(radius, 2) > (Math.pow(distanceX, 2) + Math.pow(distanceY, 2))) {

¿Vale la pena optimizar? Reducir la complejidad ciclomática.
Calcular estos valores al momento de crear el círculo o el cuadrado y/o si habría una manera visual más legible de expresarlo.
Saludos.

Imagen de beto.bateria

Ese codigo sirve para saber

Ese codigo que escribiste, sirve para saber si el toque del usuario es sobre el area de la figura, y solo se ejecuta cuando se toca la pantalla del dispositivo, no creo que se pueda optimizar. Respecto al codigo faltante, son los archivos del proyecto ya funcionando, para que puedan probarlo,

Imagen de paranoid_android

Ahora veo

Ok ahora entiendo.
Solo es sugerencia, lo único que se me ocurre...

// public class Square extends Shape{

boolean limiteIzquierdo = (x > this.x);
boolean limiteDerecho = (x < this.x + this.width);
boolean limiteSuperior= (y > this.y);
boolean limiteIneferior= (y < this.y + this.height);

if(  limiteIzquierdo   &&  limiteDerecho && limiteSuperior && y < limiteIneferior ) {

// public class Circle extends Shape{
float areaToque = Math.pow(radius, 2);
float entradaUsuario = Math.pow(distanceX, 2) + Math.pow(distanceY, 2) ;

if( areaToque > entradaUsuario ) {

Esta interesante tu post. Un saludo

Imagen de beto.bateria

Te voy a responder con un

Te voy a responder con un ejemplo:

Hace tiempo hice una aplicacion en donde la informacion del xml, y el ResultSet los pasaba a un DTO y despues esa informacion la convertia en otro formato. Tiempo despues deje de trabajar en esa empresa, y otro programador se dio cuenta que era algo lento ese procedimiento, lo que hizo el es quitar el DTO y, por ejemplo, pasar la informacion del ResulSet a XML directamente, la cuestion es ¿cual de los dos procedimientos esta mal? la respuesta: ninguno, todo depende de los requerimientos. El hecho de pasar todo a DTO hace mas flexible la aplicacion en cuanto a cambios, si haces la conversion directamente es mas rapido.

Debes de considerar: ¿cual es el costo de la mano de obra?, ¿cual es el costo de comprar hardware mas rapido?, ¿Vale la pena pasar todo a DTO y despues hacer otra conversion?, es lo mismo con tu ejemplo.

Una observacion: En este ejemplo en especial, estamos manejando graficos, por lo cual los algoritmos deberian ser lo mas rapido posible, es decir, hasta donde se pueda evitar la OOP y "optimizaciones"(como tu ejemplo) para el programador (aqui se debe de considerar que un buen programador escribe codigo entendible).

Imagen de paranoid_android

Es cierto.

Hola Beto. Es cierto a veces no se puede optimizar en todo se tiene que decidir optimizar en performance o en complejidad.
Esa optimización ya es un lujo normalmente no da tiempo para esas cosas.
Otro intermedio podría ser lo mismo pero con comentarios.

// public class Square extends Shape{
if( x > this.x && // Limite Izquierdo
    x < this.x + this.width  &&  // limite derecho
    y > this.y && // Limite superior
    y < this.y + this.height) {  // Limite inferior

Solo la sugerí por compartir, disfrutar de la programación y del refactoring.
En el fondo ninguna de esas optimizaciones es necesaria, solo por el mero gusto de aportar.
En un programa que se hace por gusto y no por obligación da tiempo para esas cosas ;)

Saludos.

Imagen de beto.bateria

Los leere...

Los leere...

style="display:inline-block;width:728px;height:90px"
data-ad-client="ca-pub-5164839828746352"
data-ad-slot="7563230308">