Dla orientacji: można mieć pewność że wszystko co pisane jest czcionką taką jak ten wstęp, istnieje tylko i wyłącznie w tych notatkach (odwrotnie się to jednak nie sprawdza - niektórych niewyróżnionych informacji w slajdach też nie będzie)
Zwłaszcza w pierwszej części można znaleźć dużo linków do przykładów refaktoryzacji prostych, których ze względów na ograniczony czas prezentacji, w slajdach nie umieściłem.
Tyle tytułem wstępu, jeśli powyższy tekst Cie nie przestraszył to zapraszam do ,,lektury"
Jednoczesnie pragne zaznaczyc, iż material zamieszczony w rozdziale 3 "Trudności napotykane podczas refaktoryzacji Javy", stanowi streszczenie fragmentu pracy magisterskiej p. Adama Kieżuna (link na dole strony)
Adam Mazur
Ładnie to widać w matyce - mając liczbę 6432 można ja reprezentować jako 32 * 3 * 67. Refaktoryzując ja do 8 * 12 * 67 otrzymamy dokładnie ten sam produkt (6432) ale o innej strukturze.
Ułatwia on budowę i pielęgnację oprogramowania, umożliwiając utrzymanie systemu komputerowego w stanie pozwalającym na łatwą rozbudowę.
Poza tym refaktoryzacja jest znacznie tansza niz przepisywanie kodu od nowa
void C::cos() {
for(int i; i
zmieniamy na:
void C::wylosuj() {
for(int i; i<a.length(); i++)
a[i] = rand();
}
void C::cos() {
wylosuj();
wypisz();
}
int discount( int inputVal ) {
if( inputVal > 50 ) inputVal -= 2;
...
przechodzi na:
int discount( int inputVal ) {
int result = inputVal;
if( inputVal > 50 ) result -= 2;
...
class Order...
double price() {
double primaryBasePrice;
double secondaryBasePrice;
double tertiaryBasePrice;
// long computation;
...
}
View>>...
....
area := bounds bottom - bounds top * (bounds right - bounds left).
....
stanie się:
Rectangle>>area
^self bottom - self top * (self right . self left)
View>>...
....
area := bounds area.
....
(ponoć opłaca się to już przy dwóch odwołaniach do pól innej klasy)
Date newStart = new Date( previousEnd.getYear(), previousEnd.getMonth(), previousEnd.getDate() + 1;
przechodzi w:
Date newStart = nextDay(previousEnd);
private static Date nextDay( Date arg ) {
return new Date( arg.getYear(), arg.getMonth(), arg.getDate() + 1 );
}
if ( (x==0) || ((y<17) && name == null) ) {
..
}
przejdzie na:
final boolean outOfActiveAreal = (x==0) || ((y<17) && name == null) ;
if (outOfActiveAreal ) {
..
}
double temp = 2 * (_height + _width);
System.out.println (temp);
temp = _height * _width;
System.out.println (temp);
a lepiej tak:
final double perimeter = 2 * (_height + _width);
System.out.println (perimeter);
final double area = _height * _width;
System.out.println (area);
rsObject.Open ...
If rsObject.RecordCount = 0 Then
rsObject.AddNew
rsObject!PK = value
End If
rsObject!Attr = value
to...
With rsObject
.Open ...
If .RecordCount = 0 Then
.AddNew
!PK = value
End If
!Attr = value
End With
With rsObject
.Open ...
If .RecordCount? = 0 Then
.AddNew?
!PK = value
End If
!Attr = value
End With
to:
rsObject.Open ...
If rsObject.RecordCount? = 0 Then
rsObject.AddNew?
rsObject!PK = value
End If
rsObject!Attr = value
For i = 1 to max
array[i-1] = otherarray[i-1] + factor * (i-1)
' other stuff using "i-1" a lot...
Next i
przechodzi na:
For i = 0 to max-1
array[i] = otherarray[i] + factor * (i)
' other stuff using "i" a lot...
Next i
For i = 1 to 10
If isFound(i) Then Exit For
Next i
przechodzi na:
ysnFound = False
i = 0
While (not ysnFound) And (i < 10)
i = i + 1
ysnFound = isFound(i)
Wend
if ( !isSummer( date ) )
charge = winterCharge( quantity );
else
charge = summerCharge( quantity );
przechodzi na:
if ( isSummer( date ) )
charge = summerCharge( quantity );
else
charge = winterCharge( quantity );
func() {
if (cond1) {
f1();
if (cond2) {
f2();
if (cond3) {
f3();
if (cond4) {
f4();
if (cond5) {
do_the_main_point_at_last();
}}}}}
}
mozna zamienic na
func() {
if (!cond1) return;
f1();
if (!cond2) return;
f2();
if (!cond3) return;
f3();
if (!cond4) return;
f4();
if (!cond5) return;
do_the_main_point_at_last();
}
Dim dblTotal As Double
dblTotal = 0#
For k = 1 To MaxK
For j = 1 To MaxJ
For i = 1 To MaxI
dblTotal = dblTotal + arrValues(i, j, k)
Next i
Next j
Next k
' Use "dblTotal" here...
...can safely be changed to...
Dim dblTotal As Double
dblTotal = 0#
For i = 1 To MaxI
For j = 1 To MaxJ
For k = 1 To MaxK
dblTotal = dblTotal + arrValues(i, j, k)
Next k
Next j
Next i
' Use "dblTotal" here...
interface I1{
public void m();
}
interface I2{
public void m();
}
class A implements I1, I2{
public void m(){}
}
Zmiana nazwy metody I1::m() musi pociągać za sobą zmianę nazwy A::m() - w przeciwnym
razie powstanie błąd kompilacji. Zauważmy także, że również nazwa I2::m() musi ulec zmianie.
interface I{
public void m();
}
class A{
public void m(){}
}
class B extends A implements I{
}
Po zmianie nazwy metody I::m() również nazwa metody A::m() musi zostać zmieniona.
interface I{
public void m();
}
class A{
private void m(){}
}
class B extends A implements I{
public void m() {}
}
Zmianie nazwy metody I::m() musi towarzyszyć zmiana nazwy metody B::m(),
natomiast metoda A::m() może pozostać nieprzemianowana.
"A.java"
package p1;
public class A{}
"B.java"
package p2;
public class B{}
"Test.java"
package test;
import p1.A;
import p2.B;
class Test{
B b;
}
Zmiana nazwy typu A na B (wraz z aktualizacją odniesień) daje w rezultacie błąd kompilacji
w ostatniej jednostce kompilacji - która importuje wówczas (import pojedynczego typu) dwa
typy o tej samej nazwie prostej (tj. B)
"A.java"
package p;
public class A{}
"Test.java"
package test;
import java.util.*; //zawiera typ List
import p.*;
class Test{
A a;
List l; /*1*/
}
Jeśli zmienimy nazwę typu A na List, to odniesienie w wierszu oznaczonym przez /*1*/ stanie
się niejednoznaczne, co spowoduje błąd kompilacji
"B.java"
package p1;
public class B{
public static int x= 42;
}
"A.java"
package p;
public class A{
public static int x= 0;
}
"Test.java"
package p;
import p1.B;
class Test{
static int i;
static {
i= A.x;
}
}
zmieniając nazwe p1.B na A, działanie programu ulega zmianie (bez wystąpienia błędów kompilacji)
przez przesłonięcie widoczności typu zdefiniowanego w tym samym pakiecie, ale innej jednostce kompilacji.
Jeśli jedynie zmienimy nazwę typu p1.B na A (wraz z aktualizacją wszystkich odniesień do tego typu), to pole i w klasie Test zostanie zainicjalizowane wartością 42, a nie 0, jak poprzednio. Pomimo zmiany w działaniu programu, pozostaje on poprawny, tzn. nie powstają błędy kompilacji. Zauważmy, że działanie programu się zmienia, mimo, że w klasie Test nie ma odniesień do typu p1.B, którego nazwę zmieniamy. Importowanie typu (i związane z tym przesłanianie) wystarcza, by w istotny sposób zmienić funkcjonowanie programu.
package p;
import java.util.*; // zawiera klase ArrayList
class A{
public Object m(){
return new ArrayList();
}
}
Samo stworzenie w pakiecie p klasy o nazwie ArrayList spowoduje,
że metoda A::m() przekaże obiekt klasy innej niż dotychczas, zmieniając w
ten sposób działanie programu (bez powodowania błędów kompilacji).
import java.util.*; //zawiera typ Stack
class A{
class B{
Object m(){
return new Stack{};
}
}
}
Zmiana nazwy klasy dowolnej z klas A lub B na Stack spowoduje zmianę działania programu - bez powstania błędów kompilacji.
Każda z podanych nazw może być przesłaniana (lub zaciemniana) niezależnie od innych. Podczas refaktoryzacji musimy, po pierwsze, odszukać je wszystkie jako odniesienia to danego typu, a po drugie analizować każdą z nich oddzielnie. Zmiana nazwy lub położenia typu, dodanie nowego typu do systemu oraz wszelkie zmiany w hierarchiach dziedziczenia, czy zawierania) to kilka przykładów refaktoryzacji, które mogą być niepoprawne, jeśli nie zostaną poprzedzone analizą przypadków przedstawionych w tym punkcie (tych a'propo przeslaniania typow zagniezdzonych).
package p;
public class O{
public class I{
public class J{}
J j; /**/
}
I.J ij; /**/
}
class O1 extends O{
class O2 extends O.I{
O2(O o){ o.super(); }
}
O2.J o2j; /**/
}
class Test{
O.I.J oij; /**/
p.O.I.J poij; /**/
O1.I.J o1ij; /**/
p.O1.I.J po1ij; /**/
O1.O2.J o1o2j; /**/
p.O1.O2.J po1o2j; /**/
}
package p;
class X{}
class Test{
Object m(){
class A{};
return new X(); /*1*/
}
}
class S{
protected int g;
}
class A extends S{
public int m(int p){
return g + p;
}
}
zmiana nazwy pola g na p powoduje, że jego deklaracja zostaje przesłonięta przez nazwę parametru w treści metody A::m()
class A{
protected int f;
}
class B extends A{
void m(){
f= 42; /*1*/
}
}
po dodaniu do klasy B nowego pola typu int, o nazwie f,
należałoby wiersz oznaczony przez /*1*/ zmienić na: ((A)this).f= 42; /*1*/
class X {
static int length(){
return 42;
};
}
class Test {
String s= "hello";
int m(){
return X.length(); /*1*/
}
}
Zmiana nazwy typu X na s nie powoduje błędów kompilacji, a działanie programu zostanie zmienione.
class Foo{
void a(String s){}
void b(Object s){}
}
...
void cos( Foo f ) {
f.a("hello");
f.b("hello");
}
Zmiana nazwy metody Foo::b na Foo::a powoduje, że wczesniejsze wywołanie b("hello")
musi zostać zastąpione przez a((Object)"hello").
class Foo{
void a(String s){}
void a(Object s){}
}
...
void cos( Foo f ) {
f.a("hello");
f.a((Object)"hello");
f.a( f );
}
Zmieniając nazwe metody Foo::a(Object) na Foo::b, musimy dla każdego wołania
przeciążonej metody Foo::a badać typ przekazywanego parametru
(może się zdarzyć że przekazywany parametr bedzie niejawnie rzutowany na typu parametru, lub z niego dziedziczy)
W specyfikacji języka napisano ([JLS2 9.2]), że:
,,jeżeli interfejs nie ma żadnych bezpośrednich nadinterfejsów, wówczas niejawnie deklaruje
publiczną, abstrakcyjną metodę m o sygnaturze s, typie wyniku r oraz klauzuli throws t
odpowiadającą każdej z
publicznych metod instancyjnych m o sygnaturze s, typie wyniku r oraz klauzuli throws t
zadeklarowanych w klasie java.lang.Object, chyba, że
metoda o tej samej sygnaturze, typie wyniku i odpowiadającej klauzuli throws
jest jawnie zadeklarowana w tym interfejsie.''
Wynika z tego, że zgłaszany jest błąd kompilacji, jeżeli interfejs deklaruje metodę o tej samej
sygnaturze i odmiennym typie wyniku bądź nieodpowiadającej klauzuli throws.
z tej reguły wynika pewna liczba ograniczeń dla niektórych refaktoryzacji
o.getClass().getMethod("m", null).invoke(o)
Zmiana nazwy metody m spowoduje, że efektem wykonania podanego fragmentu kodu będzie zgłoszenie wyjątku