Choinki, czyli DRY in action

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

package pl.koziolekweb.blog.choinki;

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 < i; e++) {
			System.out.println(new Rzad(0, 1));
		}

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

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

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

		System.out.println("--------------");
		for (int e = i; 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 < iostream>
 
using namespace std;
 
char znak ='*';

void rzad(int spacje, int znaki){
   for(int i =0; i < spacje; i++){
     cout << " ";
   }
   for(int i = 0; i < znaki; i++){
     cout << znak;
   }
   cout << "" << endl;
}

int main(int argc,char **argv) {    
 
    int i = 5;

    rzad(4,1) ;

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

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

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

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

    return 0;
}

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

8 myśli na temat “Choinki, czyli DRY in action

  1. Ech, nasuwa mi się pythonowe

    def rzad(spacje, znaki):
    print „%s%s” % (’ ’ * spacje, '*’ * znaki)

  2. Jak już trzymamy się DRY to w wersji C powinno być

    String rzad(int spacje, int znaki);

    Walnięcie tam cout’a, to tak jak użycie System.out w klasie Rzad.

  3. @machu nie kombinuj. To zadanie ma wypisywać gwiazdki i wypisuje, a są wykładowcy, którzy jak nie widzą sysouta w pętli to cię nie zaliczą. Zresztą chodzi mi od kliku dni po głowie tekst o nauczaniu informatyki i generalnie o tym jak powinien wyglądać program na uczelni. Na razie jednak zostala ostatnia część SOLID do zrobienia.

  4. Tak, tylko że to są totalnie różne języki, a podany kod to C++…

Napisz odpowiedź

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax