Clean Code - instrukcja `switch`, wyjaśnienie polimorfizmu

1

Czytam Czysty Kod Roberta C. Martina i nie rozumiem jednej części dotyczącej korzystania z polimorfizmu zamiast instrukcji switch.

Mając klasę Payroll.java

public Money calculatePay(Employee e)
throws InvalidEmployeeType {
	switch (e.type) {
		case COMMISSIONED:
			return calculateComissionedPay(e);
		case HOURLY:
			return calculateHourlyPay(e);
		case SALARIED:
			return calculateSalariedPay(e);
		default:
			throw new InvalidEmployeeType(e.type);

W treści odnajdujemy taki fragment:

Z tą funkcją wiąże się wiele problemów. Po pierwsze jest duża, a po dodaniu kolejnego typu pracownika urośnie jeszcze bardziej. Po drugie jasne jest, że wykonuje jeszcze jedną operację. Po trzecie narusza zasadę pojedynczej odpowiedzialności (SRP), ponieważ istnieje więcej niż jeden powód uzasadniający zmianę. Po czwarte, narusza zasadę otwarty-zamknięty (OCP), ponieważ musi się zmieniać przy każdym dodaniu nowego typu. ...

Autor proponuje takie rozwiązanie:

public abstract class Employee {
	public abstract boolean isPayday();
	public abstract Money calculatePay();
	public abstract void deliverPay(Money pay);
}
----------------------
public interface EmployeeFactory {
	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
----------------------
public class EmployeeFactoryImpl implements EmployeeFactory {
	public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
		switch (r.type) {
		case COMMISSIONED:
			return new CommissionedEmployee(r);
		case HOURLY:
			return new HourlyEmployee(r);
		case SALARIED:
			return new SalariedEmployee(r);
		default:
			throw new InvalidEmployeeType(r.type);
		}
	}
}

Moje pytanie odnosi się właśnie czwartego punktu, czyli zasady otwarty-zamknięty. Jak to, co zaproponował autor ma zapewnić nienaruszanie zasady OCP skoro, przy każdym nowym typie (Next)Employee trzeba otworzyć klasę EmployeeFactoryImpl i edytować metodę makeEmployee?

3

Po tej zmianie ta pierwotna funkcja w Payroll.java będzie pewnie wyglądać tak:

public Money calculatePay(Employee e) {
    return e.calculatePay();
}

i nie będzie się zmieniać przy dodaniu kolejnych typów. A to że fabryka będzie się zmieniać to niejako konieczność, bo gdzieś musi być zawarta informacja o typach. Ale powiesz "no to jaki jest zysk skoro zamiast zmieniać w jednym miejscu będę zmieniać w innym?". Zysk jest taki, że przy drugim podejściu nadal zmiana jest w 1 miejscu nawet gdy masz 10 czy 20 funkcji takich jak Payroll.calculatePay(), a w pierwszym w każdej z tych funkcji trzeba zmienić kod. No i oczywiści SRP.

3

@tworek dobrze napisał.

Od siebie dodam, że ta fabryka też nie musi być oparta na konstrukcji switch-case. To jest najprostsze, ale jej implementację też można zamknąć.

Chociażby tak:

interface EmployeeProducer {
    Employee makeEmployee(EmployeeRecord record);
}

public final class EmployeeFactory implements EmployeeProducer {
    private final Map<EmployeeRecordType, EmployeeProducer> producers = new HashMap<>();
    
    public void register(EmployeeRecordType employeeType, EmployeeProducer producer) {
        producers.put(employeeType, producer);
    }

    @Override
    public Employee makeEmployee(EmployeeRecord record)  {
        EmployeeProducer producer = producers.get(record.type);
        if (producer != null) {
            return producer.makeEmployee(record);
        }
        throw new InvalidEmployeeType(record.type);
    }
}

I wtedy gdzieś z zewnątrz "wstrzykujemy" do fabryki producenta, niejako rejestrując nowy typ pracownika. Np.:

factory.register(
        EmployeeRecordType.DRUNKARD,
        new EmployeeProducer() {
            @Override
            public Employee makeEmployee(EmployeeRecord record) {
                return new DrunkardEmployee(record);
            }
        });

Albo, jeśli mamy szczęście korzystać z bardziej współczesnej wersji Javy, z użyciem lambdy:

factory.register(EmployeeRecordType.DRUNKARD, record -> new DrunkardEmployee(record)); 
// czy nawet, jeśli dobrze pamiętam, record -> DrunkardEmployee::new;

Ot, i wszystko. Nie musimy wtedy w ogóle rozpruwać kodu fabryki - możemy rozwijać nasz kod inkrementacyjnie, wstrzykując do niej nowego producenta obiektów Employee skądkolwiek nam pasuje.

0

Jeszcze tylko jedno pytanie - po co nam interfejs skoro wiemy (autor wie) dokładnie co chcemy napisać? (Pomijając ewentualną lambdę, książka jest chyba za stara na tego typu zabiegi)

0

@V-2: Nie wiem, próbowałem sobie to rozpisać, ale jakoś wzorce fabryki abstrakcyjnej mnie przerastają...
Nawet testu nie umiałbym do tego napisać :/

0

Może powinieneś więcej eksperymentować z kodem i zajmować się praktyką, bo jadąc na samej teorii (nawet tak dobrej jak "Czysty kod") szybko odczujesz ograniczenia tego podejścia.

1 użytkowników online, w tym zalogowanych: 0, gości: 1