มันจะโอเคมั้ยถ้ามีวัตถุที่หล่อด้วยตัวเองแม้ว่ามันจะทำให้ API ของคลาสย่อยของพวกเขาสกปรก


33

Baseฉันได้เรียนฐาน มันมีอยู่สอง subclasses, และSub1 Sub2แต่ละคลาสย่อยมีวิธีเพิ่มเติมบางอย่าง ตัวอย่างเช่นSub1มีSandwich makeASandwich(Ingredients... ingredients)และมีSub2boolean contactAliens(Frequency onFrequency)

เนื่องจากวิธีการเหล่านี้ใช้พารามิเตอร์ที่แตกต่างกันและทำสิ่งที่แตกต่างอย่างสิ้นเชิงพวกเขาไม่เข้ากันอย่างสมบูรณ์และฉันไม่สามารถใช้ความหลากหลายในการแก้ปัญหานี้ได้

Baseมอบการทำงานส่วนใหญ่และฉันมีBaseวัตถุจำนวนมาก อย่างไรก็ตามBaseวัตถุทั้งหมดเป็นแบบ a Sub1หรือ a Sub2และบางครั้งฉันจำเป็นต้องรู้ว่ามันคืออะไร

ดูเหมือนจะเป็นการดีที่จะทำสิ่งต่อไปนี้:

for (Base base : bases) {
    if (base instanceof Sub1) {
        ((Sub1) base).makeASandwich(getRandomIngredients());
        // ... etc.
    } else { // must be Sub2
        ((Sub2) base).contactAliens(getFrequency());
        // ... etc.
    }
}

ดังนั้นฉันจึงคิดกลยุทธ์เพื่อหลีกเลี่ยงสิ่งนี้โดยไม่ต้องร่าย Baseตอนนี้มีวิธีการเหล่านี้:

boolean isSub1();
Sub1 asSub1();
Sub2 asSub2();

และแน่นอนSub1ใช้วิธีการเหล่านี้เป็น

boolean isSub1() { return true; }
Sub1 asSub1();   { return this; }
Sub2 asSub2();   { throw new IllegalStateException(); }

และSub2นำไปใช้ในทางตรงกันข้าม

น่าเสียดายที่ตอนนี้Sub1และSub2มีวิธีการเหล่านี้ใน API ของตัวเอง Sub1ดังนั้นผมจึงสามารถทำเช่นนี้เช่นบน

/** no need to use this if object is known to be Sub1 */
@Deprecated
boolean isSub1() { return true; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub1 asSub1();   { return this; }

/** no need to use this if object is known to be Sub1 */
@Deprecated
Sub2 asSub2();   { throw new IllegalStateException(); }

ด้วยวิธีนี้หากวัตถุเป็นที่รู้จักเพียง a Baseวิธีการเหล่านี้จะไม่เลิกใช้และสามารถใช้ในการ "cast" ตัวเองไปเป็นประเภทอื่นดังนั้นฉันจึงสามารถเรียกใช้เมธอด subclass ของมันได้ สิ่งนี้ดูสง่างามสำหรับฉัน แต่ในทางกลับกันฉันชอบใช้คำอธิบายประกอบที่ไม่เหมาะสมเป็นวิธี "ลบ" วิธีการออกจากชั้นเรียน

เนื่องจากSub1อินสแตนซ์จริงๆเป็น Base จึงเหมาะสมที่จะใช้การสืบทอดมากกว่าการห่อหุ้ม ฉันทำอะไรได้ดี มีวิธีที่ดีกว่าในการแก้ปัญหานี้หรือไม่?


12
ตอนนี้ทั้ง 3 คลาสต้องรู้ซึ่งกันและกัน การเพิ่ม Sub3 จะเกี่ยวข้องกับการเปลี่ยนแปลงรหัสจำนวนมากและการเพิ่ม Sub10 จะเจ็บปวดอย่างสิ้นเชิง
Dan Pichelman

15
มันจะช่วยได้มากถ้าคุณให้รหัสจริงแก่เรา มีสถานการณ์ที่เหมาะสมในการตัดสินใจตามระดับของบางสิ่งบางอย่าง แต่มันเป็นไปไม่ได้ที่จะบอกว่าคุณเป็นคนชอบธรรมในสิ่งที่คุณทำกับตัวอย่างที่วางแผนไว้ สำหรับสิ่งที่มันคุ้มค่าสิ่งที่คุณต้องการเป็นผู้เข้าชมหรือยูเนี่ยนที่ติดแท็ก
Doval

1
ขออภัยฉันคิดว่าการทำตัวอย่างง่าย ๆ จะช่วยให้ง่ายขึ้น บางทีฉันอาจโพสต์คำถามใหม่ด้วยขอบเขตที่กว้างขึ้นของสิ่งที่ฉันต้องการจะทำ
codebreaker

12
คุณเพียงแค่reimplementingหล่อและinstanceofในทางที่ต้องใช้จำนวนมากของการพิมพ์เป็นข้อผิดพลาดและทำให้มันยากที่จะเพิ่มมากขึ้น subclasses
253751

5
หากSub1และSub2ไม่สามารถใช้แทนกันได้ทำไมคุณปฏิบัติต่อพวกเขาเช่นนี้? ทำไมไม่ติดตาม 'ผู้สร้างแซนวิช' และ 'ผู้ติดต่อกับคนต่างด้าว' แยกกันล่ะ
Pieter Witvoet

คำตอบ:


27

มันไม่สมเหตุสมผลที่จะเพิ่มฟังก์ชั่นให้กับคลาสพื้นฐานดังที่แนะนำไว้ในคำตอบอื่น ๆ การเพิ่มฟังก์ชั่นเคสพิเศษจำนวนมากเกินไปอาจส่งผลให้เกิดการรวมส่วนประกอบที่ไม่เกี่ยวข้องเข้าด้วยกัน

ตัวอย่างเช่นฉันอาจมีAnimalคลาสพร้อมCatและDogคอมโพเนนต์ ถ้าฉันต้องการที่จะสามารถพิมพ์พวกเขาหรือแสดงพวกเขาใน GUI มันอาจ overkill สำหรับฉันที่จะเพิ่มrenderToGUI(...)และsendToPrinter(...)ชั้นฐาน

วิธีการที่คุณใช้โดยใช้การตรวจสอบประเภทและการปลดเปลื้องนั้นมีความเปราะบาง แต่อย่างน้อยก็แยกความกังวลออกจากกัน

อย่างไรก็ตามหากคุณพบว่าตัวเองทำเช็ค / คาสต์ประเภทนี้บ่อยครั้งตัวเลือกหนึ่งคือการใช้รูปแบบของผู้เยี่ยมชม / ส่งซ้ำสองครั้ง ดูเหมือนว่าจะเป็นแบบนี้:

public abstract class Base {
  ...
  abstract void visit( BaseVisitor visitor );
}

public class Sub1 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub1(this); }
}

