Componentes de Software

A seguir são reunidos alguns conceitos importantes para se entender o princípio de Componentes de Software.

Em linhas gerais, um componente é um módulo de software reusável, que se liga aos seus parceiros exclusivamente através de interfaces. Apresentaremos aqui uma abordagem típica de ligação de componentes baseada em interfaces e conexões:

  • Interfaces - expressam os serviços providos pelos componentes, declarados através de interfaces Java;
  • Conexões - é a forma como componentes são ligados entre si; a partir delas um componente toma conhecimento do outro para que se comuniquem.

Interfaces

Idealmente, todas as funcionalidades de um componente deveriam ser acessadas exclusivamente através de interfaces.

Vamos analisar o processo de definição do DataSetComponent como um componente de software. Considerando que o código a seguir é uma versão inicial do componente, uma representação visual para o mesmo pode ser feita em UML, conforme segue:

Note que cada interface que ele disponibiliza é apresentada na forma de uma haste com um círculo na ponta.

Como a interface IDataSetProperties tem métodos get e set para definir a propriedade DataSource, em vez de representar essa interface da forma tradicional, podemos representá-la na forma de uma propriedade associada ao componente como segue:

A representação de propriedades não é padrão UML, mas é usada por algumas extensões. Isso introduz um terceiro elemento usual em algumas implementações de componentes:

  • Propriedades - podem ser usadas como mecanismos de configuração externa do componente; por exemplo, através da propriedade dataSource é possível externamente se configurar a fonte de dados do componente.

In [1]:
public interface ITableProducer {
  String[] requestAttributes();
  String[][] requestInstances();
}


Out[1]:
com.twosigma.beaker.javash.bkr43436306.ITableProducer

In [2]:
public interface IDataSetProperties {
  public String getDataSource();
  public void setDataSource(String dataSource);
}


Out[2]:
com.twosigma.beaker.javash.bkr43436306.IDataSetProperties

In [3]:
public interface IDataSet extends IDataSetProperties, ITableProducer {
}


Out[3]:
com.twosigma.beaker.javash.bkr43436306.IDataSet

In [4]:
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;

public class DataSetComponent implements IDataSet {
  private String dataSource = null;
  private String[] attributes = null;
  private String[][] instances = null;
  
  public DataSetComponent() {
    /* nothing */
  }

  public String getDataSource() {
    return dataSource;
  }

  public void setDataSource(String dataSource) {
    this.dataSource = dataSource;
    if (dataSource == null) {
      attributes = null;
      instances = null;
    } else
      readDS();
  }
  
  public String[] requestAttributes() {
    return attributes;
  }
  
  public String[][] requestInstances() {
    return instances;
  }
  
  private void readDS() {
    ArrayList<String[]> instArray = new ArrayList<String[]>();
    try {
      BufferedReader file = new BufferedReader(new FileReader(dataSource));
        
      String line = file.readLine();
      if (line != null) {
        attributes = line.split(",");
        line = file.readLine();
        while (line != null) {
          String[] instLine = line.split(",");
          instArray.add(instLine);
          line = file.readLine();
        }
        instances = instArray.toArray(new String[0][]);
      }
        
      file.close();
    } catch (IOException erro) {
      erro.printStackTrace();
    }
  }
  
}


Out[4]:
com.twosigma.beaker.javash.bkr43436306.DataSetComponent

Conexões

É comum em componentes se usar a mesma estratégia vista em aulas anteriores, na qual um objeto de uma classe guarda referência de um objeto de outra classe. Utilizaremos um método padrão chamado connect que conecta dois componentes. Para isso um dos objetos recebe a referência de outro.

No exemplo a seguir, foi criado um componente chamado ConsoleComponentA, cuja função é mostrar dados de outro componente conectado a ele no console. Ele dispõe do seguinte método:

public void connect(ITableProducer producer)

Esse método recebe a referência para qualquer objeto que implementa a interface ITableProducer e a guarda. A referência é posteriormente usada para a comunicação entre os dois objetos.

Visualmente, a conexão entre os dois componentes pode ser vista da seguinte maneira:


In [5]:
public class ConsoleComponentA {
  private ITableProducer iProducer;
  
  public void connect(ITableProducer producer) {
    iProducer = producer;
  }
  
  public void update() {
    if (iProducer != null) {
        System.out.println("=== Attributes ===");
        String attributes[] = iProducer.requestAttributes();
        for (int a = 0; a < attributes.length-1; a++)
          System.out.print(attributes[a] + ", ");
        System.out.println(attributes[attributes.length-1]);

        System.out.println();
        System.out.println("=== Instances ===");
        String instances[][] = iProducer.requestInstances();
        for (int i = 0; i < instances.length; i++) {
          for (int a = 0; a < attributes.length-1; a ++)
            System.out.print(instances[i][a] + ", ");
          System.out.println(instances[i][attributes.length-1]);
        }
    }
  }
}


Out[5]:
com.twosigma.beaker.javash.bkr43436306.ConsoleComponentA

Referência não explícita

Entretanto, a do lado do ConsoleComponentA a forma como ele guarda a referência para o outro componente exige que se chame o método diretamente nele, veja a seguir:


In [6]:
IDataSet dataset = new DataSetComponent();
dataset.setDataSource("../../../db/zombie/zombie-health-spreadsheet.csv");

ConsoleComponentA console = new ConsoleComponentA();
console.connect(dataset);

console.update();


=== Attributes ===
paralysis, yellow_tong, member_loss, chest_pain, trembling_finger, severe_anger, history_bacteria, diagnostic

=== Instances ===
t, t, f, f, f, f, f, bacterial_infection
f, t, f, f, f, f, f, bacterial_infection
f, t, f, f, t, f, t, bite_deficit
f, t, t, f, t, f, f, bite_deficit
f, f, t, t, f, f, f, viral_infection
f, f, t, f, f, t, f, fights
f, f, f, f, f, t, f, nothing
f, f, f, f, t, f, f, bite_deficit
f, t, f, t, f, f, f, bacterial_infection
f, f, f, t, f, f, f, viral_infection
f, t, t, f, f, f, t, bite_deficit
t, t, f, f, f, f, f, bacterial_infection
f, f, f, t, f, f, t, viral_infection
f, f, t, f, f, f, f, fights
f, t, f, f, t, f, t, bite_deficit
f, t, t, f, t, f, f, bite_deficit
f, f, f, t, f, f, f, fights
f, t, f, f, f, f, f, bacterial_infection
f, f, f, f, t, f, f, bite_deficit
Out[6]:
null

Interface Requerida

O ideal é que tudo seja explícito e que haja uma interface para realizar essa conexão. Desse modo, criaremos uma interface chamada ITableReceptacle. Ela define o método para se estabelecer a conexão com objetos que têm a interface ITableProducer.

A interface requerida é representada visualmente por um meio círculo e indica o nome da interface que ela requer:

Outra maneira bastante usual de representar os dois componentes conectados é ligando diretamente a interface provida com a requerida:

Para tornar o componente ConsoleComponent completamente acessível por interfaces, acrescentamos outra interface para o seu método update(). O Diagrama completo fica:

Como foi feito antes, usamos a interface IConsole para juntar as duas outras interfaces através da herança.


In [7]:
public interface ITableReceptacle {
  public void connect(ITableProducer producer);
}


Out[7]:
com.twosigma.beaker.javash.bkr43436306.ITableReceptacle

In [8]:
public interface IConsoleUpdate {
  public void update();
}


Out[8]:
com.twosigma.beaker.javash.bkr43436306.IConsoleUpdate

In [9]:
public interface IConsole extends ITableReceptacle, IConsoleUpdate {
}


Out[9]:
com.twosigma.beaker.javash.bkr43436306.IConsole

In [10]:
public class ConsoleComponent implements IConsole {
  private ITableProducer iProducer;
  
  public void connect(ITableProducer producer) {
    iProducer = producer;
  }
  
  public void update() {
    if (iProducer != null) {
        System.out.println("=== Attributes ===");
        String attributes[] = iProducer.requestAttributes();
        for (int a = 0; a < attributes.length-1; a++)
          System.out.print(attributes[a] + ", ");
        System.out.println(attributes[attributes.length-1]);

        System.out.println();
        System.out.println("=== Instances ===");
        String instances[][] = iProducer.requestInstances();
        for (int i = 0; i < instances.length; i++) {
          for (int a = 0; a < attributes.length-1; a ++)
            System.out.print(instances[i][a] + ", ");
          System.out.println(instances[i][attributes.length-1]);
        }
    }
  }
}


Out[10]:
com.twosigma.beaker.javash.bkr43436306.ConsoleComponent

Usando a Interface Requerida

Note no código a seguir que ambos os componentes passam a ser tratados completamente a partir de suas interfaces. Mais adiante analisaremos a importância de realizar dessa maneira.


In [11]:
IDataSet dataset = new DataSetComponent();
dataset.setDataSource("../../../db/zombie/zombie-health-spreadsheet.csv");

IConsole console = new ConsoleComponent();
console.connect(dataset);

console.update();


=== Attributes ===
paralysis, yellow_tong, member_loss, chest_pain, trembling_finger, severe_anger, history_bacteria, diagnostic

=== Instances ===
t, t, f, f, f, f, f, bacterial_infection
f, t, f, f, f, f, f, bacterial_infection
f, t, f, f, t, f, t, bite_deficit
f, t, t, f, t, f, f, bite_deficit
f, f, t, t, f, f, f, viral_infection
f, f, t, f, f, t, f, fights
f, f, f, f, f, t, f, nothing
f, f, f, f, t, f, f, bite_deficit
f, t, f, t, f, f, f, bacterial_infection
f, f, f, t, f, f, f, viral_infection
f, t, t, f, f, f, t, bite_deficit
t, t, f, f, f, f, f, bacterial_infection
f, f, f, t, f, f, t, viral_infection
f, f, t, f, f, f, f, fights
f, t, f, f, t, f, t, bite_deficit
f, t, t, f, t, f, f, bite_deficit
f, f, f, t, f, f, f, fights
f, t, f, f, f, f, f, bacterial_infection
f, f, f, f, t, f, f, bite_deficit
Out[11]:
null