Swift의 확고한 원칙: 단일 책임 원칙

이번 주에 s.O.L.I.D. 원칙을 수정하고 첫 번째 원칙을 깊이 있게 이해하자. 또한 가장 유명한 원칙인 단일 책임이나 SRP일 수도 있다.
이 원칙은 하나의 종류는 반드시 하나가 있어야 하며, 단지 하나의 이유만 바뀌어야 한다고 규정하고 있다.나는 이 정의가 일부 사람들에게 약간 추상적일 수 있다고 생각한다.이렇게 생각한다: 한 대상이 바뀐 이유가 하나밖에 없어야 한다(이것도 도움이 되지 않는다)😭) 아니면**한 반에 일거리가 하나밖에 없을 거예요**(더 좋아요)😁) 혹은 결국 한 종류는 한 가지 책임만 져야 한다.
이 원칙을 위반하면 종류가 더욱 복잡해지고 테스트와 유지보수가 더욱 어려워질 수 있다.그러나 가장 도전적인 부분은 여러 가지 원인이 바뀌어야 하는지, 아니면 책임이 많은지 보는 것이다.

죄를 지었습니다.
iOS 세계의 예를 들어 보기 컨트롤러가 한 가지 직책만 있는 것이 아니라는 것을 알 수 있다.많은 젊은 개발자들이 iOS 개발자가 된 첫날 저지른 오류다.보통 MVC 구조에서 컨트롤러는 우리가 관련되지 않은 많은 것을 던지는 곳이다. 왜냐하면 나는 특정 컨트롤러와 관련된 모든 코드를 보기 쉽고 편리하기 때문이다.
final class LoginViewController: UIViewController {

  private var emailTextField: UITextField!
  private var passwordTextField: UITextField!
  private var submitButton: UIButton!

  // initializers...

  override func viewDidLoad() {
    super.viewDidLoad()
    setupView()
  }

  private func setupView() {
    // ... other view related code here
    submitButton.addTarget(self, action: #selector(submitButtonTapped), for: .touchUpInside)
  }

  @objc private func submitButtonTapped() {
    signinUser(email: emailTextField.text ?? "", password: passwordTextField.text ?? "")
  }

}
이것은 간단한 직접 로그인 화면으로 전자 우편과 비밀번호 필드, 제출 단추 두 개의 텍스트 필드가 있다.버튼을 눌렀을 때, 우리는 사용자를 로그인시키려고 시도했다.우리가 다른 방법을 추가하기 전에, 이것은 완전히 정확한 것 같다.
이러한 확장에 적용할 것입니다.
extension LoginViewController {

  // 1
  private func signinUser(email: String, password: String) {
    let url = URL(string: "https://my-api.com")!
    let json = ["email": email, "password": password]
    let jsonData = try! JSONSerialization.data(withJSONObject: json, options: [])
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.httpBody = jsonData
    request.addValue("application/json", forHTTPHeaderField: "Content-Type")
    request.addValue("application/json", forHTTPHeaderField: "Accept")

    let task = URLSession.shared.dataTask(with: request)  { (data, response, error) in
      if let error = error {
        DispatchQueue.main.async {
          self.showErrorAlert(message: error.localizedDescription)
        }
      }
      guard let data = data else {
        self.showErrorAlert(message: "sorry, could not log in, try later")
        return
      }
      // 2
      let user = try! JSONDecoder().decode(User.self, from: data)
      self.log(user: user)
      DispatchQueue.main.async {
        self.showWelcomeMessage(user: user)
      }
    }
    task.resume()
  }

  private func showErrorAlert(message: String) {
    // logic to show an error alert
  }

  private func showWelcomeMessage(user: User) {
    // logic to show a welcome message
  }

  // 3
  private func log(user: User) {
    // log user logic
  }
}
우리는 컨트롤러가 SRP를 위반한 것을 볼 수 있다. 왜냐하면 우리는 매우 다른 일을 처리하기 위해 몇 가지 방법을 짰기 때문이다.
  • signinUser 메소드는 네트워크 호출을 담당하고 사용자가 로그인하도록 시도
  • 여전히 signinUser에서 API에서 호출된 데이터를 도메인 대상으로 변환하려고 시도했습니다. 예시
  • 에서 User
  • 사용자의 정보를 원격 서비스에 기록할 수 있는 log 방법이 있습니다.
  • 이러한 방법과 조작 코드를 LoginViewController류에 넣는 것을 통해 우리는 이 참여자 중 하나하나를 다른 참여자에게 결합시켰다. 이제 우리는 컨트롤러가 하나의 직책만 있는 것이 아니라는 것을 알 수 있다.만약 우리가 애플의 문서를 참고한다면, 보기 컨트롤러의 주요 직책은 다음과 같다.
  • 보기의 내용을 업데이트하는 것은 보통 기초 데이터의 변경에 응답하기 위한 것이다.
  • 보기를 통해 사용자의 상호작용에 응답한다.
  • 뷰 크기를 조정하고 전체 인터페이스의 레이아웃을 관리합니다.
  • 응용 프로그램의 다른 보기 컨트롤러를 포함한 다른 대상과 조율한다.
    이제 상황을 개선하고 SRP를 존중하는 방법을 살펴보겠습니다.

  • 재구성: 해결의 길
    우리가 클래스가 SRP를 존중하기를 원할 때, 이것은 일반적으로 우리가 단일한 직책을 가진 추가 대상을 만들고 서로 다른 기술을 사용하여 서로 통신해야 한다는 것을 의미한다.우리의 예에서 어떻게 이것을 응용하는지 봅시다.
    첫 번째 작업은 단독 대상에 대한 API 요청 호출의 논리를 추출하는 것입니다.
    struct APIClient {
    
      func load(from request: URLRequest, completionHandler: @escaping (Result<Data, Error>) -> ()) {
        let task = URLSession.shared.dataTask(with: request) { (data, response, error) in
          if let error = error {
            return completionHandler(.failure(error))
          }
          completionHandler(.success(data ?? Data()))
        }
        task.resume()
      }
    
    }
    
    이렇게 하면 우리는 현재 전문적인 대상이 하나 있는데, 이것은 API 호출만 책임지고, 다른 것은 아무것도 책임지지 않는다는 것이다.좋습니다. API에서 온 데이터를 역 대상에 디코딩하는 데 사용할 다른 대상을 추가합니다.
    struct Decoder<A> where A: Decodable {
    
      func decode(from data: Data) throws -> A {
        do {
          let object = try JSONDecoder().decode(A.self, from: data)
          return object
        } catch {
          fatalError(error.localizedDescription)
        }
      }
    
    }
    
    마찬가지로, 우리는 매우 간단한 대상이 하나 있는데, 단지 하나의 임무만 맡는다. 데이터를 역 대상으로 디코딩하는 것이다.
    이제 사용자를 기록하는 마지막 대상을 추가합니다.
    protocol Loggable {
      var infos: String { get }
    }
    
    struct Logger<A> where A: Loggable {
    
      func log(object: A) {
        print("doing some logging stuff with \(object.infos)")
      }
    
    }
    
    이 예에서 모든 대상은 하나의 방법이 있다.물론 많은 것들이 있을 수 있지만, 관건은 하나의 종류나 구조에 하나의 방법이 있다는 것이다. 만약 당신이 더 많은 것을 추가한다면, 당신이 추가하고자 하는 방법이 이 종류에 속하는지 스스로에게 물어보는 것이다.
    우리가 특정 임무를 맡을 다른 대상이 생겼으니 다음 문제는 그것을 어떻게 연결할 것인가 하는 것이다.여기에는 몇 가지 솔루션이 있지만 iOS 세계의 두 가지 일반적인 솔루션은 다음과 같습니다.
  • 구조 함수 주입
  • 을 통해 모든 대상을 주입LoginViewController
  • View 모델 클래스를 만들고 이 클래스는 APlClient, DecoderLogger 대상의 실례를 저장한 다음LoginViewController 구조 함수를 통해 View 모델을 주입한다.
    나는 이전 해결 방안이 더 좋은 선택이라는 것을 발견했다. 따라서 보기 컨트롤러는 네트워크 호출, 디코딩, 기타 내용을 모른다. SwiftUI가 도래함에 따라 우리는 이런 모델을 대량으로 사용하는 경향이 있다.우리는 이런 물건이 있을 것이다.
  • class LoginViewModel {
    
      private var logger: Logger<User>
      private var apiClient: APIClient
      private var decoder: Decoder<User>
    
      init(logger: Logger<User>, apiClient: APIClient, decoder: Decoder<User>) {
        self.logger = logger
        self.apiClient = apiClient
        self.decoder = decoder
      }
    
      func signin(email: String, password: String, completionHandler: @escaping (Result<User, Error>)->()) {
        let url = URL(string: "https://my-api.com")!
        let json = ["email": email, "password": password]
        let jsonData = try! JSONSerialization.data(withJSONObject: json, options: [])
        var request = URLRequest(url: url)
        request.httpMethod = "POST"
        request.httpBody = jsonData
        request.addValue("application/json", forHTTPHeaderField: "Content-Type")
        request.addValue("application/json", forHTTPHeaderField: "Accept")
    
        apiClient.load(from: request) { response in
          switch response {
            case .success(let data):
              let user = try! self.decoder.decode(from: data)
              self.logger.log(object: user)
              completionHandler(.success(user))
            case .failure(let error):
              completionHandler(.failure(error))
          }
        }
    
      }
    
    }
    