public class Sub2 extends Base {
  ...
  void visit(BaseVisitor visitor) { visitor.onSub2(this); }
}

public interface BaseVisitor {
   void onSub1(Sub1 that);
   void onSub2(Sub2 that);
}

ตอนนี้รหัสของคุณกลายเป็น

public class ActOnBase implements BaseVisitor {
    void onSub1(Sub1 that) {
       that.makeASandwich(getRandomIngredients())
    }

    void onSub2(Sub2 that) {
       that.contactAliens(getFrequency());
    }
}

BaseVisitor visitor = new ActOnBase();
for (Base base : bases) {
    base.visit(visitor);
}

ประโยชน์หลักคือถ้าคุณเพิ่มคลาสย่อยคุณจะได้รับข้อผิดพลาดในการรวบรวมมากกว่ากรณีหายไปอย่างเงียบ ๆ คลาสผู้เยี่ยมชมใหม่จะกลายเป็นเป้าหมายที่ดีในการดึงฟังก์ชั่นเข้ามา ตัวอย่างเช่นมันอาจเข้าท่าที่จะย้ายgetRandomIngredients()เข้าActOnBaseมา

คุณยังสามารถแยกลอจิกวนลูป: ตัวอย่างเช่นส่วนย่อยด้านบนอาจกลายเป็น

BaseVisitor.applyToArray(bases, new ActOnBase() );

การนวดเพิ่มเติมเล็กน้อยและการใช้ lambdas และการสตรีมของ Java 8 จะทำให้คุณไปถึงได้

bases.stream()
     .forEach( BaseVisitor.forEach(
       Sub1 that -> that.makeASandwich(getRandomIngredients()),
       Sub2 that -> that.contactAliens(getFrequency())
     ));

IMO ตัวไหนที่ดูเรียบร้อยและรวบรัดที่สุดเท่าที่จะทำได้

นี่คือตัวอย่างที่สมบูรณ์ของ Java 8:

public static abstract class Base {
    abstract void visit( BaseVisitor visitor );
}

public static class Sub1 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub1(this); }

    void makeASandwich() {
        System.out.println("making a sandwich");
    }
}

public static class Sub2 extends Base {
    void visit(BaseVisitor visitor) { visitor.onSub2(this); }

    void contactAliens() {
        System.out.println("contacting aliens");
    }
}

public interface BaseVisitor {
    void onSub1(Sub1 that);
    void onSub2(Sub2 that);

    static Consumer<Base> forEach(Consumer<Sub1> sub1, Consumer<Sub2> sub2) {

        return base -> {
            BaseVisitor baseVisitor = new BaseVisitor() {

                @Override
                public void onSub1(Sub1 that) {
                    sub1.accept(that);
                }

                @Override
                public void onSub2(Sub2 that) {
                    sub2.accept(that);
                }
            };
            base.visit(baseVisitor);
        };
    }
}

Collection<Base> bases = Arrays.asList(new Sub1(), new Sub2());

bases.stream()
     .forEach(BaseVisitor.forEach(
             Sub1::makeASandwich,
             Sub2::contactAliens));

+1: สิ่งนี้ใช้กันทั่วไปในภาษาที่มีการสนับสนุนชั้นหนึ่งสำหรับการจับคู่ประเภทและการจับคู่รูปแบบและเป็นการออกแบบที่ถูกต้องใน Java แม้ว่ามันจะน่าเกลียดมาก syntactically
Mankarse

@Mankarse น้ำตาล syntactic เล็กน้อยสามารถทำให้ไกลจากน่าเกลียด - ฉันได้อัปเดตด้วยตัวอย่าง
Michael Anderson

สมมติว่าสมมุติฐาน OP Sub3เปลี่ยนแปลงใจของพวกเขาและตัดสินใจที่จะเพิ่ม ผู้เข้าชมไม่มีปัญหาเหมือนกับที่แน่นอนinstanceofใช่หรือไม่ ตอนนี้คุณต้องเพิ่มonSub3ผู้เข้าชมและมันจะแตกถ้าคุณลืม
Radiodef

1
@Radiodef ข้อได้เปรียบของการใช้รูปแบบผู้เข้าชมมากกว่าการเปลี่ยนเป็นประเภทโดยตรงคือเมื่อคุณเพิ่มonSub3ไปยังส่วนต่อประสานผู้เยี่ยมชมคุณจะได้รับข้อผิดพลาดในการรวบรวมในทุกสถานที่ที่คุณสร้างผู้เข้าชมใหม่ที่ยังไม่ได้รับการอัปเดต ในทางตรงกันข้ามการเปลี่ยนชนิดที่ดีที่สุดจะทำให้เกิดข้อผิดพลาดรันไทม์ - ซึ่งเป็นเรื่องยากที่จะทริกเกอร์
Michael Anderson

1
@FiveNine หากคุณยินดีที่จะเพิ่มตัวอย่างที่สมบูรณ์เข้าไปในส่วนท้ายของคำตอบที่อาจช่วยเหลือผู้อื่น
Michael Anderson

83

จากมุมมองของฉัน: การออกแบบของคุณเป็นสิ่งที่ผิด

แปลเป็นภาษาธรรมชาติคุณกำลังพูดต่อไปนี้:

ป.ร. ให้เรามีanimalsมีและ cats มีคุณสมบัติซึ่งเป็นเรื่องธรรมดาที่จะและ แต่นั่นไม่เพียงพอ: มีคุณสมบัติบางอย่างที่แตกต่างจากดังนั้นคุณต้อง subclassfishanimalscatsfishcatfish

ตอนนี้คุณมีปัญหาที่คุณลืมรูปแบบการเคลื่อนไหว ถูก ค่อนข้างง่าย:

for(Animal a : animals){
   if (a instanceof Fish) swim();
   if (a instanceof Cat) walk();
}

