آموزش دیزاین پترن Builder در کاتلین

آموزش دیزاین پترن Builder در کاتلین
فهرست مقاله [نمایش]

    دیزاین پترن builder ثابت کرده است که پارادایمی مفید در زبان برنامه نویسی جاوا است  در حالی که پارامترهای زیادی برای ساخت یک شی مورد نیاز است. همانطور که جاوا اشاره می کند ، سازنده ها یا متد های کارخانه با پارامترهای بسیار زیاد (وقتی پارامترها به طور رندم در کلاینت ها عوض می شوند) در معرض اشکال قرار می گیرند.

     

    پارامترهای نامگذاری شده Kotlin در بسیاری از موارد این مشکل را برطرف می کنند زیرا در کاتلین زمانی که متد را فراخوانی و مقدار دهی میکنیم می توانیم نام هر پارامتر را مشخص کنیم و احتمال جایگذاری نامناسب پارامتر را کاهش بدهیم. با این حال از آنجا که client های جاوا نمی توانند از پارامترهای نام برده استفاده کنند  دیزاین پترن builder همچنان مفید است. علاوه بر این تنظیم داینامیک پارامترهای اختیاری می تواند مفید باشد ، همچنین فقط با دیزاین پترن builder امکان پذیر است.

     برای آشنایی با کاتلین دوره آموزش زبان برنامه نویسی کاتلین سایت باگتو رو ببین.

     

    بیایید یک دیزاین پترن builder ساده را در زبان برنامه نویسی جاوا در نظر بگیریم. ما ابتدا یک کلاس POJO به اسم Company(شرکت) داریم که دارای چندین attribute است ، شاید برای ساختش استفاده از builder کافی باشد:

    public final class Company {
        public final String name;
        public final double marketCap;
        public final double annualCosts;
        public final double annualRevenue;
        public final List<Employee> employees;
        public final List<Office> offices;
    
        private Company(Builder builder) {
            List<Employee> builtEmployees = new ArrayList<>();
            for (Employee.Builder employee : builder.employees) {
                builtEmployees.add(employee.build());
            }
            List<Office> builtOffices = new ArrayList<>();
            for (Office.Builder office : builder.offices) {
                builtOffices.add(office.build());
            }
            employees = Collections.unmodifiableList(builtEmployees);
            offices = Collections.unmodifiableList(builtOffices);
            name = builder.name;
            marketCap = builder.marketCap;
            annualCosts = builder.annualCosts;
            annualRevenue = builder.annualRevenue;
        }
    
        public static class Builder {
            private String name;
            private double marketCap;
            private double annualCosts;
            private double annualRevenue;
            private List<Employee.Builder> employees = new ArrayList<>();
            private List<Office.Builder> offices = new ArrayList<>();
    
            public Company build() {
                return new Company(this);
            }
    
            public Builder addEmployee(Employee.Builder employee) {
                employees.add(employee);
                return this;
            }
    
            public Builder addOffice(Office.Builder office) {
                offices.add(office);
                return this;
            }
    
            public Builder setName(String name) {
                this.name = name;
                return this;
            }
    
            public Builder setMarketCap(double marketCap) {
                this.marketCap = marketCap;
                return this;
            }
    
            public Builder setAnnualCosts(double annualCosts) {
                this.annualCosts = annualCosts;
                return this;
            }
    
            public Builder setAnnualRevenue(double annualRevenue) {
                this.annualRevenue = annualRevenue;
                return this;
            }
        }
    }
    

     

    یک شرکت لیستی از Employee ها و لیستی از office دارد. در این کلاس ها از دیزاین پترن builder نیز استفاده می شود:

     

    public final class Employee {
        public final String firstName;
        public final String lastName;
        public final String id;
        public final boolean isManager;
        public final String managerId;
    
        private Employee(Builder builder) {
            this.firstName = builder.firstName;
            this.lastName = builder.lastName;
            this.id = builder.id;
            this.isManager = builder.isManager;
            this.managerId = builder.managerId;
        }
    
        public static class Builder {
            private String firstName;
            private String lastName;
            private String id;
            private boolean isManager;
            private String managerId;
    
            Employee build() {
                return new Employee(this);
            }
    
            public Builder setFirstName(String firstName) {
                this.firstName = firstName;
                return this;
            }
    
            public Builder setLastName(String lastName) {
                this.lastName = lastName;
                return this;
            }
    
            public Builder setId(String id) {
                this.id = id;
                return this;
            }
    
            public Builder setIsManager(boolean manager) {
                isManager = manager;
                return this;
            }
    
            public Builder setManagerId(String managerId) {
                this.managerId = managerId;
                return this;
            }
        }
    }
    
    

     

    و یک کلاس به نام office داریم که به صورت زیر میباشد:

     

    public final class Office {
        public final String address;
        public final int capacity;
        public final int occupancy;
        public final int sqft;
    
        private Office(Builder builder) {
            address = builder.address;
            capacity = builder.capacity;
            occupancy = builder.occupancy;
            sqft = builder.sqft;
        }
    
        public static class Builder {
            private String address;
            private int capacity;
            private int occupancy;
            private int sqft;
    
            Office build() {
                return new Office(this);
            }
    
            public Builder setAddress(String address) {
                this.address = address;
                return this;
            }
    
            public Builder setCapacity(int capacity) {
                this.capacity = capacity;
                return this;
            }
    
            public Builder setOccupancy(int occupancy) {
                this.occupancy = occupancy;
                return this;
            }
    
            public Builder setSqft(int sqft) {
                this.sqft = sqft;
                return this;
            }
        }
    }
    

     

    حال اگر بخواهیم شرکتی ایجاد کنیم که شامل یک کارمند(Employee) و یک تماس گیرنده office باشد ، می توانیم کارهای زیر را در جاوا انجام دهیم:

     

    public class JavaClient {
        public Company buildCompany() {
            Company.Builder company = new Company.Builder();
            Employee.Builder employee = new Employee.Builder()
                    .setFirstName("Doug")
                    .setLastName("Sigelbaum")
                    .setIsManager(false)
                    .setManagerId("XXX");
            Office.Builder office = new Office.Builder()
                    .setAddress("San Francisco")
                    .setCapacity(2500)
                    .setOccupancy(2400);
            company.setAnnualCosts(0)
                    .setAnnualRevenue(0)
                    .addEmployee(employee)
                    .addOffice(office);
            return company.build();
        }
    }
    
    

     

    حال اگر این کلاس را با زبان kotlinبنویسیم به صورت زیر خواهد بود:

     

    class KotlinClient {
        fun buildCompany(): Company {
            val company = Company.Builder()
            val employee = Employee.Builder()
                .setFirstName("Doug")
                .setLastName("Sigelbaum")
                .setIsManager(false)
                .setManagerId("XXX")
            val office = Office.Builder()
                .setAddress("San Francisco")
                .setCapacity(2500)
                .setOccupancy(2400)
            company.setAnnualCosts(0.0)
                .setAnnualRevenue(0.0)
                .addEmployee(employee)
                .addOffice(office)
            return company.build()
        }
    }
    

     

    متد های wrapper کاتلین با پارامترهای Lambda

     

     wrapper سطح بالا

    در اینجا یک تابع Kotlin وجود دارد که تنها تابع سطح بالا در کلاس Company ما خواهد بود:

    inline fun company(buildCompany: Company.Builder.() -> Unit): Company {
        val builder = Company.Builder()
        // Since `buildCompany` is an extension function for Company.Builder,
        // buildCompany() is called on the Company.Builder object.
        builder.buildCompany()
        return builder.build()
    }
    

    توجه: ما عملکرد را به صورت درون خطی علامت گذاری می کنیم تا overhead را از لامبدا حذف کنیم.

    از آنجا که پارامتر lambda از نوع Company.Builder. () -> Unit برچسب گذاری شده است ، تمام عبارات موجود در lambda از Scope(دامنه) Company.Builder استفاده میکنند.

    در حال حاضر ، client کاتلین می تواند از تابع company برای جلوگیری از ایجاد مستقیم نمونه استفاده کند.Company.Builder و فراخوانی build()  :

    class KtxClient1 {
        fun buildCompany(): Company {
            return company {
                // `this` scope is the Company.Builder being built.
                addEmployee(
                    Employee.Builder()
                        .setFirstName("Doug")
                        .setLastName("Sigelbaum")
                        .setIsManager(false)
                        .setManagerId("XXX")
                )
                addOffice(
                    Office.Builder()
                        .setAddress("San Francisco")
                        .setCapacity(2500)
                        .setOccupancy(2400)
                )
            }
        }
    }
    

    wrapper builder تو در تو

    اکنون می توانیم چند متد extension برای company اضافه کنیم. Builder برای جلوگیری از اضافه کردن مستقیم Employee یا اضافه کردن آنها.یک راه حل بالقوه وجود دارد:

    inline fun Company.Builder.employee(
        buildEmployee: Employee.Builder.() -> Unit
    ) {
        val builder = Employee.Builder()
        builder.buildEmployee()
        addEmployee(builder)
    }
    
    inline fun Company.Builder.office(buildOffice: Office.Builder.() -> Unit) {
        val builder = Office.Builder()
        builder.buildOffice()
        addOffice(builder)
    }
    

     

    با استفاده از این توابع extension ، کلاس client کاتلین مانند زیر است:

     

    class KtxClient2 {    
        fun buildCompany(): Company {
            return company {
                employee {
                    setFirstName("Doug")
                    setLastName("Sigelbaum")
                    setIsManager(false)
                    setManagerId("XXX")
                }
                office {
                    setAddress("San Francisco")
                    setCapacity(2500)
                    setOccupancy(2400)
                }
            }
        }
    }
    

     

    تقریباً تمام شد!  ما در مرتب سازی API Builder به هدف خود رسیده ایم ، اما موضوع جدیدی را معرفی کرده ایم. کلاس client زیر را بررسی کنید:

     

    class KtxBadClient {  
        fun buildBadCompany(): Company {
            return company {
                employee {
                    setFirstName("Doug")
                    setLastName("Sigelbaum")
                    setIsManager(false)
                    setManagerId("XXX")
                    employee {
                        setFirstName("Sean")
                        setLastName("Mcq")
                        setIsManager(false)
                        setManagerId("XXX")
                    }
                }
                office {
                    setAddress("San Francisco")
                    setCapacity(2500)
                    setOccupancy(2400)
                }
            }
        }
    }
    

     

    متأسفانه این کار با وجود کلاس Employee تو در تو کامپایل و اجرا می شود! در کاتلین  Scope شما می تواند همزمان چندین شی را دربر بگیرد. این بدان معنی است که هر چیزی در داخل company{…} به Company.Builder. دسترسی دارد. بنابراین ، هرچیزی درون { … } employee تو در تو  به Employee دسترسی دارد. Builder و Company.Builder. بنابراین این کد به طور خطی 2 کارمند "Doug" و "Sean" را به Company اضافه می کند که هیچ رابطه مستقیمی بین Doug و Sean ندارند.

    چگونه می توانیم توابع extension خود را تغییر دهیم تا هنگام دسترسی client به Scope اشتباه باعث ایجاد خطای کامپایلر شود؟ به عبارت دیگر ، چگونه می توانیم نوع DSL خود را ایمن کنیم؟

     

    question

     

    خوشبختانه Kotlin 1.1 انوتیشن DslMarker را برای حل این مشکل معرفی کردsmiley

    DSL خود را با استفاده از انوتیشنDslMarker ایمن کنید.

    ابتدا یک کلاس annotation با انوتیشن DslMarker ایجاد کنیم:

    @DslMarker
    annotation class CompanyDsl
    

    حال ، اگر مجموعه ای از کلاس ها با انوتیشن CompanyDsl را بسازیم تماس گیرندگان این کلاس ها  دسترسی ضمنی به چندین receiver که کلاس آنها در مجموعه کلاس های annotation است ، نخواهند داشت. در عوض تماس گیرنده ها فقط با نزدیکترین محدوده به receiver دسترسی ضمنی دارند. DslMarker در Kotlin stdlib است ، بنابراین این احتمال وجود دارد که کلاسهای Builder اصلی شما این وابستگی را نداشته باشند و بنابراین نمی توانید مستقیماً برای کلاسهای Builder خود انوتیشن بنویسد.

    در این صورت ، می توانید Builder ها را زیرکلاس کنید و از این زیرکلاس ها در متد های  wrapper کاتلین خود استفاده کنید:

    @CompanyDsl
    class CompanyBuilderDsl : Company.Builder()
    
    @CompanyDsl
    class EmployeeBuilderDsl : Employee.Builder()
    
    @CompanyDsl
    class OfficeBuilderDsl : Office.Builder()
    
    inline fun company(buildCompany: CompanyBuilderDsl.() -> Unit): Company {
        val builder = CompanyBuilderDsl()
        // Since `buildCompany` is an extension function for Company.Builder,
        // buildCompany() is called on the Company.Builder object.
        builder.buildCompany()
        return builder.build()
    }
    
    inline fun CompanyBuilderDsl.employee(
        buildEmployee: EmployeeBuilderDsl.() -> Unit
    ) {
        val builder = EmployeeBuilderDsl()
        builder.buildEmployee()
        addEmployee(builder)
    }
    
    inline fun CompanyBuilderDsl.office(
        buildOffice: OfficeBuilderDsl.() -> Unit
    ) {
        val builder = OfficeBuilderDsl()
        builder.buildOffice()
        addOffice(builder)
    }
    

     

    اکنون ، مثال client قبلی خطای کامپایلر مورد نظر ما را دریافت می کند:

    …can’t be called in this context by implicit receiver. Use the explicit one if necessary 

    برای درک بهتر این مقاله دوره  آموزش زبان برنامه نویسی کاتلین  رو در سایت حتما ببینید.

    این آموزش به پایان رسید سوالات خود را در قسمت نظرات مطرح کنید تا به آنها پاسخ دهم.


    • نویسنده: میثم بابائی

    ارسال دیدگاه

    برای افزودن دیدگاه خود، نیاز است ابتدا وارد حساب کاربری‌تان شوید


    دیدگاه کاربران