깨끗한 건물.그물심

나는 최근에 코드 라이브러리 자동화 인코딩 표준의 중요성을 둘러싼 글을 썼다.
탭이든 빈칸이든 파일당 클래스 수든 주석의 구조든표준과 공통된 주제가 있을 때, 그것은 코드의 읽기 쉽도록 크게 향상시킨다.
내가 사용하는 표준 환매 협의here를 찾을 수 있다.이제 나는 이런 기준에서 한 걸음 더 나아가 청결 체계 구조 원칙에 대한 이해를 높였다.

청결 건축


클린 어치텍처는 밥 마틴 삼촌이 자신의 동명 저서에서 처음으로 제안한 소프트웨어를 설계하고 구축하는 방법이다.

이 사진은 인터넷에서 여러 번 반복되었지만, 응용 프로그램의 디자인 방식을 명확하게 정의했다.
모든 원은 내부 원의 어떤 정보만 알고 의존 관계를 밖으로 확대해서는 안 된다.
그렇다면 이것은 우리 소프트웨어에 무엇을 의미하는가?아래의 모든 예는 내 GitHub repo에서 찾을 수 있다

솔리드


실체는 상업 분야에서 가장 순수한 표현 형식이다.내가 현지 은행을 위해 대출을 관리하는 예시 응용 프로그램에서 실체는 다음과 같다.
using System;
using System.Collections.Generic;
using System.Text;
using CleanArchitecture.Entities.Exceptions;

namespace CleanArchitecture.Core.Entities
{
    /// <summary>
    /// Encapsulates all code for managing loans.
    /// </summary>
    public abstract class Loan
    {
        /// <summary>
        /// Initializes a new instance of the <see cref="Loan"/> class.
        /// </summary>
        /// <param name="principal">The principal valie of the loan.</param>
        /// <param name="rate">The annual interest rate percentage.</param>
        /// <param name="period">The term of the loan.</param>
        internal Loan(decimal principal, decimal rate, decimal period)
        {
            this.Principal = principal;
            this.Rate = rate;
            this.Period = period;
            this.Balance = principal;
        }

        /// <summary>
        /// Gets the standard late fee to be charged.
        /// </summary>
        public virtual decimal LateFee
        {
            get
            {
                return 125M;
            }
        }

        /// <summary>
        /// Gets the initial principal of the loan.
        /// </summary>
        public decimal Principal { get; private set; }

        /// <summary>
        /// Gets the interest rate of the loan.
        /// </summary>
        public decimal Rate { get; private set; }

        /// <summary>
        /// Gets the period the loan will be repayed over.
        /// </summary>
        public decimal Period { get; private set; }

        /// <summary>
        /// Gets the current balance of the loan.
        /// </summary>
        public decimal Balance { get; private set; }

        /// <summary>
        /// Make a payment against the loan.
        /// </summary>
        /// <param name="paymentAmount">The value of the payment made.</param>
        public virtual void MakePayment(decimal paymentAmount)
        {
            var newCalculatedBalance = this.Balance - paymentAmount;

            if (newCalculatedBalance < 0)
            {
                throw new LoanOverpaymentException($"A payment of {paymentAmount} would take the current loan balance below 0");
            }

            this.Balance = this.Balance - paymentAmount;
        }

        /// <summary>
        /// Apply the interest for the elapsed period.
        /// </summary>
        /// <returns>The total accrued interest value.</returns>
        public virtual decimal ApplyInterest()
        {
            this.Balance = this.Principal * (1 + (this.Rate / 100));

            return this.Balance - this.Principal;
        }

        /// <summary>
        /// Charge a late payment fee to this loan.
        /// </summary>
        /// <returns>The new balance after the late fee has been added.</returns>
        public virtual decimal ChargeLateFee()
        {
            this.Balance = this.Balance + this.LateFee;

            return this.Balance;
        }
    }
}
대출은 은행 부서가 합작하는 가장 기본적인 실체다.각 대출금:
  • 잔액에서 지불 가능
  • 이자신청가능
  • 체납금
  • 부과 가능
    개방/폐쇄 원칙에 따라 클래스는 추상적인 클래스로 만들어져 서로 다른 유형의 대출을 허용한다.구체적인 BasicLoan 클래스에서 하나의 예를 볼 수 있다
    using System;
    using System.Collections.Generic;
    using System.Text;
    
    namespace CleanArchitecture.Core.Entities
    {
        /// <summary>
        /// A basic loan implementation.
        /// </summary>
        public class BasicLoan : Loan
        {
            internal BasicLoan(decimal principal, decimal rate, decimal term)
                : base(principal, rate, term)
            {
            }
        }
    }
    
    기본 대출은 어떤 기본 대출 기능도 커버하지 않지만, 미래에는 확장될 가능성이 높다.
    은행에도 고객이 있어요.마찬가지로 그들의 표시 방식은 가능한 한 실제 업무 용례와 일치한다.업무가 실체에 사용하는 용어와 같은 용어를 사용하면 개발자와 분야 전문가 간의 의사소통이 매우 쉽다.
    using System;
    using System.Collections.Generic;
    using System.Runtime.CompilerServices;
    using System.Text;
    
    [assembly: InternalsVisibleTo("CleanArchitecture.UnitTest")]
    
    namespace CleanArchitecture.Core.Entities
    {
        /// <summary>
        /// Encapsulates all logic for a customer entity.
        /// </summary>
        public class Customer
        {
            private int _age;
    
            /// <summary>
            /// Initializes a new instance of the <see cref="Customer"/> class.
            /// </summary>
            /// <param name="name">The new customers name.</param>
            /// <param name="address">The new customers address.</param>
            /// <param name="dateOfBirth">The new customers date of birth.</param>
            /// <param name="nationalInsuranceNumber">The new customers national insurance number.</param>
            internal Customer(string name, string address, DateTime dateOfBirth, string nationalInsuranceNumber)
            {
                this.CustomerId = Guid.NewGuid().ToString();
                this.Name = name;
                this.Address = address;
                this.DateOfBirth = dateOfBirth;
                this.NationalInsuranceNumber = nationalInsuranceNumber;
            }
    
            private Customer()
            {
            }
    
            /// <summary>
            /// Gets the internal identifier of this customer.
            /// </summary>
            public string CustomerId { get; private set; }
    
            /// <summary>
            /// Gets the name of the customer.
            /// </summary>
            public string Name { get; private set; }
    
            /// <summary>
            /// Gets the address of the customer.
            /// </summary>
            public string Address { get; private set; }
    
            /// <summary>
            /// Gets the customers date of birth.
            /// </summary>
            public DateTime DateOfBirth { get; private set; }
    
            /// <summary>
            /// Gets the national insurance number of the customer.
            /// </summary>
            public string NationalInsuranceNumber { get; private set; }
    
            /// <summary>
            /// Gets the customers credit score.
            /// </summary>
            public decimal CreditScore { get; private set; }
    
            /// <summary>
            /// Gets the age of the person based on their <see cref="Customer.DateOfBirth"/>.
            /// </summary>
            public int Age
            {
                get
                {
                    if (this._age <= 0)
                    {
                        this._age = new DateTime(DateTime.Now.Subtract(this.DateOfBirth).Ticks).Year - 1;
                    }
    
                    return this._age;
                }
            }
    
            internal void UpdateCreditScore(decimal newCreditScore)
            {
                if (newCreditScore != this.CreditScore)
                {
                    this.CreditScore = newCreditScore;
                }
            }
        }
    }
    
    실체'권'에서 나는 고객 데이터베이스와 상호작용을 허용하는 인터페이스를 가지고 있다.
    청결 체계 구조의 가장 큰 수확 중 하나는 업무 논리와 용례를 가능한 한 세부 사항(데이터베이스 제공자, 사용자 상호작용)에서 멀리하는 것이다.
    그렇기 때문에 이런 실체들은 ICustomers 데이터 저장의 개념을 가지고 있지만 고객 데이터 저장의 실제 작업 방식을 진정으로 이해하거나 관심을 가지지 않는다.
    // ------------------------------------------------------------
    // Copyright (c) James Eastham.
    // ------------------------------------------------------------
    
    using System;
    using System.Collections.Generic;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace CleanArchitecture.Core.Entities
    {
        /// <summary>
        /// Encapsulates all persistance of customer records.
        /// </summary>
        public interface ICustomerDatabase
        {
            /// <summary>
            /// Store the customer object in the database.
            /// </summary>
            /// <param name="customer">The <see cref="Customer"/> to be stored.</param>
            public void Store(Customer customer);
        }
    }
    
    일단 우리가 실체를 그려내면, 우리는 바깥쪽으로 다음 원으로 이동할 수 있다.

    용례


    응용 프로그램의 실제 업무 사용을 예로 들다.만약에 실체가 핵심 업무 대상을 가지고 있다면 이러한 대상이 어떻게 협동하여 일을 하는지에 대한 논리를 가지고 있다.
    밥 삼촌이 잘했어.
    은행을 예로 들면 대출 이자의 계산은 실체에 속할 것이다. 왜냐하면 이것은 업무에 매우 중요한 규칙이기 때문이다. 이것은 관건적인 업무 규칙이다.
    그러나 특정 고객의 대출을 허용해야 하는지를 결정하는 시스템은 자동화에서 이득을 볼 수 있다.이것은 용례다.예를 들어 자동화 시스템의 사용 방식에 대한 설명으로 사용자가 제공한 입력과 사용자에게 되돌아오는 출력을 지정합니다."
    이 확실한 예를 기억해라. 고객이 대출을 받을 수 있는지 없는지를 검사하기 위해 용례를 추가하자.
    우선, 우리는 예상한 입력과 출력을 볼 것이다.
    은행과 한 분야의 전문가와의 대화에서 우리는 대출을 받을지 말지에 관건적인 결정이 있다는 것을 안다.
  • 고객은 유효한 이름, 주소 및 국민보험 번호가 있어야 합니다
  • 그들은 18세를 넘어야 한다
  • 신용 점수는 500점 이상이어야 합니다
  • .
    논리를 요청/응답 대상으로 변환합니다.
    // ------------------------------------------------------------
    // Copyright (c) James Eastham.
    // ------------------------------------------------------------
    
    using System;
    
    namespace CleanArchitecture.Core.Requests
    {
        /// <summary>
        /// Gather required data for a new loan.
        /// </summary>
        protected class GatherContactInfoRequest
            : IRequest<GatherContactInfoResponse>
        {
            /// <summary>
            /// Initializes a new instance of the <see cref="GatherContactInfoRequest"/> class.
            /// </summary>
            /// <param name="name">The applicants name.</param>
            /// <param name="address">The applicants address.</param>
            /// <param name="dateOfBirth">The applicants date of birth.</param>
            /// <param name="nationalInsuranceNumber">The applicants NI number.</param>
            public GatherContactInfoRequest(string name, string address, DateTime dateOfBirth, string nationalInsuranceNumber)
            {
                this.Name = name;
                this.Address = address;
                this.DateOfBirth = dateOfBirth;
                this.NationalInsuranceNumber = nationalInsuranceNumber;
            }
    
            /// <summary>
            /// Gets the name.
            /// </summary>
            public string Name { get; private set; }
    
            /// <summary>
            /// Gets the address.
            /// </summary>
            public string Address { get; private set; }
    
            /// <summary>
            /// Gets the date of birth.
            /// </summary>
            public DateTime DateOfBirth { get; private set; }
    
            /// <summary>
            /// Gets the National Insurance number.
            /// </summary>
            public string NationalInsuranceNumber { get; private set; }
        }
    }
    
    // ------------------------------------------------------------
    // Copyright (c) James Eastham.
    // ------------------------------------------------------------
    
    using System;
    using System.Collections.Generic;
    
    namespace CleanArchitecture.Core.Requests
    {
        /// <summary>
        /// Response from a successul gather of ContractInfo.
        /// </summary>
        protected class GatherContactInfoResponse
            : BaseResponse
        {
            internal GatherContactInfoResponse(string name, string address, DateTime dateOfBirth, string nationalInsuranceNumber)
                : base()
            {
                this.Name = name;
                this.Address = address;
                this.DateOfBirth = dateOfBirth;
                this.NationalInsuranceNumber = nationalInsuranceNumber;
            }
    
            /// <summary>
            /// Gets or sets the applicants name.
            /// </summary>
            public string Name { get; set; }
    
            /// <summary>
            /// Gets or sets the applicants address.
            /// </summary>
            public string Address { get; set; }
    
            /// <summary>
            /// Gets or sets the applicants date of birth.
            /// </summary>
            public DateTime DateOfBirth { get; set; }
    
            /// <summary>
            /// Gets or sets the applicants national insurance number.
            /// </summary>
            public string NationalInsuranceNumber { get; set; }
    
            /// <summary>
            /// Gets or sets the applicants credit score.
            /// </summary>
            public decimal CreditScore { get; set; }
    
            /// <summary>
            /// Gets a value indicating if the customer has been accepted for a loan.
            /// </summary>
            public bool IsAcceptedForLoan  { get; private set; }
        }
    }
    
    우리는 위의 두 과목으로 끝난다.이름, 주소, 생년월일 및 NI 번호를 입력합니다.신용 평점과'Is Accepted ForLoan'이 나온다.
    요청과 응답을 기억하고 가장 깨끗한 체계 구조 실천을 사용하면 Gather Contact Info Interactor를 만들 수 있습니다.
    // ------------------------------------------------------------
    // Copyright (c) James Eastham.
    // ------------------------------------------------------------
    
    using System;
    using System.Threading.Tasks;
    using CleanArchitecture.Core.Entities;
    using CleanArchitecture.Core.Services;
    
    namespace CleanArchitecture.Core.Requests
    {
        /// <summary>
        /// Handles a <see cref="GatherContactInfoRequest"/>.
        /// </summary>
        public class GatherContactInfoInteractor
            : IRequestHandler<GatherContactInfoRequest, GatherContactInfoResponse>
        {
            private readonly ICreditScoreService _creditScoreService;
            private readonly ICustomerDatabase _customerDatabase;
    
            /// <summary>
            /// Initializes a new instance of the <see cref="GatherContactInfoInteractor"/> class.
            /// </summary>
            /// <param name="creditScoreService">A <see cref="ICreditScoreService"/>.</param>
            /// <param name="customerDatabase">A <see cref="ICustomerDatabase"/>.</param>
            public GatherContactInfoInteractor(ICreditScoreService creditScoreService, ICustomerDatabase customerDatabase)
            {
                this._creditScoreService = creditScoreService;
                this._customerDatabase = customerDatabase;
            }
    
            /// <summary>
            /// Handle the given <see cref="GatherContactInfoRequest"/>.
            /// </summary>
            /// <param name="request">A <see cref="GatherContactInfoRequest"/>.</param>
            /// <returns>A <see cref="GatherContactInfoResponse"/>.</returns>
            public GatherContactInfoResponse Handle(GatherContactInfoRequest request)
            {
                if (request is null)
                {
                    throw new ArgumentNullException(nameof(request));
                }
    
                var response = new GatherContactInfoResponse(
                    request.Name,
                    request.Address,
                    request.DateOfBirth,
                    request.NationalInsuranceNumber);
    
                if (string.IsNullOrEmpty(request.Name))
                {
                    response.AddError("Name cannot be empty");
                }
    
                if (string.IsNullOrEmpty(request.Address))
                {
                    response.AddError("Address cannot be empty");
                }
    
                if (string.IsNullOrEmpty(request.NationalInsuranceNumber))
                {
                    response.AddError("National Insurance number cannot be empty.");
                }
    
                var customerRecord = new Customer(request.Name, request.Address, request.DateOfBirth, request.NationalInsuranceNumber);
    
                if (customerRecord.Age < 18)
                {
                    response.AddError("A customer must be over the age of 18");
                }
    
                if (response.HasError == false)
                {
                    response.CreditScore = this._creditScoreService.GetCreditScore(request.NationalInsuranceNumber);
    
                    if (response.CreditScore > 500)
                    {
                        this._customerDatabase.Store(customerRecord);
                    }
                    else
                    {
                        response.AddError("Credit score is too low, sorry!");
                    }
                }
    
                return response;
            }
        }
    }
    
    나는 코드를 한 줄 한 줄 토론할 생각은 없지만, 당신은 업무 용례가 명확하다는 것을 볼 수 있습니다.
    새로운 개발자가 이 부류에 들어가면 지금 벌어지고 있는 일을 쉽게 볼 수 있다.그것은 뚜렷하고 간결하며 실제 상업 분야에 매우 가깝다.
    비프로그래머와 앉아서 이 코드 파일을 줄줄이 토론해서 어떤 문제를 해결할 수 있다고 상상하는 것도 큰 연장이 아니다.
    그래서 현재 우리는 이미 우리 업무의 핵심(이것은 매우 작은 은행)을 그렸지만, 우리는 실제로 코드를 어떻게 사용하는가.

    사용자 인터페이스


    현재 우리는 업무 논리를 제정했기 때문에 은행 직원들이 소프트웨어와 어떻게 실제 상호작용을 하는지를 고려하기 시작할 수 있다.
    이런 상황에서 은행은 작은 실행 가능한 파일을 가지고 있어서 매우 기쁘다. 그들은 실행할 수 있고 수동으로 정보를 입력한 후에 결과가 돌아오기를 기다릴 수 있다.
    우리의 모든 업무 논리는 우리의 핵심 라이브러리에 포함되어 있기 때문에 컨트롤러 응용 프로그램은 매우 간단해진다.
    // ------------------------------------------------------------
    // Copyright (c) James Eastham.
    // ------------------------------------------------------------
    
    using System;
    using CleanArchitecture.Core.Requests;
    
    namespace CleanArchitecture.ConsoleApp
    {
        /// <summary>
        /// Main application entry point.
        /// </summary>
        public static class Program
        {
            /// <summary>
            /// Main application entry point.
            /// </summary>
            /// <param name="args">Arguments passed into the application at runtime.</param>
            public static void Main(string[] args)
            {
                if (args == null)
                {
                    throw new ArgumentException("args cannot be null");
                }
    
                var getContactInteractor = new GatherContactInfoInteractor(new MockCreditScoreService(), new CustomerDatabaseInMemoryImpl());
    
                Console.WriteLine("Name?");
                var name = Console.ReadLine();
    
                Console.WriteLine("Address?");
                var address = Console.ReadLine();
    
                Console.WriteLine("Date of birth (yyyy-MM-dd)?");
                var dateOfBirth = DateTime.Parse(Console.ReadLine());
    
                Console.WriteLine("NI Number?");
                var niNumber = Console.ReadLine();
    
                var getContactResponse = getContactInteractor.Handle(new GatherContactInfoRequest(name, address, dateOfBirth, niNumber));
    
                if (getContactResponse.HasError)
                {
                    foreach (var error in getContactResponse.Errors)
                    {
                        Console.WriteLine(error);
                    }
                }
                else
                {
                    var result = getContactResponse.IsAcceptedForLoan ? "accepted" : "rejected";
    
                    Console.WriteLine($"Credit score is {getContactResponse.CreditScore} so customer has been {result}");
                }
    
                Console.WriteLine("Press any key to close");
                Console.ReadKey();
            }
        }
    }
    
    UI는 고객이 인증하는 방법보다는 데이터가 사용자에게 어떻게 전달되는지에 초점을 맞춥니다.
    만약 6개월 후에 은행이 잠재 고객이 수동 입력이 아닌 사이트를 통해 자신을 검증할 수 있도록 허락한다면 이 과정은 매우 좋고 간단하다.
    완전히 같은 라이브러리를 사용할 수 있는데, 이 인터페이스를 어떻게 제공하는지는 전혀 무관하다.
    갑자기 그 은행은 그들이 통제할 수 없는 변화에 더욱 유연하게 대응할 수 있었다.
    만약 새로운 기술 추세가 시작되면 은행은 뒤에서 잠재적 고객을 식별하기 시작한다. (은행은 아직 GDPR을 들어보지 못했으니, 당신은 볼 수 있다.) 그들은 완전히 같은 라이브러리를 다시 사용하여 검증을 실행할 수 있다.
    CreditScoreService와CustomerDatabase의 실현도 실행할 때 주입된다.

    요약


    나는 청결 건축에 관한 많은 댓글과 평론을 보았는데, 그것은 많은 항목에 있어서 지나쳤다고 말했다.
    소형 유틸리티나 일회용 프로그램에 대해 나는 이 주장에 동의한다. 이 프로그램들은 모든 종류의 열 복구에 사용될 것이다.그러나 나는 모든 생산 장면에서 사용하는 응용 프로그램이 처음부터 정확한 방식으로 구축되어야 한다고 믿는다.
    예를 들어 데이터베이스와 업무 논리를 분리하면 응용 프로그램이 변화에 더욱 쉽게 적응할 수 있다.

    좋은 웹페이지 즐겨찾기