แต่นั่นเป็นการออกแบบที่ผิด วิธีที่ถูกต้องจะเป็น:

for(Animal a : animals){
    animal.move()
}

ที่ไหนmoveพฤติกรรมจะร่วมกันดำเนินการที่แตกต่างจากสัตว์แต่ละ

เนื่องจากวิธีการเหล่านี้ใช้พารามิเตอร์ที่แตกต่างกันและทำสิ่งที่แตกต่างอย่างสิ้นเชิงพวกเขาไม่เข้ากันอย่างสมบูรณ์และฉันไม่สามารถใช้ความหลากหลายในการแก้ปัญหานี้ได้

ซึ่งหมายความว่า: การออกแบบของคุณเสีย

คำแนะนำของฉัน: refactor Base, และSub1Sub2


8
หากคุณจะเสนอรหัสจริงฉันสามารถให้คำแนะนำได้
Thomas Junk

9
@codebreaker หากคุณยอมรับว่าตัวอย่างของคุณไม่ดีฉันขอแนะนำให้ยอมรับคำตอบนี้แล้วเขียนคำถามใหม่ อย่าแก้ไขคำถามเพื่อให้มีตัวอย่างที่แตกต่างมิฉะนั้นคำตอบที่มีอยู่จะไม่สมเหตุสมผล
Moby Disk

16
@ codebreaker ฉันคิดว่านี่ "แก้ปัญหา" โดยตอบคำถามจริง คำถามที่แท้จริงคือ: ฉันจะแก้ปัญหาของฉันได้อย่างไร สร้างรหัสของคุณใหม่อย่างถูกต้อง Refactor เพื่อให้คุณ.move()หรือ.attack()อย่างถูกต้องและนามธรรมthatส่วนหนึ่ง - แทนที่จะต้อง.swim(), .fly(), .slide(), .hop(), .jump(), .squirm(), .phone_home()ฯลฯ คุณจะทำอย่างไร refactor ถูกต้องหรือไม่ ไม่ทราบโดยไม่มีตัวอย่างที่ดีกว่า ... แต่โดยทั่วไปแล้วยังเป็นคำตอบที่ถูกต้อง - ยกเว้นว่าโค้ดตัวอย่างและรายละเอียดเพิ่มเติมจะแนะนำเป็นอย่างอื่น
WernerCD

11
@codebreaker หากลำดับชั้นของชั้นเรียนของคุณนำคุณไปสู่สถานการณ์เช่นนี้ดังนั้นตามนิยามแล้วมันไม่สมเหตุสมผลเลย
Patrick Collins

7
@ codebreaker ที่เกี่ยวข้องกับความคิดเห็นล่าสุดของ Crisfole มีวิธีการดูอีกวิธีหนึ่งที่อาจช่วยได้ ตัวอย่างเช่นด้วยmovevs fly/ run/ ฯลฯ ที่สามารถอธิบายได้ในหนึ่งประโยคว่า: คุณควรบอกวัตถุว่าจะทำอย่างไรไม่ใช่วิธีการทำ ในแง่ของ accessors ดังที่คุณกล่าวถึงเป็นจริงมากกว่าในความคิดเห็นที่นี่คุณควรถามคำถามวัตถุไม่ตรวจสอบสถานะของมัน
Izkata

9

เป็นการยากที่จะจินตนาการถึงสถานการณ์ที่คุณมีกลุ่มของสิ่งต่าง ๆ และต้องการให้พวกเขาทำแซนด์วิชหรือติดต่อมนุษย์ต่างดาว ในกรณีส่วนใหญ่ที่คุณพบการคัดเลือกนักแสดงคุณจะทำงานด้วยประเภทเดียว - เช่นในเสียงดังกราวคุณกรองชุดของโหนดสำหรับการประกาศที่getAsFunctionส่งกลับค่าที่ไม่เป็นโมฆะแทนที่จะทำบางสิ่งที่แตกต่างกันสำหรับแต่ละโหนดในรายการ

อาจเป็นไปได้ว่าคุณต้องการลำดับของการดำเนินการและไม่เกี่ยวข้องกับวัตถุที่ใช้ในการดำเนินการ

ดังนั้นแทนที่จะเป็นรายการ Baseให้ทำงานกับรายการการกระทำ

for (RandomAction action : actions)
   action.act(context);

ที่ไหน

interface RandomAction {
    void act(Context context);
} 

interface Context {
    Ingredients getRandomIngredients();
    double getFrequency();
}

