Newsgroup: it.comp.java
Titolo: [source] StringReplacer - This class replace a substring with another one...


Visto che si č parlato di scrivere codice per descrivere dei principi
di design, cosa non banale, si potrebbe provare ad 'intervenire' su
codice gią esistente (Refactoring), come ad esempio questo qui postato
da Castagna.
Questo rimanendo nelle linee guida principali, ad esempio
Accoppiamento e Coesione, e ricordando chiaramente che la cosa č fatta
al solo scopo descrittivo dei principi e non tanto per criticare.
1a cosa da fare, dopo aver letto questo stralcio dal pattern di basso
accoppiamento, individuare come diminuire il "potenziale"
accoppiamento di questa classe. Poi passo passo vedremo altri
potenziali problemi (e ripeto potenziali: dipende dall'uso che se ne
fa, cioč dal contesto)
<LOW COUPLING>
Intento
Rendere pił facile il riuso e l'estensibilitą di "moduli" 
Motivazione
L'Accoppiamento č una misura della quantitą di dipendenza fra metodi,
oggetti, classi o packages (in generale di qualsiasi "modulo" in un
software. Per quantitą di dipendenza si intende quanto un "modulo"
software ha "conoscenza" di un altro modulo, quanto si affida su di
esso per svolgere un determinato compito, in sostanza quanto il suo
corretto funzionamento dipende da altri moduli.
Avere un accoppiamento alto fra "moduli" č sconveniente in quanto una
modifica ad uno di essi potrebbe ripercuotersi a cascata su tutti gli
altri, andando ad inficiare le qualitą fondamentali di un software
object oriented, cioč l'estensibilitą e la riusabilitą. 
Per definire in maniera pił precisa l'accoppiamento fra diversi
"moduli" di un software, partiamo dall'accoppiamento fra metodi:
infatti i moduli via via sovrastanti (oggetti, interfacce, classi,
componenti, packages, ecc.) possono essere espressi direttamente o
indirettamente tramite i metodi di cui sono direttamente od
indirettamente composti.
Accoppiamento fra metodi
A questo punto, per definire correttamente l'accoppiamento fra metodi,
ci serve una distinzione coerente fra i tipi di metodi possibili:
posto che lo stato di un'oggetto č la "fotografia" (snapshot) dei suoi
attributi in un preciso istante, tutti i metodi si possono dividere in
tre grandi classi diverse, o "Tipi di metodi": 
Utility: non usa nč modifica lo stato dell'oggetto per es:
Integer.toString(int), Math.cos(double) 
State View: usa lo stato dell'oggetto per es: TextField.getText(),
Object.toString() 
State Change: modifica lo stato dell'oggetto per es:
Vector.addElement(..), Hashtable.clear() 
 
Definiamo ora come un metodo (utility, state view o state change) puņ
trattare il suo input ed il suo output (in senso lato)
Modalitą di INPUT per un metodo: 
puņ prendere parametri d'ingresso 
puņ prendere dati da attributi e metodi "statici" (di classe) 
puņ prendere dati da attributi e metodi (d'istanza) 
Modalitą di OUTPUT per un metodo: 
puņ tornare un valore, di tipo primitivo o "handle" 
puņ alterare oggetti passati come parametro 
puņ alterare attibuti "statici" direttamente o attraverso metodi
"statici" 
puņ alterare attibuti direttamente o attraverso metodi 
puņ lanciare un'eccezione 
 
Avendo ora a disposizione una distinzione sulla tipologia di input e
di output, possiamo "catalogare" ognuno dei tre tipi di metodi in base
a come gestiscono l'input e l'output. 
Metodo utility: il metodo utility č quello che puņ raggiungere il
minimo accoppiamento possibile. La tabella seguente schematizza un
metodo utility con il minimo accoppiamento possibile. 
INPUT 
prende solo parametri d'ingresso e solo dati di cui ha effettivamente
bisogno per produrre l'output 
OUTPUT 
ritorna un valore, di tipo primitivo o "handle", oppure altera oggetti
passati come parametro 
i dati in output sono tutti prodotti da lui stesso (non chiama altri
metodi) 
 
Un esempio preso dalla libreria di classi Java puņ essere
Integer.toString(int). Analizziamo quanto questo metodo č accoppiato
con altri metodi. Innanzitutto definiamo il fine di questo metodo:
trasformare un int in String, poi studiamone l'implementazione. Il
metodo č chiaramente un "utility", in quanto non altera nč ha bisogno
dello stato della classe Integer, essendo un metodo statico (di
classe). Prende in input un tipo scalare, int, e crea l'oggetto String
corrispondente al numero intero passatogli come parametro. In output
produce un handle a String, senza l'ausilio di altri metodi, dunque
produce tutto l'output da sč. Concludendo, il metodo in questione č un
metodo utility con il minimo accoppiamento possibile: il metodo
"conosce" solo il costruttore della classe String, ovviamente
necessario per produrre l'output. 
Metodo State View: metodi di questo tipo producono di solito un
accoppiamento maggiore di quelli "utility". La tabella seguente
schematizza un metodo "state view" con il minimo accoppiamento
possibile. 
INPUT 
prende parametri d'ingresso e usa attributi o metodi (sia di classe
che di istanza) appartenenti alla sua classe (quindi non attributi o
metodi di altre classi, escluse le costanti) 
OUTPUT 
torna un valore, altera parametri oppure lancia un'eccezione 
 
Un esempio preso dalla libreria di classi Java puņ essere
TextField.getText(). Questo metodo restituisce una stringa che č il
contenuto di un oggetto grafico AWT che rappresenta un campo
editabile. A differenza del caso precedente, questo metodo utilizza lo
stato dell'oggetto specifico sul quale viene invocato, in questo caso
un attributo che č la rappresentazione interna del testo del campo
editabile, e che supponiamo sia una String (ma non č assolutamente
detto che lo sia). Dunque il metodo ha chiaramente bisogno dello stato
dell'oggetto, che lascia perņ invariato, ed č dunque uno "state/view".
Ha perņ un accoppiamento minimo, in quanto non interagisce con metodi
di altre classi nč per quanto riguarda l'input nč per quanto riguarda
l'output. 
Metodo State Change: questi metodi producono di solito un
accoppiamento ancora pił forte di quelli "state/view". La tabella
seguente schematizza un metodo "state change" con il minimo
accoppiamento possibile. 
INPUT 
prende parametri d'ingresso e usa attributi o metodi (sia di classe
che di istanza) appartenenti alla sua classe (quindi non attributi o
metodi di altre classi, escluse le costanti) 
OUTPUT 
torna un valore, altera parametri, lancia un'eccezione, oppure lo
esprime attraverso attributi e metodi (sia di classe che di istanza)
appartenenti alla sua classe (in qualunque maniera tranne che con
variabili di classe di altre classi) 
 
