Od pewnego czasu na forum przewija się temat „choinek”, czyli wypisywania za pomocą pętli różnych prostych znaków na przykład:

  *
 **
***

*
**
***

  *
 ***
*****

Zadanie te są banalne, ale w bardzo prosty sposób pozwalają na ilustrację jednej z fundamentalnych zasad dobrego programowania (nie tylko obiektowego), czyli DRY – Don’t Repeat Yourself. Zadania te pozwalają też na szybkie wyłowienie ludzi, którzy intuicyjnie potrafią tworzyć dobry kod, czy inaczej rzecz ujmując jednostki leniwe acz myślące.

Sama zasada DRY mówi nam, że kod powtarzający się w co najmniej dwóch miejscach należy wyłączyć tak by nie było konieczności wprowadzania zmian w więcej niż w jednym miejscu. Generalnie sposób realizacji tej zasady jest uzależniony od tego co chcemy wyciągnąć. Choinki są najprostszym przykładem. Bardziej skomplikowanym przykładem jest wzorzec metody szablonowej wzbogacony o odpowiednio rozbudowane dziedziczenie. Jeszcze innym jest modularyzacja kodu. Bardziej zaawansowane metody są jednak trudne do wytłumaczenia osobom, które jeszcze nie za bardzo łapią o co chodzi w programowaniu.

Magia choinek, poza ilustracją DRY, polega też na bardzo szybkim odsianiu osób, które na informatykę trafiają przez przypadek i nie mają opanowanych podstaw warsztatu. Jednak po kolei.

Problem

Uproszczę trochę zagadnienie ograniczając się do pięciu figur.

  • Wypisanie N znaków ‘*’ w jednym rzędzie za pomocą pętli
  • Wypisanie N znaków w N rzędach jeden pod drugim
  • Wypisanie N znaków w N rzędach po skosie w prawo
  • Wypisanie N znaków w N rzędach po skosie w lewo
  • Wypisanie w N znaków tak, że w pierwszym jest jeden odsunięty o N-1 spacji od lewej krawędzi ekranu, a w każdym następnym jeden więcej tak, że w ostatnim rzędzie jest N znaków zaczynających się przy krawędzi (trójkąt)

Rozwiązanie typowe

W klasycznym rozwiązaniu tego problemu typowy student, który nie myśli będzie powtarzał kod. Stworzy zagnieżdżone pętle, które będą wykorzystywać różne operacje arytmetyczne na indeksach. Ogólnie rzecz biorąc 99,9% wykładowców będzie zadowolona z takiego rozwiązania. Jest to jednak postawa nie słuszna. Jeżeli było by to zadanie oceniane w skali 2-5 to student oddający taki program nie powinien otrzymać oceny wyższej niż 3. Na 3,5 zasługuje student, który będzie umiał ładnie opisać kod.

Tak niska ocena wynika z faktu, że problem choć został rozwiązany to metoda postępowania jest bardzo toporna. Nie ma w niej ani krzty mózgu, nie ma analizy problemu i co najważniejsze całość nie jest elastyczna. Kod wam zaoszczędzę ponieważ bądźmy szczerzy on boli.

Rozwiązanie w duchu DRY

Jeżeli jednak chcemy wykonać zadanie dobrze i zaskoczyć wykładowcę to należy użyć odpowiedniego warsztatu. W pierwszym kroku należy przeanalizować sposób w jaki wypisywane są znaki. Schemat zawsze jest taki sam. Na początku jest X spacji, po czym następuje Y znaków, a co do zasady X+Y=N, gdzie N jest liczbą rzędów. Takie coś aż prosi się o wyniesienie do osobnej metody, a w skrajnym przypadku do osobnej klasy:

Listing 1. Klasa Rzad

public class Rzad {

	private static final String znak = "*";

	private int znaki;

	private int spacje;

	private String linia;

	public Rzad(int spacje, int znaki) {
		this.spacje = spacje;
		this.znaki = znaki;
	}

	@Override
	public String toString() {
		if (linia == null) {
			linia = "";
			for (int i = 0; i < spacje; i++) {
				linia += " ";
			}

			for (int i = 0; i < znaki; i++) {
				linia += znak;
			}
		}
		return linia;
	}
}

I tu w praktyce koniec. Wypisanie dowolnej figury to tylko odpowiednie odszukanie algorytmów wiążących licznik pętli z ilością spacji i znaków. Oczywiście nie zawsze da się to ładnie połączyć. Czasami wymagane jest wiele pętli następujących po sobie, a czasami wyzbycie się pętli na rzecz na przykład rekurencji. Oczywiście nie byłbym sobą gdybym nie wrzucił tu rozwiązania:

Listing 2. Rozwiązanie zadania

package pl.koziolekweb.blog.choinki;

public class Choinka {

	public static void main(String[] args) {

		int i = 5;

		for (int e = 0; e  0; e--) {
			System.out.println(new Rzad(e, 1));
		}

		System.out.println("--------------");
		for (int e = 0; e  0; e--) {
			System.out.println(new Rzad(e, i - e + 1));
		}

	}
}

Żeby nie było wersja w C:

Listing 3. Rozwiązanie zadania w C

#include 
 
using namespace std;
 
char znak ='*';

void rzad(int spacje, int znaki){
   for(int i =0; i  0; e--) {
        rzad(e, 1);
    }

    for (int e = 0; e  0; e--) {
      rzad(e, i - e + 1);
    }

    return 0;
}

I tak oto niepostrzeżenie na Koziołekweb dostało się programowanie w C…

Artykuły są dostępne na licencji CC-BY.
</br> Jeżeli spodobał ci się ten wpis, to podziel się nim z innymi lub wesprzyj autora. </div> ```