คุณสามารถให้ Base ใช้เมธอดเพื่อส่งคืนการกระทำหรือวิธีการอื่นใดที่คุณต้องเลือกการกระทำจากอินสแตนซ์ในรายการฐานของคุณ (เนื่องจากคุณบอกว่าคุณไม่สามารถใช้ polymorphism ดังนั้นการกระทำที่จะทำ ไม่ใช่ฟังก์ชันของคลาส แต่มีคุณสมบัติอื่นของเบสไม่เช่นนั้นคุณจะให้เมธอดเบส (บริบท) กับเบส


2
คุณจะได้รับวิธีการที่ไร้สาระของฉันถูกออกแบบมาเพื่อแสดงความแตกต่างในสิ่งที่ชั้นเรียนสามารถทำได้เมื่อคลาสย่อยของพวกเขาเป็นที่รู้จัก (และพวกเขาไม่มีอะไรเกี่ยวข้องกัน) แต่ฉันคิดว่าการมีContextวัตถุทำให้เรื่องแย่ลง ฉันก็อาจจะส่งผ่านObjectไปยังวิธีการคัดเลือกพวกเขาและหวังว่าพวกเขาจะเป็นประเภทที่เหมาะสม
codebreaker

1
@ codebreaker คุณต้องชี้แจงคำถามของคุณจริงๆ - ในระบบส่วนใหญ่จะมีเหตุผลที่ถูกต้องสำหรับการเรียกฟังก์ชั่นมากมายที่ไม่เชื่อเรื่องพระเจ้าในสิ่งที่พวกเขาทำ ตัวอย่างเช่นการประมวลผลกฎในเหตุการณ์หรือดำเนินการขั้นตอนในการจำลอง โดยปกติระบบดังกล่าวจะมีบริบทที่การดำเนินการเกิดขึ้น หาก 'getRandomIngredients' และ 'getFrequency' ไม่เกี่ยวข้องก็ไม่ควรอยู่ในวัตถุเดียวกันและคุณต้องการวิธีการที่แตกต่างออกไปเช่นการดำเนินการจับแหล่งที่มาของส่วนผสมหรือความถี่
Pete Kirkham

1
คุณพูดถูกและฉันคิดว่าฉันไม่สามารถกู้คำถามนี้ได้ ฉันลงเอยด้วยการเลือกตัวอย่างที่ไม่ดีดังนั้นฉันอาจจะโพสต์อีกครั้ง getFrequency () และ getIngredients () เป็นเพียงตัวยึดตำแหน่ง ฉันควรจะใส่ "... " เป็นข้อโต้แย้งเพื่อให้ชัดเจนยิ่งขึ้นว่าฉันจะไม่รู้ว่ามันคืออะไร
codebreaker

นี่คือ IMO คำตอบที่ถูกต้อง เข้าใจว่าContextไม่จำเป็นต้องเป็นคลาสใหม่หรือแม้แต่อินเทอร์เฟซ action.act(this)โดยปกติบริบทเป็นเพียงโทรเพื่อโทรในรูปแบบ หากgetFrequency()และgetRandomIngredients()เป็นวิธีการคงที่คุณอาจไม่จำเป็นต้องมีบริบท นี่คือวิธีที่ฉันจะใช้พูดคิวงานซึ่งปัญหาของคุณฟังดูแย่มาก ๆ
QuestionC

นอกจากนี้ส่วนใหญ่จะเหมือนกับโซลูชันรูปแบบผู้เข้าชม ความแตกต่างคือคุณกำลังใช้วิธีผู้เข้าชม :: OnSub1 () / ผู้เข้าชม :: OnSub2 () วิธีการเป็น Sub1 :: act () / Sub2 :: act ()
คำถาม

4

ถ้าคุณมี subclasses ของคุณใช้หนึ่งหรือมากกว่าหนึ่งอินเตอร์เฟสที่กำหนดสิ่งที่พวกเขาสามารถทำได้? บางสิ่งเช่นนี้

interface SandwichCook
{
    public void makeASandwich(String[] ingredients);
}

interface AlienRadioSignalAwarable
{
    public void contactAliens(int frequency);

}

จากนั้นคลาสของคุณจะมีลักษณะดังนี้:

class Sub1 extends Base implements SandwichCook
{
    public void makeASandwich(String[] ingredients)
    {
        //some code here
    }
}

class Sub2 extends Base implements AlienRadioSignalAwarable
{
    public void contactAliens(int frequency)
    {
        //some code here
    }
}

และ for-loop ของคุณจะกลายเป็น:

for (Base base : bases) {
    if (base instanceof SandwichCook) {
        base.makeASandwich(getRandomIngredients());
    } else if (base instanceof AlienRadioSignalAwarable) {
        base.contactAliens(getFrequency());
    }
}

ข้อดีสองประการที่สำคัญสำหรับวิธีการนี้:

  • ไม่มีส่วนเกี่ยวข้องกับการคัดเลือกนักแสดง
  • คุณสามารถให้แต่ละคลาสย่อยใช้อินเทอร์เฟซได้มากเท่าที่คุณต้องการซึ่งจะให้ความยืดหยุ่นสำหรับการเปลี่ยนแปลงในอนาคต

PS: ขออภัยสำหรับชื่อของอินเทอร์เฟซฉันไม่สามารถนึกถึงอะไรที่เจ๋งกว่าในขณะนั้น: D


3
นี่ไม่ได้แก้ปัญหาจริงๆ คุณยังต้องการนักแสดงในวง (หากคุณไม่ต้องการคุณก็ไม่ต้องการนักแสดงในตัวอย่างของ OP)
Taemyr

2

วิธีการที่อาจจะเป็นหนึ่งที่ดีในกรณีที่เกือบทุกประเภทใด ๆ ภายในครอบครัวจะอย่างใดอย่างหนึ่งได้โดยตรงสามารถใช้งานได้ในฐานะที่เป็นอินเตอร์เฟซการดำเนินงานของบางอย่างที่มีคุณสมบัติตรงตามเกณฑ์บางส่วนหรือสามารถนำมาใช้ในการสร้างการดำเนินงานของอินเตอร์เฟซที่ ประเภทคอลเลกชันในตัว IMHO จะได้รับประโยชน์จากรูปแบบนี้ แต่เนื่องจากพวกเขาไม่ใช้เพื่อจุดประสงค์เช่นฉันจะคิดค้นส่วนต่อประสานการรวบรวมBunchOfThings<T>แต่เนื่องจากพวกเขาไม่ได้สำหรับวัตถุประสงค์ของตัวอย่างเช่นฉันจะคิดค้นอินเตอร์เฟซคอลเลกชัน

การใช้งานบางอย่างของBunchOfThingsไม่แน่นอน บางคนไม่ ในหลายกรณีวัตถุ Fred อาจต้องการเก็บสิ่งที่สามารถใช้เป็นBunchOfThingsและรู้ว่าไม่มีอะไรอื่นนอกจาก Fred จะสามารถแก้ไขได้ ข้อกำหนดนี้อาจทำได้สองวิธี:

  1. เฟร็ดรู้ว่ามันมีเพียงการอ้างอิงถึงสิ่งBunchOfThingsนั้นและไม่มีการอ้างอิงภายนอกกับไออาร์ว่าBunchOfThingsมันมีอยู่ภายในทุกที่ในจักรวาล หากไม่มีใครมีการอ้างอิงถึงBunchOfThingsหรือภายในก็จะไม่มีใครสามารถแก้ไขได้ดังนั้นข้อ จำกัด จะเป็นที่พอใจ

  2. ไม่BunchOfThingsสามารถแก้ไขหรืออ้างอิงภายในใด ๆ ที่มีการอ้างอิงภายนอกด้วยวิธีการใด ๆ หากไม่มีใครสามารถแก้ไข a ได้BunchOfThingsแน่นอนข้อ จำกัด จะเป็นที่พอใจ

วิธีหนึ่งในการทำให้พอใจข้อ จำกัด คือการคัดลอกวัตถุใด ๆ ที่ได้รับโดยไม่มีเงื่อนไข (การประมวลผลคอมโพเนนต์ที่ซ้อนกันซ้ำ ๆ ) อีกสิ่งหนึ่งคือการทดสอบว่าวัตถุที่ได้รับสัญญาว่าไม่สามารถเปลี่ยนแปลงได้หรือไม่และทำสำเนาของวัตถุนั้นและทำเช่นเดียวกันกับองค์ประกอบที่ซ้อนกัน อีกทางเลือกหนึ่งซึ่งจะสะอาดกว่าครั้งที่สองและเร็วกว่าครั้งแรกคือเสนอAsImmutableวิธีการที่ขอให้วัตถุทำสำเนาตัวเองที่ไม่เปลี่ยนรูป (ใช้AsImmutableกับส่วนประกอบที่ซ้อนกันที่สนับสนุน)

อาจมีการจัดเตรียมวิธีการที่เกี่ยวข้องasDetached(สำหรับใช้เมื่อรหัสได้รับวัตถุและไม่ทราบว่าจะต้องทำการกลายพันธุ์หรือไม่ในกรณีนี้วัตถุที่เปลี่ยนแปลงได้ควรถูกแทนที่ด้วยวัตถุที่เปลี่ยนแปลงได้ใหม่ แต่วัตถุที่ไม่เปลี่ยนรูปสามารถเก็บได้ ตามที่เป็น), asMutable(ในกรณีที่วัตถุรู้ว่ามันจะเก็บวัตถุก่อนหน้านี้ส่งคืนจากasDetachedเช่นการอ้างอิงที่ไม่ได้ใช้งานร่วมกันไปยังวัตถุที่ไม่แน่นอนหรือการอ้างอิงที่ใช้ร่วมกันได้กับวัตถุที่ไม่แน่นอน)asNewMutable (ในกรณีที่รหัสได้รับภายนอก การอ้างอิงและรู้ว่ามันจะต้องการที่จะกลายพันธุ์สำเนาของข้อมูลในนั้น - หากข้อมูลที่เข้ามาไม่แน่นอนไม่มีเหตุผลที่จะเริ่มต้นด้วยการทำสำเนาไม่เปลี่ยนรูปซึ่งจะถูกนำมาใช้ทันทีเพื่อสร้างสำเนาที่ไม่แน่นอนและถูกทอดทิ้ง)

โปรดทราบว่าในขณะที่asXXวิธีการอาจส่งคืนชนิดที่แตกต่างกันเล็กน้อยบทบาทที่แท้จริงของพวกเขาคือเพื่อให้แน่ใจว่าวัตถุที่ส่งคืนจะตอบสนองความต้องการของโปรแกรม


0

ไม่สนใจว่าคุณจะมีการออกแบบที่ดีหรือไม่และหากว่ามันดีหรืออย่างน้อยก็ยอมรับได้ฉันอยากจะพิจารณาความสามารถของคลาสย่อยไม่ใช่ประเภท

ดังนั้นทั้ง:


ย้ายความรู้เกี่ยวกับการดำรงอยู่ของแซนวิชและเอเลี่ยนลงในคลาสฐานแม้ว่าคุณจะรู้ว่าอินสแตนซ์บางตัวของคลาสพื้นฐานไม่สามารถทำได้ ใช้มันในคลาสฐานเพื่อโยนข้อยกเว้นและเปลี่ยนรหัสเป็น:

if (base.canMakeASandwich()) {
    base.makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    base.contactAliens(getFrequency());
    // ... etc.
}

จากนั้นหนึ่งหรือทั้งสอง subclasses แทนที่canMakeASandwich()และเพียงหนึ่งการดำเนินการแต่ละและmakeASandwich()contactAliens()


ใช้อินเตอร์เฟสไม่ใช่คลาสย่อยที่เป็นรูปธรรมในการตรวจสอบความสามารถของชนิดที่มี ออกจากคลาสฐานเพียงอย่างเดียวและเปลี่ยนรหัสเป็น:

if (base instanceof SandwichMaker) {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
    // ... etc.
} else { // can't make sandwiches, must be able to contact aliens
    ((AlienContacter)base).contactAliens(getFrequency());
    // ... etc.
}

หรืออาจเป็นไปได้ (และอย่าลังเลที่จะเพิกเฉยตัวเลือกนี้หากมันไม่เหมาะกับสไตล์ของคุณหรือสำหรับสไตล์ Java ที่คุณคิดว่าเหมาะสม)

try {
    ((SandwichMaker)base).makeASandwich(getRandomIngredients());
} catch (ClassCastException e) {
    ((AlienContacter)base).contactAliens(getFrequency());
}

ส่วนตัวผมไม่ได้มักจะชอบสไตล์หลังของการจับข้อยกเว้นครึ่งคาดว่าเพราะความเสี่ยงของการไม่เหมาะสมจับที่ClassCastExceptionออกมาของgetRandomIngredientsหรือmakeASandwichแต่ YMMV


2
การจับ ClassCastException เป็นเพียงการส่งต่อ นั่นคือจุดประสงค์ทั้งหมดของอินสแตนซ์ของ
Radiodef

@Radiodef: จริง แต่จุดประสงค์หนึ่งของ<การหลีกเลี่ยงการจับArrayIndexOutOfBoundsExceptionและบางคนตัดสินใจที่จะทำเช่นกัน ไม่มีการบัญชีสำหรับรสนิยม ;-) โดยทั่วไป "ปัญหา" ของฉันที่นี่คือฉันทำงานใน Python ซึ่งรูปแบบที่ต้องการมักจะเป็นที่จับข้อยกเว้นใด ๆ ที่เป็นที่นิยมในการทดสอบล่วงหน้าว่าข้อยกเว้นจะเกิดขึ้นไม่ว่าการทดสอบล่วงหน้าจะง่ายเพียงใด . บางทีความเป็นไปได้อาจไม่คุ้มค่าที่จะกล่าวถึงใน Java
Steve Jessop