    현재 우리는 LoginViewController 클래스에서 이 보기 모델을 사용하여 사용자 로그인을 처리할 수 있습니다.
    final class LoginViewController: UIViewController {
    
      private var emailTextField: UITextField!
      private var passwordTextField: UITextField!
      private var submitButton: UIButton!
    
      private let viewModel: LoginViewModel
    
      init(viewModel: LoginViewModel) {
        self.viewModel = viewModel
        super.init(nibName: nil, bundle: nil)
      }
    
      required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
      }
    
      override func viewDidLoad() {
        super.viewDidLoad()
        setupView()
      }
    
      private func setupView() {
        // ... other view related code here
        submitButton.addTarget(self, action: #selector(submitButtonTapped), for: .touchUpInside)
      }
    
      @objc private func submitButtonTapped() {
        viewModel.signin(email: emailTextField.text ?? "",
                         password: passwordTextField.text ?? "") { response in
          switch response {
            case .success(let user): self.showWelcomeMessage(user: user)
            case .failure(let error): self.showErrorAlert(message: error.localizedDescription)
          }
        }
      }
    
      private func showErrorAlert(message: String) {
        DispatchQueue.main.async {
          // logic to show an error alert
        }
      }
    
      private func showWelcomeMessage(user: User) {
        DispatchQueue.main.async {
          // logic to show a welcome message
        }
      }
    
    }
    
    나는 너의 상황을 모르겠지만, 나는 이것이 더욱 깨끗한 코드라고 생각한다.컨트롤러는 아무것도 모른다.그는 단지 사용자의 입력을 처리하고 응답할 뿐이다.우리는 이제 느슨하게 결합되어 유지보수와 테스트가 더욱 쉬운 코드가 생겼다.이것은 모두 s.O.L.I.D의 원칙에 관한 것이다. 보상으로 우리의 보기 컨트롤러는 더 이상 비대하지 않다(우리는 iOS에서 우스갯소리가 있는데 MVC는Massive view controller를 대표한다고 한다)😅).

    결론
    SRP의 핵심은 모든 종류가 자신의 책임을 져야 한다는 것이다. 또는 다시 말하면, 모든 종류가 바뀌어야 하는 이유가 있어야 한다.클래스가 변경되어야 하는 여러 사용자와 여러 가지 원인을 식별하기 시작하면, 그 중 일부 논리를 전용 클래스에 추출해야 할 수도 있습니다.그러나 앞에서 말한 바와 같이 이 원칙의 가장 도전적인 부분은 대상의 경계를 이해하거나 대상이 언제 여러 가지 책임이나 이유를 바꾸기 시작할지 결정하는 것이다.이것은 실천과 끊임없는 반성이 필요하다.정확한 방향으로 전진하기 위해서 우리는 항상 자신에게 정확한 문제를 물어야 한다.다음 주에 우리는 개폐 원칙을 배울 것이다.그 전에 즐거운 한 주 보내시고, 원력이 함께 하시기를 바랍니다.👊.

    좋은 웹페이지 즐겨찾기