L'unica differenza con lo state/view č che il metodo puņ modificare lo
stato dell'oggetto su cui č stato invocato (se non č un metodo di
classe, nel qual caso modificherą lo stato di altri oggetti) per
produrre l'output che ci si aspetta. Un metodo "state/change" con
accoppiamento minimo perņ non modificherą lo stato di altri oggetti
tramite metodi di questi ultimi. Un esempio preso dalla libreria di
classi Java puņ essere Vector.addElement(), che modifica lo stato
dell'oggetto di classe Vector poichč aggiunge un altro Object alla
lista. 
</LOW COUPLING> 
         ______
        /_____/\             Ugo Landini
       /____ \\ \            Education Technical Supervisor
      /_____\ \\ /           Sun Educational Services
     /_____/ \/ / /          
    /_____/ /   \//\         SUN Microsystems Italia S.p.A.
    \_____\//\   / /         Via Romagnosi, 4
     \_____/ / /\ /          00196 - Rome (RM) - Italy
      \_____/ \\ \           @mail : 
ugo.l...@sun.com
       \_____\ \\            Phone : +39 (0)  6 xxxx.xxx
        \_____\/             GSM   : +39 (0)  335 7766651

fa, cioè dal contesto)
Of course! 8)

<LOW COUPLING>
Come classificheresti un metodo replace di una stringa?
Utility penso io...
Modalità di INPUT per un metodo: 
prende parametri d'ingresso 
prende dati da attributi e metodi (d'istanza)
String è un'oggetto, quindi quest'ultimo mi sembra più appropriato!
Modalità di OUTPUT per un metodo: 
torna un valore, di tipo primitivo o "handle" 
... e difatti:
Metodo utility: il metodo utility è quello che può raggiungere il
minimo accoppiamento possibile. La tabella seguente schematizza un
metodo utility con il minimo accoppiamento possibile. 
INPUT 
prende solo parametri d'ingresso e solo dati di cui ha effettivamente
bisogno per produrre l'output 
OUTPUT 
ritorna un valore, di tipo primitivo o "handle", oppure altera oggetti
passati come parametro 
i dati in output sono tutti prodotti da lui stesso (non chiama altri
metodi) 
... a questo punto, credo sia meglio:
-------------------------------------------------------------------------------------------[ source ]---
  public static String replace (String original, String find, String replace) {
    if ( original == null ) return null;
    if ( (find == null) || (replace == null) ) {
      return original;
    }
    int index = original.indexOf(find);
    while ( index >= 0 ) {
      original = original.substring(0, index ) + replace + original.substring(index + find.length());
      index = original.indexOf(find, index + replace.length());
    } // while
    return original;
  } // replace
--------------------------------------------------------------------------------------------------------
Idee, suggerimenti, consigli?
... ben accetti, da parte di tutti! :P

Utility penso io...
si

Ok, primo passo fatto. Visto, colpito, affondato!
Quindi, riassumendo, non c'è ragione xchè StringReplacer abbia un
attributo di tipo String che contenga la Stringa su cui operare, visto
che fra le sue responsabilità non è prevista quella di "ricordarsi"
qualcosa. A cosa servirebbe tenersi quel reference? In questo caso
nulla (e sottolineo in questo caso: non vorrei che qualcuno
cominciasse a scrivere solo classi con metodi utility e poi se la
prendesse con me...)
Un oggetto può "ricordare" delle cose, e lo fa ovviamente attraverso i
suoi attributi. Può inoltre "fare" delle cose sui suoi attributi e/o
sui parametri passati ai metodi. In questo caso, "fa" solo sui
parametri che gli si passano, ottenendo un accopppiamento potenziale
minimo,
Facendo i formali (ma poco), la classe è una classe "Helper", e il
metodo è un utility. Rientra in una classe che starebbe bene in un
package "esterno", insomma una cara vecchia libreria.
Abbiamo fatto del Refactoring, "migliorando" il Design. Ora possiamo
lavorare sia su  questioni di "stile" che di Design. Per chi non
sapesse la differenza, il Design è qualcosa di più del semplice stile,
xchè le scelte hanno una influenza importante su estensibilità
(facilità con la quale si aggiungono nuove funzionalità),
manutenibilità, ecc. (tutte le varie qualità secondo Meyer di cui
trovate una disonesta copia sul mio sito). Lo stile invece rappresenta
un modo più "elegante" di risolvere un problema, ma senza grosse
implicazioni pratiche. Il confine è ovviamente labile.
Ora io vedo un paio di cose nel codice di cui sopra:
una di stile, che influenza la leggibilità/complessità del codice
una di design, che influenza le performance
A voi la palla.


A voi la palla.
Ehy... ma così non vale! :(
Oh, io ci provo... la butto là...
--------------------------------------------------------------------------------------[ source code ]---
public static String replace (String original, String find, String replace) {
  if ( original == null ) {
    return null;
  }
  if ( (find == null) || (replace == null) ) {
    return original;
  }
  int index = original.indexOf(find);
  int findLength = find.length();
  int replaceLength = replace.length();
  while ( index >= 0 ) {
    original = original.substring(0, index ) + replace + original.substring(index + findLength);
    index = original.indexOf(find, index + replaceLength);
  } // while
  return original;
} // replace
--------------------------------------------------------------------------------------[ source code ]---
Ciao, e buona giornata!
A domani il responso?
Sono curioso! :)


Ok, sicuramente il length fuori dai cicli va meglio di prima. 
Allora c'erano almeno DUE problemi di design ed uno di stile: l'altro
problema di performance è probabilmente più "grave" del length.
Poi si vedrà se c'è qualcosa di altro!
ciao


 
Intendi StringBuffer (usare StringBuffer invece di String per le 
operazioni sulla stringa)?
Ciao,
Tatiana

operazioni sulla stringa)?
... allora siamo arrivati a questo codice.
Così va bene? Ugo?! 8)
--------------------------------------------------------------------------------------[ source code ]---
public static StringBuffer replace (StringBuffer original, StringBuffer find, StringBuffer replace) {


problema di performance č probabilmente piů "grave" del length.
Suvvia, U., non mi/ci puoi tenere cosě sulle spine... fuori il rospo! :P

-----------------------------------------------------------------------------
---------[ source code ]---
...nel pomeriggio provero' a postare una soluzione...
Un saluto
  Lucio
-----------------------------------------
Lucio Benfante (benf...@MailandNews.com)
Brainbench MVP for Java 1
http://www.brainbench.com

...
Eccola.
Innanzitutto ho accorpato i due statement if che controllano la nullita' dei 
parametri: il risultato e' equivalente ,ma secondo me e' un po' piu' chiaro. 
E' sicuramente una preferenza personale. Io il controllo dei parametri in 
questo caso lo interpreto cosi': se qualche parametro non va bene, il valore 
di ritorno e' la stringa originale.
Per l'algoritmo di sostituzione sono andato a riprendere un mio post di 
qualche tempo fa, approfittandone per fare qualche ottimizzazione.
Esso utilizza ora uno StringBuffer, sfruttando il suo metodo replace(). 
L'oggetto String originale viene invece usato per trovare la posizione della 
stringa da sostituire. Ovviamente occorre tenere conto della differente 
lunghezza della stringa sostituita e di quella sostituente per il calcolo 
degli indici nella String e nello StringBuffer, i quali differiranno dopo la 
prima sostituzione.
----------------[Start Source]----------
public static String replace (String original, String find, String replace) 
{
  if ( (original == null) || (find == null) || (replace == null) ) {
    return original;
  }
  StringBuffer originalBuffer = new StringBuffer(original);
  int replaced = 0, difference;
  int findLength = find.length();
  int replaceLength = replace.length();
  int differenceLength = findLength-replaceLength;
  int index = original.indexOf(find, 0);
  while (index != -1) {
    difference = index-replaced*differenceLength;
    originalBuffer.replace(difference, difference+findLength, replace);
    replaced++;
    index = original.indexOf(find, index + findLength);
  } // while
  return originalBuffer.toString();
} // replace
------------[End Source]---------------
Qualche idea per testarne la bonta' rispetto a quelli gia proposti e a 
quelli 
che si spera verranno proposti in seguito?
Un saluto
  Lucio Benfante


>===== Original Message From Lucio Benfante <benf...@MailAndNews.com> =====

....scusate...solo una piccola ottimizzazione eliminando il prodotto 
all'interno del ciclo while.
-----------[Start Source]..........
public static String replace (String original, String find, String replace) 
{
  if ( (original == null) || (find == null) || (replace == null) ) {
    return original;
  }
  StringBuffer originalBuffer = new StringBuffer(original);
  int findLength = find.length();
  int differenceLength = findLength-replace.length();
  int difference = 0;
  int indexBuffer;
  int index = original.indexOf(find, 0);
  while (index != -1) {
    indexBuffer = index-difference;
    originalBuffer.replace(indexBuffer, indexBuffer+findLength, replace);
    difference += differenceLength;
    index = original.indexOf(find, index + findLength);
  } // while
  return originalBuffer.toString();
} // replace
---------------[End Source]----------


 Esso utilizza ora uno StringBuffer, sfruttando il suo metodo replace(). 
E' stato aggiunto in Java2?
Nel jdk1.1.8 stringBuffer.replace() non esiste (io devo utilizzare ancora 
quello)...
Ciao,
Tatiana

 la versione StringBuffer.
Infatti, intendevo utilizzare lo StringBuffer all'interno del metodo, non 
nella dichiarazione. Purtroppo, avevo poco tempo e non avevo la 
possibilita' di proporre il mio codice in alternativa.
Una delle "poche" cose assodate (almeno per jdk1.1.x) e' che la 
concatenazione delle stringhe e' molto meno performante rispetto a quella 
di StringBuffer. Per cui e' sicuramente meglio
StringBuffer sb = new StringBuffer();
while (...) {
  sb.append(...);
}
String str = sb.toString();
piuttosto che
String str = "";
while (...) {
  str += ...;
}
Ciao,
Tatiana

 =====

Ciao,
si' in effetti e' stato aggiunto a partire dalla versione 1.2...col jdk 1.1 
e' 
un po' piu' complicato...e temo anche meno performante, dato che e' 
praticamente impossibile manipolare solamente lo StringBuffer, senza creare 
oggetti di tipo String in qualche fase dell'algoritmo.
Un'alternativa migliore e' forse quella di gestirsi in proprio un buffer 
come 
array di char, come nel seguente esempio:
----------[Start Source]----------
public static String replace (String original, String find, String replace)
  {
    if ( (original == null) || (find == null) || (replace == null) ) {
      return original;
    }
    StringBuffer originalBuffer = new StringBuffer(original);
    int originalBufferLength = originalBuffer.length();
    int findLength = find.length();
    int replaceLength = replace.length();
    int differenceLength = findLength-replaceLength;
    int difference = 0;
    int indexBuffer;
    int index = original.indexOf(find, 0);
    while (index != -1) {
      indexBuffer = index-difference;
      char[] replacedBuffer = new 
char[originalBufferLength-differenceLength];
      originalBuffer.getChars(0, indexBuffer, replacedBuffer, 0);
      replace.getChars(0, replaceLength, replacedBuffer, indexBuffer);
      originalBuffer.getChars(indexBuffer+findLength, originalBufferLength, 
replacedBuffer, indexBuffer+replaceLength);
      originalBuffer.setLength(0);
      originalBuffer.append(replacedBuffer);
      originalBufferLength = replacedBuffer.length;
      difference += differenceLength;
      index = original.indexOf(find, index + findLength);
    } // while
    return originalBuffer.toString();
} // replace
----------[End Source]---------
Un soluzione migliore potrebbe richiedere una classe di supporto, in modo 
che 
non ci sia bisogno di allocare ogni volta l'array di caratteri, ma esso 
venga 
allocato all'inizio sufficientemente grande ed espanso solo se necessario.
Un saluto
  Lucio


Suvvia, U., non mi/ci puoi tenere così sulle spine... fuori il rospo! :P
Solo ora riesco a postare! 
Certo, lo StringBuffer è sicuramente una scelta molto migliore xchè la
differenza di prestazioni è evidente. Attenzione xò alle troppe
ottimizzazioni: in effetti bisognerebbe utilizzare un profiler ed
individuare i colli di bottiglia con esso. Con una VM
superottimizzante infatti non puoi mai sapere esattamente cosa
cercherà di ottimizzare, e qualche cosa in effetti potrebbe anche
essere controproducente. E' vero che lo StringBuffer è evidente
(esiste apposta x questo motivo), ma ovviamente l'assunzione che
stiamo facendo è che oggetti StringReplacer siano usati molto
"pesantemente" da altri oggetti, altrimenti ogni ottimizzazione
sarebbe in ogni caso prematura.
Le altre ottimizzazioni di cui si è discusso mi sembrano abbastanza
"ago nel pagliaio" e sarebbero giustificate solo se in un particolare
contesto il Profiler ci dicesse che il collo di bottiglia sono i 20
StringReplacer istanziati da quel client multithread ecc ecc.
Rimane, mi sembra, solo una piccola questione di stile.
ciao!


 come array di char, come nel seguente esempio:
[cut]
Questa soluzione non potrebbe essere un'alternativa un po' piu' semplice?
---------------------------------------------- source code
  /**
   * Esegue la sostituzione di tutte le occorrenze di una sottostringa
   * con un'altra sottostringa all'interno di una stringa
   * @param   original   stringa originale
   * @param   find       sottostringa da sostituire
   * @param   replace    nuova sottostringa
   * @return  stringa risultante dopo la sostituzione
   */
  public static String replace (String original, 
                                String find, 
                                String replace) {
    if ( original == null ) {
      return null;
    }
    if ( (find == null) || (replace == null) ) {
      return original;
    }
    int from = 0;
    int to = original.indexOf(find);
    int findLength = find.length();
    StringBuffer result = new StringBuffer();
    while ( to >= 0 ) {
      result.append(original.substring(from, to)).append(replace);
      from = to+findLength;
      to = original.indexOf(find, to+findLength);
    } // while
    return result.append(original.substring(from)).toString();
  } // replace
----------------------------------------------------------
Ciao,
Tatiana