@SteveJessop »มันง่ายที่จะขอขมามากกว่าอนุญาต«เป็นสโลแกน;)
โทมัสจังค์

0

ที่นี่เรามีกรณีที่น่าสนใจของคลาสฐานที่ downcasts ตัวเองไปยังคลาสที่ได้รับของตัวเอง เรารู้ดีว่านี่เป็นเรื่องไม่ดี แต่ถ้าเราต้องการบอกว่าเราพบเหตุผลที่ดีให้เราดูและดูว่าข้อ จำกัด ของเรื่องนี้คืออะไร:

  1. เราสามารถครอบคลุมทุกกรณีในคลาสฐาน
  2. ไม่มีคลาสที่ได้รับจากต่างประเทศจะต้องเพิ่มเคสใหม่
  3. เราสามารถอยู่กับนโยบายที่การเรียกเพื่อให้อยู่ภายใต้การควบคุมของคลาสพื้นฐาน
  4. แต่เรารู้จากวิทยาศาสตร์ของเราว่านโยบายของสิ่งที่ต้องทำในชั้นเรียนที่ได้รับมาสำหรับวิธีการที่ไม่ได้อยู่ในชั้นฐานคือชั้นเรียนที่ได้รับไม่ใช่ชั้นเรียนพื้นฐาน

