1-1. 객체지향구현이란?
- 모든 대상을 객체로 나누고, 객체의 행동과 고유한 값을 정의하여 설계하는 방법.
- 캡슐화, 상속, 추상화, 다형성의 특징을 가지고 있음.
- 객체가 또 다른 객체를 호출하거나 상호작용하는 방식으로 수행
ex) 클래스가 다른 클래스를 호출.
-> 일련의 단계를 순차대로 수행하는 절차지향 구현과는 반대되는 개념.1-2. 장점
- 효율적인 관리 및 성능 향상.
- 전체적인 소프트웨어 복잡성 감소.
- 테스트 및 수정 용이
1-3. 효과적인 객체지향구현을 위해서 어떻게 해야하는가?
- 모듈의 응집도를 높이고 모듈 간 결합도를 낮춘다!
-> 모듈이란?
ㅇ 독립된 하나의 소프트웨어 또는 하드웨어.
ㅇ 객체지향구현에서 모듈은 하나의 객체를 의미하기도 함. ex) 클래스- 응집도: 모듈 내부에서 구성요소 간에 밀접한 관계를 맺고 있는 정도
- 결합도: 모듈과 모듈간의 관련성/의존 정도
-> 즉, 응집도를 높이면 모듈 내부가 견고해지고, 결합도를 낮추면 모듈의 독립성이 강해져 결과적으로 확장 및 유지보수가 용이.
2-1. SOLID
- 이러한 객체지향의 4가지 특징을 활용하면서 효과적이고 체계적으로 프로그래밍을 하기위해서 준수해야하는 5가지 원칙.
2-2. 5가지 원칙
- 단일 책임 원칙
- 하나의 클래스는 하나의 기능만 책임진다.
- 개방 폐쇄 원칙
- 확장에는 열려있고, 수정에는 닫혀있다
-> 기존의 코드는 변경하지 않으면서 기능을 추가할 수 있어야함.
- 리스코프 치환원칙
- 자식클래스는 언제나 자신의 부모클래스를 대체할 수 있어야 한다.
-> 부모클래스를 대신해 자식클래스로 치환을 해도 우리가 의도하는 대로 구현이 되어야 한다.
-> 기본 클래스의 방향성을 유지!
- 인터페이스 분리원칙
- 인터페이스를 최소 단위로 분리하고, 사용하지 않는 인터페이스에 의존을 하지 않는 원칙
-> 필요한 인터페이스만을 사용.
- 의존성 역전 원칙
- 자주 변화하는 것보단 변화가 없는 것에 의존해야하는 원칙.
- 하이레벨 클래스는 로우레벨 클래스 구현에 의존해서는 안된다.
-> 구체적 클래스가 추상화에 의존하도록함.(역순x)
-> 구체적 클래스보단 인터페이스나 추상클래스에 의존관계를 맺자.
- 하나의 클래스는 하나의 기능만 책임진다.
3-1. 안좋은 예시
//안좋은 예시 class Human { //사람관련 기능 public void Eat() { Console.WriteLine("먹다"); } public void Sleep() { Console.WriteLine("자다"); } public void Walk() { Console.WriteLine("걷다"); } //프로그래머관련 기능 public void Develop() { Console.WriteLine("개발하다"); } public void ReFactoring() { Console.WriteLine("재개발하다"); public void Maintain() { Console.WriteLine("유지하다"); } }
- Human이라는 클래스안에 일반 사람과 프로그래머 관련 기능을 포함.
-> 하나의 클래스에 두가지 이상의 기능이 존재하기 때문에 단일 책임 원칙에 위배!3-2. 좋은 예시
//좋은 예시 class Human { //사람관련 기능 public void Eat() { Console.WriteLine("먹다"); } public void Sleep() { Console.WriteLine("자다"); } public void Walk() { Console.WriteLine("걷다"); } } class Programmer { //프로그래머관련 기능 public void Develop() { Console.WriteLine("개발하다"); } public void ReFactoring() { Console.WriteLine("재개발하다"); public void Maintain() { Console.WriteLine("유지하다"); } }
- 사람과 프로그래머 관련 기능을 다른 클래스로 분류하여, 하나의 클래스가 하나의 기능만 책임지도록 함.
- 확장에는 열려있고, 수정에는 닫혀있다.
-> 기존의 코드는 변경하지 않으면서 기능을 추가할 수 있어야함.4-1. 안좋은 예시
public class Human { public void Introduce(Programmer programmer, string name) { Console.WriteLine($"내이름은 {name}이고, 프로그래머다"); } } public class Programmer { public string name; } public class Designer { public string name; }
- 프로그래머 뿐만아니라 디자이너의 이름을 소개를 하고 싶다면,
Human 클래스 안에 Designer 클래스를 매개 변수로 받는 Introduce함수를 생성하거나
기존의 Introduce함수에 조건을 넣어 수정해야함.
-> 새로운 직업을 가진 클래스가 늘어날수록, Human 클래스를 계속 수정해야하기 때문에 개방 폐쇄 원칙에 위배!4-2. 좋은 예시
public abstract class Human { protected string name; public abstract void Introduce(); } public class Programmer: Human { public Programmer(string name) { this.name = name; } public override void Introduce(){ Console.WriteLine($"내이름은 {name}이고, 프로그래머다"); } } public class Designer: Human { public Designer(string name) { this.name = name; } public override void Introduce(){ Console.WriteLine($"내이름은 {name}이고, 디자이너다"); } }
- Human을 추상 클래스로 만들어, 하위클래스에서 Introdouce()함수를 새롭게 정의하도록 바꾸면(오버라이딩), 새로운 직업 클래스를 확장해도 기존의 Human 클래스는 수정할 필요x.
- 자식클래스는 부모클래스를 대체할 수 있어야 한다.
ex) (부모클래스 A 자식클래스B)
A a = new A(); -> A a = new B();
-> 부모클래스를 대신해 자식클래스로 치환을 해도 우리가 의도하는 대로 구현이 되어야 한다.
-> 기본 클래스의 방향성을 유지!5-1. 안좋은 예시
public class Game { public virtual void UseCLanguage() { Console.WriteLine("c#사용!"); } public virtual void DoMeeting() { Console.WriteLine("팀프로젝트 회의...!"); } } public class Programmer : Game { public override void UseCLanguage() { Console.WriteLine("c#으로 게임개발!"); } public override void DoMeeting() { Console.WriteLine("개발자 회의 참여했습니다!"); } } public class Designer : Game { //디자이너에게는 필요없어서 에러처리 public override void UseCLanguage() { throw new InvalidOperationException("할 줄 모르는데...?"); } public override void DoMeeting() { Console.WriteLine("기획자 회의 참여했습니다!"); } }
- Game 클래스 안에는 UseCLanguage()함수와 DoMeeting()함수가 존재.
- Programmer와 Designer는 Game을 상속받음.
- Programmer는 두 기능 모두 필요로 하지만, Designer 는 c언어를 사용할 줄 모르기 때문에 UseCLanguage() 기능이 필요x.
-> 따라서 이를 Designer 함수에서 에러처리 하게되면, Game클래스 또는 상속받은 자식클래스에서 원하는 의도와는 다른 동작을 함.
부모클래스인 Game Class의 방향성에 위배되어 리스코프 치환원칙에 위배!5-2. 좋은 예시
public class Game { public virtual void DoMeeting() { Console.WriteLine("팀프로젝트 회의...!"); } } public interface ILanguageUsable { public void UseCLanguage(); } public class Programmer : Game, ILanguageUsable { public override void DoMeeting() { Console.WriteLine("개발자 회의 참여했습니다!"); } public void UseCLanguage() { Console.WriteLine("c#으로 게임개발!"); } } public class Designer : Game { public override void DoMeeting() { Console.WriteLine("기획자 회의 참여했습니다!"); } }
- 인터페이스 ILanguageUsable을 따로 생성.
- UseCLanguage()함수를 인터페이스 내부로 옮겨 Programmer에게만 상속.
- Programmer와 Designer는 부모클래스 Game을 대체 하여도 문제없이 의도하는대로 구현된다.
- 인터페이스를 최소 단위로 분리하고, 사용하지 않는 인터페이스에 의존을 하지 않는 원칙
6-1. 안좋은 예시
public interface IGame { public void Meeting(); public void Develop(); public void Design(); } class Programmer: IGame { public void Meeting() { Console.WriteLine("회의!"); } public void Develop() { Console.WriteLine("개발!"); } public void Design() { Console.WriteLine("기획..도 내가 해야해?"); } } class Designer : IGame { public void Meeting() { Console.WriteLine("회의!"); } public void Develop() { Console.WriteLine("개발..도 내가 해야해?"); } public void Design() { Console.WriteLine("기획!"); } }
- 프로그래머와 디자이너 둘다 Game이라는 인터페이스를 상속받고 있다.
- Programmer는 디자인할 필요가 없고, Designer는 개발할 필요가 없다.
-> 사용하지 않는 인터페이스 함수들이 있기 때문에 이는 인터페이스 분리원칙에 위배!6-2. 좋은 예시
public interface IGame { public void Meeting(); } public interface ICanDevelop { public void Develop(); } public interface ICanDesign { public void Design(); } class Programmer: IGame, ICanDevelop { public void Meeting() { Console.WriteLine("회의!"); } public void Develop() { Console.WriteLine("개발!"); } } class Designer : IGame , ICanDesign { public void Meeting() { Console.WriteLine("회의!"); } public void Design() { Console.WriteLine("기획!"); } }
- Game 인터페이스를 더 작은 단위로 나눔.
- 프로그래머와 기획자는 각각 필요한 인터페이스만 상속받아 구현.
- 자주 변화하는 것보단 변화가 없는 것에 의존해야하는 원칙.
- 하이레벨 클래스는 로우레벨 클래스 구현에 의존해서는 안된다.
-> 구체적 클래스가 추상화에 의존하도록함.(역순x)
-> 구체적 클래스보단 인터페이스나 추상클래스에 의존관계를 맺자.7-1. 안좋은 예시
public class Human { public void Introduce(Programmer programmer, string name) { Console.WriteLine($"내이름은 {name}이고, 프로그래머다"); } } public class Programmer { public string name; } public class Designer { public string name; }
- 개방 폐쇄 원칙을 준수하지 않은 예시를 다시 가져옴.
- 이 코드를 다시보면 Human 클래스에서 Introuduce함수를 호출하려면 매개변수로 Programmer 클래스의 변수를 필요로 함.
- Human 클래스가 Programmer 클래스를 의존하는 형태를 띄게 됨.
-> 하이레벨 클래스 Human이 로우레벨 클래스 Programmer를 의존하기 때문에 의존성 역전 원칙에 위배.7-2. 좋은 예시
public abstract class Human { protected string name; public abstract void Introduce(); } public class Programmer: Human { public Programmer(string name) { this.name = name; } public override void Introduce(){ Console.WriteLine($"내이름은 {name}이고, 프로그래머다"); } } public class Designer: Human { public Designer(string name) { this.name = name; } public override voide Introduce(){ Console.WriteLine($"내이름은 {name}이고, 디자이너다"); } }
- 개방 폐쇄 원칙을 준수한 좋은 예시를 다시 가져옴.
- 이 또한 코드를 다시보면, Programmer와 Designer 클래스가 추상클래스인 Human을 상속 받으면서 구체적 클래스가 추상화에 의존하게 됨.
-> 더이상 하이레벨 클래스가 로우레벨 클래스를 의존하는 형태가 아니기 때문에 의존성 역전 원칙을 준수.
-> 주의할 점!!! 추상화할 이유가 없는 클래스들을 일부로 solid원칙에 준수한다고 무리하게 인터페이스나 추상클래스를 늘리는 것은 좋지 않다.
-> 특정 클래스를 상속 관계를 가지고 있는 클래스와 의존관계를 맺을 때, 상위 클래스랑 맺는 것이 좋다는 것이다!
8-1. 인터페이스 활용
- 7-2에서 좋은 예시로 보여준 코드를 보면 사람들 중에서도 이름을 밝히고 싶지않거나, 밝히면 안되는 직업을 가지는 사람이 있을 수 있다.
-> 인터페이스를 활용!public interface IIntroduce { public void Introduce(); } public abstract class Human { protected string name; } public class Programmer : Human, IIntroduce { public Programmer(string name) { this.name = name; } public void Introduce() { Console.WriteLine($"내이름은 {name}이고, 프로그래머다"); } } public class Designer : Human , IIntroduce { public Designer(string name) { this.name = name; } public void Introduce() { Console.WriteLine($"내이름은 {name}이고, 디자이너다"); } } public class SecretAgent : Human { public SecretAgent(string name) { this.name = name; } }
- 인터페이스를 만들어 Introduce를 따로 분리하면, 새로운 직업클래스를 추가하여 Human을 상속받아도 원하는 사람만 소개 가능.
- 하나의 클래스가 하나의 기능만 책임지고 -> 단일 책임 원칙 준수.
- 새로운 클래스를 만들어도 기존의 기능을 수정할 필요 x -> 개방 폐쇄 원칙 준수.
- 작은 단위로 나눠진 인터페이스나 추상클래스로부터 자식 클래스 각자 필요한 기능만을 상속받음.
-> 리스코프 치환원칙, 인터페이스 분리 원칙, 의존성 역전 원칙 준수.8-2. UML 다이어그램
- is A관계 : 추상클래스와의 관계
- has A관계 : 인터페이스와의 관계