c# Solid원칙

JHO·2024년 7월 24일
0

c#스터디

목록 보기
4/9

1. 객체지향


1-1. 객체지향구현이란?

  • 모든 대상을 객체로 나누고, 객체의 행동과 고유한 값을 정의하여 설계하는 방법.
  • 캡슐화, 상속, 추상화, 다형성의 특징을 가지고 있음.
  • 객체가 또 다른 객체를 호출하거나 상호작용하는 방식으로 수행
    ex) 클래스가 다른 클래스를 호출.
    -> 일련의 단계를 순차대로 수행하는 절차지향 구현과는 반대되는 개념.

1-2. 장점

  • 효율적인 관리 및 성능 향상.
  • 전체적인 소프트웨어 복잡성 감소.
  • 테스트 및 수정 용이

1-3. 효과적인 객체지향구현을 위해서 어떻게 해야하는가?

  • 모듈의 응집도를 높이고 모듈 간 결합도를 낮춘다!
    -> 모듈이란?
    ㅇ 독립된 하나의 소프트웨어 또는 하드웨어.
    객체지향구현에서 모듈은 하나의 객체를 의미하기도 함. ex) 클래스
  • 응집도: 모듈 내부에서 구성요소 간에 밀접한 관계를 맺고 있는 정도
  • 결합도: 모듈과 모듈간의 관련성/의존 정도
    -> 즉, 응집도를 높이면 모듈 내부가 견고해지고, 결합도를 낮추면 모듈의 독립성이 강해져 결과적으로 확장 및 유지보수가 용이.

2. SOLID원칙


2-1. SOLID

  • 이러한 객체지향의 4가지 특징을 활용하면서 효과적이고 체계적으로 프로그래밍을 하기위해서 준수해야하는 5가지 원칙.

2-2. 5가지 원칙

  1. 단일 책임 원칙
  • 하나의 클래스는 하나의 기능만 책임진다.
  1. 개방 폐쇄 원칙
  • 확장에는 열려있고, 수정에는 닫혀있다
    -> 기존의 코드는 변경하지 않으면서 기능을 추가할 수 있어야함.
  1. 리스코프 치환원칙
  • 자식클래스는 언제나 자신의 부모클래스를 대체할 수 있어야 한다.
    -> 부모클래스를 대신해 자식클래스로 치환을 해도 우리가 의도하는 대로 구현이 되어야 한다.
    -> 기본 클래스의 방향성을 유지!
  1. 인터페이스 분리원칙
  • 인터페이스를 최소 단위로 분리하고, 사용하지 않는 인터페이스에 의존을 하지 않는 원칙
    -> 필요한 인터페이스만을 사용.
  1. 의존성 역전 원칙
  • 자주 변화하는 것보단 변화가 없는 것에 의존해야하는 원칙.
  • 하이레벨 클래스는 로우레벨 클래스 구현에 의존해서는 안된다.
    -> 구체적 클래스가 추상화에 의존하도록함.(역순x)
    -> 구체적 클래스보단 인터페이스나 추상클래스에 의존관계를 맺자.

3. 단일 책임 원칙


  • 하나의 클래스는 하나의 기능만 책임진다.

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. 개방 폐쇄 원칙


  • 확장에는 열려있고, 수정에는 닫혀있다.
    -> 기존의 코드는 변경하지 않으면서 기능을 추가할 수 있어야함.

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.

5. 리스코프 치환 원칙


  • 자식클래스는 부모클래스를 대체할 수 있어야 한다.
    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. 인스터페이스 분리 원칙


  • 인터페이스를 최소 단위로 분리하고, 사용하지 않는 인터페이스에 의존을 하지 않는 원칙

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 인터페이스를 더 작은 단위로 나눔.
  • 프로그래머와 기획자는 각각 필요한 인터페이스만 상속받아 구현.

7. 의존성 역전 원칙


  • 자주 변화하는 것보단 변화가 없는 것에 의존해야하는 원칙.
  • 하이레벨 클래스는 로우레벨 클래스 구현에 의존해서는 안된다.
    -> 구체적 클래스가 추상화에 의존하도록함.(역순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.번외


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관계 : 인터페이스와의 관계
profile
개발노트

0개의 댓글