ถ้า 4 เรามี: 5. นโยบายของชนชั้นที่ได้รับอยู่ภายใต้การควบคุมทางการเมืองเช่นเดียวกับนโยบายของชนชั้นฐานเสมอ

2 และ 5 ทั้งสองบอกเป็นนัยโดยตรงว่าเราสามารถระบุคลาสที่ได้รับทั้งหมดซึ่งหมายความว่าไม่ควรมีคลาสที่ได้รับภายนอก

แต่นี่คือสิ่งที่ หากเป็นของคุณทั้งหมดคุณสามารถแทนที่ if ด้วย abstraction ที่เป็นการเรียกเมธอดเสมือน (แม้ว่าจะเป็นเรื่องไร้สาระ) และกำจัด if และ cast ด้วยตนเอง ดังนั้นอย่าทำ มีการออกแบบที่ดีกว่า


-1

ใส่วิธีนามธรรมในคลาสฐานซึ่งทำสิ่งหนึ่งใน Sub1 และอีกอย่างใน Sub2 และเรียกวิธีนามธรรมนั้นในลูปผสมของคุณ

class Sub1 : Base {
    @Override void doAThing() {
        this.makeASandwich(getRandomIngredients());
    }
}

class Sub2 : Base {
    @Override void doAThing() {
        this.contactAliens(getFrequency());
    }
}

for (Base base : bases) {
    base.doAThing();
}

โปรดทราบว่าสิ่งนี้อาจต้องมีการเปลี่ยนแปลงอื่น ๆ ขึ้นอยู่กับวิธีกำหนด getRandomIngredients และ getFrequency แต่โดยความจริงแล้วในชั้นเรียนที่ติดต่อกับมนุษย์ต่างดาวน่าจะเป็นสถานที่ที่ดีกว่าในการนิยาม getFrequency

อนึ่งวิธี asSub1 และ asSub2 ของคุณนั้นเป็นวิธีปฏิบัติที่ไม่ดีอย่างแน่นอน หากคุณกำลังจะทำสิ่งนี้ให้ทำโดยการคัดเลือกนักแสดง ไม่มีวิธีการใดที่ทำให้คุณหล่อไม่ได้


2
คำตอบของคุณแนะนำว่าคุณไม่ได้อ่านคำถามทั้งหมด: ความแตกต่างเพียงแค่ไม่ตัดที่นี่ มีหลายวิธีในแต่ละ Sub1 และ Sub2 เหล่านี้เป็นเพียงตัวอย่างและ "doAThing" ไม่ได้แปลความหมายอะไรเลย มันไม่สอดคล้องกับสิ่งที่ฉันจะทำ นอกจากนี้สำหรับประโยคสุดท้ายของคุณ: รับประกันความปลอดภัยของประเภทอย่างไร
codebreaker

@ codebreaker - มันสอดคล้องกับสิ่งที่คุณทำในลูปในตัวอย่าง ถ้านั่นไม่มีความหมายอย่าเขียนลูป ถ้ามันไม่สมเหตุสมผลเป็นวิธีมันก็ไม่สมเหตุสมผลเหมือนวนลูป
Random832

1
และวิธีการของคุณ "รับประกัน" ความปลอดภัยแบบเดียวกับที่ใช้ในการหล่อ: โดยการโยนข้อยกเว้นหากวัตถุนั้นเป็นประเภทที่ไม่ถูกต้อง
Random832

ฉันรู้ว่าฉันเลือกตัวอย่างที่ไม่ดี ปัญหาคือวิธีการส่วนใหญ่ในคลาสย่อยเป็นเหมือน getters มากกว่าวิธีการกลายพันธุ์ คลาสเหล่านี้ไม่ได้ทำอะไรด้วยตัวเองเพียงแค่ให้ข้อมูลส่วนใหญ่ ความแตกต่างจะไม่ช่วยฉันที่นั่น ฉันกำลังติดต่อกับผู้ผลิตไม่ใช่ผู้บริโภค นอกจากนี้: การโยนข้อยกเว้นเป็นการรับประกันรันไทม์ซึ่งไม่ดีเท่ากับการรวบรวมเวลา
codebreaker

1
จุดของฉันคือรหัสของคุณให้การรับประกันรันไทม์เช่นกัน ไม่มีสิ่งใดหยุดยั้งบุคคลไม่สามารถตรวจสอบ isSub2 ก่อนที่จะโทรหาเป็น Sub2
Random832

-2

คุณสามารถผลักดันทั้งสองmakeASandwichและcontactAliensเข้าไปในชั้นฐานแล้วใช้พวกเขากับการใช้งานหุ่น เนื่องจากประเภท / อาร์กิวเมนต์ส่งคืนไม่สามารถแก้ไขเป็นคลาสพื้นฐานทั่วไปได้

class Sub1 extends Base{
    Sandwich makeASandwich(Ingredients i){
        //Normal implementation
    }
    boolean contactAliens(Frequency onFrequency){
        return false;
    }
 }
class Sub2 extends Base{
    Sandwich makeASandwich(Ingredients i){
        return null;
    }
    boolean contactAliens(Frequency onFrequency){
       // normal implementation
    }
 }

มีข้อเสียที่เห็นได้ชัดสำหรับสิ่งต่าง ๆ เช่นสิ่งที่หมายถึงสัญญาของวิธีการและสิ่งที่คุณสามารถและไม่สามารถอนุมานเกี่ยวกับบริบทของผลลัพธ์ คุณไม่สามารถคิดได้เพราะฉันไม่ได้ทำแซนวิชส่วนผสมก็หมดไปในความพยายาม ฯลฯ


