Techit 4th 2nd

Huisu·2023년 5월 9일
0

Techit

목록 보기
1/42
post-thumbnail

Algorithm

Constructor Pyramid

피라미드를 만들 때 각 행에 따른 공백의 개수와 피라미드로 찍힐 별의 개수를 파악하는 것이 중요하다. i번째 행에서 별의 개수는 2 * i + 1의 등차 수열을 따르고, i번째 행에서 공백의 개수는 높이 (heignt) - i 개이다. 공백과 별 숫자에 대한 파악이 끝났으면 공백 대신 다양한 문자를 넣어 피라미드를 만들 수 있도록 생성자를 활용하고자 한다. 멤버 변수로 spaceChar를 선언하고 생성자 파라미터로 문자열을 받아 설정해 주면 객체지향적인 피라미드를 만들 수 있다.

생성자란?
Class와 이름이 똑같은 메소드로 new를 할 때 실행되어 class instance를 생성해 준다. 매개변수로 값을 받아 멤버 변수의 값을 초기화할 수 있다. 생성자를 굳이 손으로 기입하지 않았을 경우에는 자바가 기본 생성자를 만들어서 사용한다. 기본 생성자는 코드로는 보이지 않지만 내가 코드로 생성자를 따로 만들지 않았다면 기본 생성자가 만들어져 있는 것이다. 매개 변수의 조합을 다르게 해서 생성자의 버전을 여러 개 만들 수도 있다.

package week4.day2;

public class Pyramid2 {
    private String spaceChar = " ";

    public Pyramid2(String spaceChar) {
        this.spaceChar = spaceChar;
    }

    public void printPyramid(int height) {
        for (int i = 0; i < height; i++) {
            System.out.printf("%s%s\n", spaceChar.repeat(height - i - 1), "*".repeat(2 * i + 1));
        }
    }

    public static void main(String[] args) {
        Pyramid2 pyramid2 = new Pyramid2(" ");
        pyramid2.printPyramid(4);
        Pyramid2 pyramidZero = new Pyramid2("0");
        pyramidZero.printPyramid(4);
    }
}
   *
  ***
 *****
*******
000*
00***
0*****
*******

한 메소드에 모든 파라미터를 사용하여 생성자 없이도 구현할 수 있다. 아래 코드가 그 예시이다. 하지만 이 경우는 print 기능, 1줄 만드는 기능, 반복하는 기능으로 세 개의 기능이 있다. 한 메소드 안에 여러 기능이 종합적으로 내포돼 있다면 확장성을 고려했을 때 좋은 코드라고 할 수 없다.

public void printPyramidWithSpaceChar(int height, String spaceChar) {
   // 기능이 3가지
   for (int i = 0; i < height; i++) {
       System.out.printf("%s%s\n", spaceChar.repeat(height - i - 1), "*".repeat(2 * i + 1));
   }
}

1줄만 출력하는 기능을 분리한 메소드도 만들어 볼 수 있다. 코드는 다음과 같다.

public String makeALine(String spaceChar, int height, int i) {
        return String.format("%s%s\n", spaceChar.repeat(height - i - 1), "*".repeat(2 * i + 1));
    }

Loosely Coupling
OOP로 나아가는 과정
1. main() 만들기
2. 핵심 로직 만들기
3. 메소드 분리
강결합이 되어 있는 기능들을 분리하여 느슨한 결합 형태로 바꾸기

Constructor Reverse Pyramid

공백과 별의 개수를 활용하여 뒤집어진 모양의 피라미드도 만들 수 있다. 이때도 위와 같이 생성자를 활용한다.

public class ReversePyramid {
    private String spaceChar = "";
    
		public ReversePyramid(String spaceChar) {
        this.spaceChar = spaceChar;
    }
    
		public String makeALine(int height, int i) {
        return String.format("%s%s\n", spaceChar.repeat(i), "*".repeat(height + 3 - (i * 2)));
    }
    
		public static void main(String[] args) {
        int height = 4;
        ReversePyramid reversePyramid = new ReversePyramid(" ");
        for (int i = 0; i < height; i++) {
            System.out.println(reversePyramid.makeALine(height, i));
        }
        ReversePyramid reversePyramidZero = new ReversePyramid("0");
        for (int i = 0; i < height; i++) {
            System.out.println(reversePyramidZero.makeALine(height, i));
        }
    }
}
*******

 *****

  ***

   *

*******

0*****

00***

000*

JAVA

Static

main 메소드에서 멤버 변수를 접근하려고 하면 접근되지 않는다. 그 이유는 main method는 프로그램이 실행될 때 스택 영역에 메모리를 할당받는다. 하지만 멤버 변수는 new 를 사용해서 객체가 만들어질 때 힙 영역에 메모리를 할당받는다. 이 시기가 다르기 때문에 main을 만들었을 때 멤버 변수는 메모리가 없는 것이나 마찬가지라 접근이 불가능하다. 이 문제를 해결하기 위해서는 멤버 변수에 static을 선언하여 해당 멤버 변수가 프로그램이 만들어질 때 스택 영역에 생성되도록 하면 된다.

public class ReversePyramid {
    private static String spaceChar = "";
    
		public ReversePyramid(String spaceChar) {
        this.spaceChar = spaceChar;
    }
    
		public String makeALine(int height, int i) {
        return String.format("%s%s\n", spaceChar.repeat(i), "*".repeat(height + 3 - (i * 2)));
    }
    
		public static void main(String[] args) {
        int height = 4;
        ReversePyramid reversePyramid = new ReversePyramid(" ");
        for (int i = 0; i < height; i++) {
            System.out.println(reversePyramid.makeALine(height, i));
        }
        ReversePyramid reversePyramidZero = new ReversePyramid("0");
        for (int i = 0; i < height; i++) {
            System.out.println(reversePyramidZero.makeALine(height, i));
        }
    }
}

메소드 역시도 static으로 선언하면 객체를 생성할 필요 없이 접근제어자를 활용해 메소드를 호출할 수 있다.

public class Pyramid {
    private String spaceChar = " ";
    public Pyramid2(String spaceChar) {
        this.spaceChar = spaceChar;
    }
    public void printPyramid(int height) {
        for (int i = 0; i < height; i++) {
            System.out.printf("%s%s\n", spaceChar.repeat(height - i - 1), "*".repeat(2 * i + 1));
        }
    }

    public static String makeALine(String spaceChar, int height, int i) {
        return String.format("%s%s\n", spaceChar.repeat(height - i - 1), "*".repeat(2 * i + 1));
    }
}
public class CallStaticMethod {
    public static void main(String[] args) {
        int h = 4;
        for (int i = 0; i < h; i++) {
            String line = Pyramid2.makeALine(" ", h, i);
            System.out.println(line);
        }
    }
}

Multiplication Refactoring

곱하기 기호는 *, x, X 등이 있다. 사용자가 원하는 곱하기 기능을 멤버 변수로 선언하여 생성자를 이용해 인스턴스마다 초기화하고, 이를 출력하는 구구단을 리팩토링하면 다음과 같다.

public class MultiplicationTable {
    private String multipleSymbol;
    public MultiplicationTable(String multipleSymbol) {
        this.multipleSymbol = multipleSymbol;
    }
    public void printDan(int dan) {
        for (int i = 1; i <= 9; i++) {
            System.out.printf("%d %s %d = %d\n", dan, multipleSymbol, i, dan * i);
        }
        System.out.println("------------");
    }
    public static void main(String[] args) {
        MultiplicationTable mt = new MultiplicationTable("X");
        mt.printDan(3);
    }
}

Sugar (Codeup 1098)

문제
부모님과 함께 유원지에 놀러간 영일이는 설탕과자(설탕을 녹여 물고기 등의 모양을 만든 것) 뽑기를 보게 되었다.길이가 다른 몇 개의 막대를 바둑판과 같은 격자판에 놓는데, 막대에 있는 설탕과자 이름 아래에 있는 번호를 뽑으면 설탕과자를 가져가는 게임이었다. 격자판의 세로(h), 가로(w), 막대의 개수(n), 각 막대의 길이(l), 막대를 놓는 방향(d:가로는 0, 세로는 1)과 막대를 놓는 막대의 가장 왼쪽 또는 위쪽의 위치(x, y)가 주어질 때, 격자판을 채운 막대의 모양을 출력하는 프로그램을 만들어 보자.

입력
첫 줄에 격자판의 세로(h), 가로(w) 가 공백을 두고 입력되고, 두 번째 줄에 놓을 수 있는 막대의 개수(n)세 번째 줄부터 각 막대의 길이(l), 방향(d), 좌표(x, y)가 입력된다. 입력값의 정의역은 다음과 같다.
1 <= w, h <= 100
1 <= n <= 10
d = 0 or 1
1 <= x <= 100-h
1 <= y <= 100-w

출력
모든 막대를 놓은 격자판의 상태를 출력한다. 막대에 의해 가려진 경우 1, 아닌 경우 0으로 출력한다.단, 각 숫자는 공백으로 구분하여 출력한다.

판 크기에 맞춰서 배열을 생성하는 기능, 막대를 방향에 맞춰 놓는 기능, 배열을 출력하는 기능으로 총 세 개의 기능이 필요하다. 추후에 어떤 기능에서 오류가 나는 건지 디버깅을 쉽게 하기 위해서는, 이렇게 기능마다 메소드를 나눠 생성하는 것이 편리하다. 아래는 객체지향의 방식으로 짠 설탕 과자 문제 풀이이다.

import java.util.Arrays;

public class Sugar {
    private int[][] arr;
    public Sugar(int rowCnt, int colCnt) {
        this.arr = new int[rowCnt][colCnt];
    }
    public void setBeam(int length, int direction, int y, int x) {
        if(direction == 0) { // 가로
            for (int i = 0; i < length; i++) {
                arr[y - 1][x - 1 + i] = 1;
            }
        }
        else { // 세로
            for (int i = 0; i < length; i++) {
                arr[y - 1 + i][x - 1] = 1;
            }
        }

    }

    public void printArr() {
        for (int i = 0; i < arr.length; i++) {
            System.out.println(Arrays.toString(arr[i]));
        }
        System.out.println("---------");
    }

    public static void main(String[] args) {
        int rowCnt = 5;
        int colCnt = 5;
        Sugar sugar = new Sugar(rowCnt, colCnt);
        sugar.printArr();
        sugar.setBeam(2, 0, 1, 1);
        sugar.printArr();
        sugar.setBeam(3,1, 2, 3);
        sugar.printArr();
    }

}
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
---------
[1, 1, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
---------
[1, 1, 0, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 1, 0, 0]
[0, 0, 0, 0, 0]
---------

Constructor Overloading

Overloading
이름은 같지만 시그니처는 다른 메소드를 중복으로 선언 할 수 있는 방법을 메소드 오버로딩(overloading)이라고 한다. 결론적으로 말하면 메소드 오버로딩은 매개변수를 사용한다. 즉 매개변수의 조합이 다르면 이름이 같아도 서로 다른 메소드가 되는 것이다.

public class ConstructorOverloading {
    private int[][] arr;

    public ConstructorOverloading() {
        this.arr = new int[5][5];
    }

    public ConstructorOverloading(int rowCnt) {
        this.arr = new int[rowCnt][5];
    }

    public ConstructorOverloading(int rowCnt, int colCnt) {
        this.arr = new int[rowCnt][colCnt];
    }

    public void printArr() {
        for (int i = 0; i < arr.length; i++) {
            System.out.println(Arrays.toString(arr[i]));
        }
        System.out.println("---------");
    }

    public static void main(String[] args) {
        int rowCnt = 8;
        int colCnt = 8;
        ConstructorOverloading one = new ConstructorOverloading();
        one.printArr();
        ConstructorOverloading two = new ConstructorOverloading(rowCnt);
        two.printArr();
        ConstructorOverloading three = new ConstructorOverloading(rowCnt, colCnt);
        three.printArr();
    }
}
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
---------
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
[0, 0, 0, 0, 0]
---------
[0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0]
---------

객체 one은 매개변수가 아무것도 없는 생성자에 의해 만들어지고, 객체 two는 매개변수가 rowCnt 하나만 있는 생성자에 의해 만들어지고, 객체 three는 매개변수 rowCnt, colCnt, 두 개가 있는 생성자에 의해 만들어진다.

This
Class 자체를 가리키는 문법이다. 만약 멤버 변수로도 arr가 있고 파라미터로도 arr가 있다면 이 둘을 구분하는 말이 필요하다. 이때 this.arr 라는 접근제어자를 사용하고 이는 클래스의 멤버 변수를 가리킨다. 프로그래밍할 때 대체로 생성자 메소드는 이름을 같게 쓰기 때문에 이를 자주 활용한다.

Abstract Class

Abstract Class
추상 메소드는 기능을 직접 구현하지 않고, 접근제어자 + 리턴 타입 + 메소드 이름 + 파라미터 조합만 선언한 메소드이다. 이 추상 메소드를 하나라도 가지고 있으면 추상 클래스라고 하며, abstract 키워드를 작성해야 한다. 이를 템플릿 메소드 패턴이라고 한다. 선언하는 곳 따로, 구현하는 곳 따로 작성하는 개발 패턴이다.

다음과 같이 추상 클래스를 정의할 수 있다. 추상 클래스 자체를 사용할 수는 없고 추상 클래스는 반드시 구현해서 사용해야 한다. 추상 클래스를 작성했다는 것 자체가 구체적인 구현체가 존재한다는 것을 의미하기 때문이다. 피라미드 찍기와 평행사변형 찍기를 예를 들어 보자. 둘 다 높이만큼 string을 출력하는 것은 동일하기 때문에 이를 printShape 메소드로 한 번만 구현해 둔 뒤, 구체적인 출력 내용은 추상 클래스로 선언한다.

public abstract class ShapeDrawer {
    public void printShape(int height) {
        for (int i = 0; i < height; i++) {
            System.out.print(makeALine(height, i));
        }
    }
    public abstract String makeALine(int height, int i);
}

이후 피라미드 별 찍기에서 구체적으로 어떤 라인을 리턴할 것인지 구현체를 다시 작성할 수 있다. 추상 클래스에 있는 추상 메소드인 makeALine을 새로 정의하는 것이기 때문에 extends 키워드를 사용한다.

public class PyramidShapeDrawer extends ShapeDrawer{
    @Override
    public String makeALine(int h, int i) {
        return String.format("%s%s\n", " ".repeat(h - i - 1), "*".repeat(2 * i + 1));
    }
}

같은 방식으로 평행사변형을 만드는 클래스도 생성할 수 있다.

public class ParallelogramShapeDrawer extends ShapeDrawer{
    @Override
    public String makeALine(int h, int i) {
        return String.format("%s%s\n", " ".repeat(h - i - 1), "*".repeat(h));
    }
}

추상 클래스에 틀만 짜여져 있는 추상 메소드를 구체적인 내용으로 새로 정의하는 과정을 override라고 한다. 라인을 그린다는 makeALine을 실제로 어떤 라인으로 그리는지 PyramidShapeDrawer와 ParallelogramShapeDrawer 클래스에서 구체적인 정의를 내린 과정이 이에 해당한다. 객체를 만들어서 실행하면 다음과 같다.

public class ShapeDrawerTest {
    public static void main(String[] args) {
        PyramidShapeDrawer pyramidShapeDrawer = new PyramidShapeDrawer();
        pyramidShapeDrawer.printShape(4);
        ParallelogramShapeDrawer parallelogramShapeDrawer = new ParallelogramShapeDrawer();
        parallelogramShapeDrawer.printShape(4);
    }
}
   *
  ***
 *****
*******
   ****
  ****
 ****
****

Inversion of Context

앞서 구현한 추상 클래스 - 추상 클래스를 구체화한 클래스들을 다른 클래스에서 멤버 변수로 사용할 때, 주로 구체화되지 않은 추상 클래스를 사용한다. 그 이유로는 실제 코드에서 구체화된 어떤 클래스를 사용할지 모르기 때문이다. 예를 들자면 ShapeDrawer 중에 피라미드 별 찍기와 평행사변형 중 어떤 것을 할지 모르겠지만 두 개를 모두 포함하고 있는 클래스가 추상 클래스이기 때문이다. DiEx처럼 DiEx에서는 반복되는 활동들만 돌아가고, 실제로 별을 찍고 평행사변형을 그리는 일은 ShapeDrawer에서 돌아갈 때, 이 경우를 제어의 역전인 Inversion of Context라고 한다.

public class DiEx {
    private ShapeDrawer shapeDrawer;
    public DiEx(ShapeDrawer shapeDrawer) {
        this.shapeDrawer = shapeDrawer;
    }
    public void doSth() {
        shapeDrawer.printShape(5);
    }
}
public class DiExTest {
    public static void main(String[] args) {
        DiEx diEx = new DiEx(new PyramidShapeDrawer());
        diEx.doSth();
    }
}

0개의 댓글