1
ฉันคิดเกี่ยวกับการทำเช่นนี้ แต่เป็นการละเมิดหลักการทดแทน Liskov และไม่ดีในการทำงานเช่นเมื่อคุณพิมพ์ "sub1" และการเติมข้อความอัตโนมัติขึ้นมาคุณจะเห็นว่าทำ AS และไม่ใช่ติดต่อเอเลี่ยน
codebreaker

-5

สิ่งที่คุณกำลังทำนั้นถูกกฎหมายอย่างสมบูรณ์ อย่าให้ความสนใจกับผู้ที่มีความสำคัญกับลัทธิชาวต่างชาติเพราะพวกเขาอ่านมันในหนังสือบางเล่ม Dogma ไม่มีที่ในด้านวิศวกรรม

ฉันใช้กลไกเดียวกันสองสามครั้งและฉันสามารถพูดด้วยความมั่นใจว่า java runtime สามารถทำสิ่งเดียวกันได้อย่างน้อยในที่เดียวที่ฉันสามารถคิดได้ดังนั้นจึงเป็นการเพิ่มประสิทธิภาพการใช้งานและการอ่านโค้ดที่ ใช้มัน

ยกตัวอย่างเช่นjava.lang.reflect.Memberซึ่งเป็นฐานของjava.lang.reflect.Fieldและjava.lang.reflect.Methodและ(ลำดับชั้นที่เกิดขึ้นจริงนั้นซับซ้อนกว่านั้นเล็กน้อย แต่มันไม่เกี่ยวข้องเลย) เขตข้อมูลและวิธีการเป็นสัตว์ที่แตกต่างกันอย่างมากมาย: หนึ่งมีค่าที่คุณสามารถได้รับหรือตั้งค่าในขณะที่คนอื่นไม่มีสิ่งนั้น แต่มันสามารถเรียก จำนวนพารามิเตอร์และมันอาจส่งกลับค่า ดังนั้นเขตข้อมูลและวิธีการเป็นทั้งสมาชิก แต่สิ่งที่คุณสามารถทำได้กับพวกเขาเกี่ยวกับความแตกต่างจากกันและกันเช่นการทำแซนวิชและการติดต่อกับมนุษย์ต่างดาว

ตอนนี้เมื่อเขียนโค้ดที่ใช้การสะท้อนเรามักจะมีMemberในมือของเราและเรารู้ว่ามันเป็นMethodหรือField(หรือไม่ค่อยมีอะไรอย่างอื่น) และเราต้องทำทุกอย่างที่น่าเบื่อinstanceofเพื่อหาอย่างแม่นยำ มันคืออะไรแล้วเราต้องโยนมันเพื่อรับการอ้างอิงที่เหมาะสม (และนี่ไม่เพียง แต่น่าเบื่อ แต่ยังไม่ได้ผลดีมาก)Methodชั้นเรียนสามารถใช้รูปแบบที่คุณอธิบายได้ง่ายมากซึ่งจะทำให้ชีวิตของโปรแกรมเมอร์นับพันง่ายขึ้น

แน่นอนว่าเทคนิคนี้ใช้ได้เฉพาะในลำดับชั้นเล็ก ๆ ที่กำหนดไว้อย่างดีของคลาสที่มีการเชื่อมโยงอย่างใกล้ชิดซึ่งคุณมี (และจะมี) การควบคุมระดับซอร์สของ: คุณไม่ต้องการทำสิ่งนี้หากลำดับชั้นของคลาสของคุณคือ มีแนวโน้มที่จะขยายออกไปโดยคนที่ไม่มีอิสระที่จะ refactor ชั้นฐาน

นี่คือสิ่งที่ฉันได้ทำแตกต่างจากสิ่งที่คุณทำ:

  • ชั้นฐานให้การเริ่มต้นใช้งานสำหรับทั้งครอบครัวของวิธีการหนึ่งที่มีผลตอบแทนพวกเขาในแต่ละasDerivedClass()null

  • แต่ละชั้นมาเฉพาะแทนที่หนึ่งในasDerivedClass()วิธีการที่กลับมาแทนthis nullมันไม่ได้แทนที่ส่วนที่เหลือใด ๆ และไม่ต้องการรู้อะไรเกี่ยวกับพวกเขา ดังนั้นจึงไม่มีการIllegalStateExceptionโยน

  • คลาสพื้นฐานยังจัดให้มีfinalการใช้งานสำหรับวิธีการทั้งisDerivedClass()ตระกูลตามลำดับดังนี้: return asDerivedClass() != null; วิธีนี้จำนวนวิธีการที่จำเป็นต้อง overriden โดยคลาสที่ได้รับจะถูกย่อให้เล็กสุด

  • ฉันไม่ได้ใช้@Deprecatedในกลไกนี้เพราะฉันไม่ได้คิด ตอนนี้คุณให้ความคิดกับฉันแล้วฉันจะใช้มันขอบคุณ!

C # มีกลไกที่เกี่ยวข้องในตัวผ่านการใช้asคำหลัก ใน C # คุณสามารถพูดDerivedClass derivedInstance = baseInstance as DerivedClassและคุณจะได้รับการอ้างอิงถึงDerivedClassถ้าคุณbaseInstanceอยู่ในชั้นเรียนนั้นหรือnullถ้ามันไม่ได้ (ในทางทฤษฎี) นี้ทำงานได้ดีกว่าisตามด้วย cast ( isเป็นคำหลักที่ยอมรับได้ดีกว่า C # สำหรับinstanceof,) แต่กลไกที่กำหนดเองที่เราได้ทำการประดิษฐ์ด้วยมือนั้นทำได้ดียิ่งขึ้น: คู่ของinstanceof- and-cast ปฏิบัติการของ Java เช่นกัน เนื่องจากasโอเปอเรเตอร์ของ C # ไม่ดำเนินการอย่างรวดเร็วเท่ากับการเรียกใช้เมธอดเสมือนเดียวของวิธีการที่กำหนดเอง

ฉันขอยกข้อเสนอที่ว่าเทคนิคนี้ควรถูกประกาศว่าเป็นรูปแบบและพบชื่อที่ดีสำหรับมัน

Gee ขอบคุณสำหรับ downvotes!

บทสรุปของการโต้เถียงเพื่อช่วยคุณจากปัญหาในการอ่านความคิดเห็น:

การคัดค้านของผู้คนดูเหมือนว่าการออกแบบดั้งเดิมนั้นไม่ถูกต้องซึ่งหมายความว่าคุณไม่ควรมีคลาสที่แตกต่างกันอย่างมากมายซึ่งมาจากคลาสพื้นฐานทั่วไปหรือแม้ว่าคุณจะทำรหัสที่ใช้ลำดับชั้นนั้นไม่ควรอยู่ในตำแหน่งที่มี การอ้างอิงพื้นฐานและจำเป็นต้องคิดออกคลาสที่ได้รับ ดังนั้นพวกเขาจึงพูดว่ากลไกการหล่อตัวเองที่เสนอโดยคำถามนี้และโดยคำตอบของฉันซึ่งช่วยปรับปรุงการใช้งานการออกแบบดั้งเดิมไม่ควรมีความจำเป็นตั้งแต่แรก (พวกเขาไม่ได้พูดอะไรจริง ๆ เกี่ยวกับกลไกการหล่อตัวเองพวกเขาเพียงแค่บ่นเกี่ยวกับธรรมชาติของการออกแบบที่กลไกนั้นตั้งใจจะนำไปใช้)

อย่างไรก็ตามในตัวอย่างข้างต้นที่ผมได้อยู่แล้วแสดงให้เห็นว่าผู้สร้างของรันไทม์จาวาได้ในความเป็นจริงเลือกออกแบบได้อย่างแม่นยำเช่นสำหรับjava.lang.reflect.Member, Field, Methodลำดับชั้นและในการแสดงความคิดเห็นด้านล่างนี้ผมยังแสดงให้เห็นว่าผู้สร้างของ C รันไทม์ # ที่มาถึงอิสระที่เทียบเท่าการออกแบบสำหรับSystem.Reflection.MemberInfo, FieldInfo, MethodInfoลำดับชั้น ดังนั้นสิ่งเหล่านี้จึงเป็นสถานการณ์จำลองในโลกแห่งความจริงที่แตกต่างกันสองสถานการณ์ซึ่งอยู่ภายใต้จมูกของทุกคน

นั่นคือสิ่งที่ความคิดเห็นต่อไปนี้ทั้งหมดต้มลงไป กลไกการหล่อด้วยตนเองนั้นแทบจะไม่ได้กล่าวถึง


13
มันถูกกฎหมายถ้าคุณออกแบบตัวเองลงในกล่องแน่นอน นั่นเป็นปัญหาของคำถามประเภทนี้มากมาย ผู้คนตัดสินใจในด้านอื่น ๆ ของการออกแบบซึ่งบังคับใช้โซลูชันแฮ็คเหล่านี้เมื่อโซลูชันที่แท้จริงคือการหาสาเหตุที่คุณลงเอยในสถานการณ์นี้ตั้งแต่แรก การออกแบบที่ดีจะไม่ทำให้คุณอยู่ที่นี่ ขณะนี้ฉันกำลังดูแอปพลิเคชัน 200K SLOC และฉันไม่เห็นว่าจะต้องทำสิ่งนี้ ดังนั้นไม่ใช่แค่สิ่งที่คนอ่านในหนังสือ การไม่ได้อยู่ในสถานการณ์ประเภทนี้เป็นเพียงผลลัพธ์สุดท้ายของการปฏิบัติตามแนวทางปฏิบัติที่ดี
Dunk

3
@Dunk ฉันเพิ่งแสดงให้เห็นว่ามีความต้องการกลไกดังกล่าวอยู่ในjava.lang.reflection.Memberลำดับชั้น ผู้สร้าง java runtime เกิดขึ้นในสถานการณ์แบบนี้ฉันไม่คิดว่าคุณจะพูดว่าพวกเขาออกแบบตัวเองลงในกล่องหรือตำหนิพวกเขาเพราะไม่ปฏิบัติตามแนวทางปฏิบัติที่ดีที่สุด จุดที่ฉันทำที่นี่คือเมื่อคุณมีสถานการณ์ประเภทนี้กลไกนี้เป็นวิธีที่ดีในการปรับปรุงสิ่งต่าง ๆ
Mike Nakis

6
@MikeNakis ผู้สร้าง Java ได้ทำการตัดสินใจที่ไม่ดีมากมายดังนั้นเพียงอย่างเดียวก็ไม่ได้พูดอะไรมาก อย่างไรก็ตามการใช้ผู้เข้าชมจะดีกว่าการทดสอบแบบเฉพาะกิจ
Doval

3
นอกจากนี้ตัวอย่างมีข้อบกพร่อง มีMemberอินเตอร์เฟส แต่ทำหน้าที่ตรวจสอบตัวระบุการเข้าถึงเท่านั้น โดยทั่วไปแล้วคุณจะได้รับรายการวิธีการและคุณสมบัติและใช้วิธีนั้นโดยตรง (โดยไม่ต้องผสมกันดังนั้นจึงไม่List<Member>ปรากฏขึ้น) ดังนั้นคุณไม่จำเป็นต้องร่าย โปรดทราบClassว่าไม่ได้มีgetMembers()วิธีการ แน่นอนโปรแกรมเมอร์ clueless อาจสร้างList<Member>แต่ที่จะทำให้รู้สึกเล็กน้อยเป็นทำให้List<Object>และเพิ่มบางส่วนIntegerหรือStringมัน
SJuan76

5
@MikeNakis "ผู้คนจำนวนมากทำ" และ "คนที่มีชื่อเสียงทำมัน" ไม่ใช่ข้อโต้แย้งที่แท้จริง Antipatterns เป็นทั้งความคิดที่ไม่ดีและมีการใช้อย่างกว้างขวางโดยคำจำกัดความ แต่ข้อแก้ตัวเหล่านั้นจะแสดงให้เห็นถึงการใช้งานของพวกเขา ให้เหตุผลที่แท้จริงในการใช้การออกแบบบางอย่าง ปัญหาของฉันเกี่ยวกับการแคสติ้งแบบเฉพาะกิจคือผู้ใช้ API ทุกคนของคุณต้องได้รับการคัดเลือกที่ถูกต้องทุกครั้งที่พวกเขาทำ ผู้เข้าชมจะต้องแก้ไขเพียงครั้งเดียว การซ่อนการคัดเลือกผู้เล่นไว้ข้างหลังวิธีการบางอย่างและกลับมาnullแทนที่จะมีข้อยกเว้นไม่ได้ทำให้ดีขึ้นมากนัก ทุกอย่างเท่าเทียมกันผู้เยี่ยมชมปลอดภัยกว่าขณะทำงานเดียวกัน
Doval
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.