คลาสควรสื่อสารกับผู้ใช้อย่างไร


12

สถานการณ์

เว็บแอ็พพลิเคชันกำหนดอินเทอร์เฟซผู้ใช้แบ็กเอนด์IUserBackendด้วยวิธีการ

  • getUser (UID)
  • CreateUser (UID)
  • deleteUser (UID)
  • setPassword (uid, รหัสผ่าน)
  • ...

แบ็กเอนด์ผู้ใช้ที่แตกต่างกัน (เช่น LDAP, SQL, ... ) ใช้อินเทอร์เฟซนี้ แต่ไม่ใช่แบ็กเอนด์ทุกคนสามารถทำทุกอย่างได้ ตัวอย่างเช่นเซิร์ฟเวอร์ LDAP ที่เป็นรูปธรรมไม่อนุญาตให้แอปพลิเคชันเว็บนี้ลบผู้ใช้ ดังนั้นLdapUserBackendคลาสที่ใช้จะไม่ดำเนินการIUserBackenddeleteUser(uid)

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

วิธีแก้ปัญหาที่รู้จัก

ฉันได้เห็นวิธีแก้ปัญหาที่IUserInterfaceมีimplementedActionsวิธีการที่ส่งกลับจำนวนเต็มซึ่งเป็นผลมาจากการ bitwise ORs ของการกระทำ bitwise ANDed กับการกระทำที่ร้องขอ:

function implementedActions(requestedActions) {
    return (bool)(
        ACTION_GET_USER
        | ACTION_CREATE_USER
        | ACTION_DELTE_USER
        | ACTION_SET_PASSWORD
        ) & requestedActions)
}

ที่ไหน

  • ACTION_GET_USER = 1
  • ACTION_CREATE_USER = 2
  • ACTION_DELETE_USER = 4
  • ACTION_SET_PASSWORD = 8
  • .... = 16
  • .... = 32

เป็นต้น

ดังนั้นเว็บแอปพลิเคชั่นจึงกำหนด bitmask ตามความต้องการและimplementedActions()คำตอบด้วยบูลีนว่ามันรองรับหรือไม่

ความคิดเห็น

การดำเนินการบิตเหล่านี้สำหรับฉันดูเหมือนว่าเป็นสิ่งที่สืบทอดกันมาตั้งแต่อายุ C ไม่จำเป็นต้องเข้าใจง่ายในแง่ของโค้ดที่สะอาด

คำถาม

รูปแบบทันสมัย ​​(ดีกว่า) คืออะไรสำหรับชั้นเรียนเพื่อสื่อสารชุดย่อยของวิธีการเชื่อมต่อที่ใช้? หรือ "วิธีการใช้งานบิต" จากด้านบนยังเป็นแนวปฏิบัติที่ดีที่สุด

( ในกรณีที่มีความสำคัญ: PHP แม้ว่าฉันกำลังมองหาโซลูชันทั่วไปสำหรับภาษา OO )


5
วิธีแก้ปัญหาทั่วไปคือการแยกส่วนต่อประสาน IUserBackendไม่ควรมีdeleteUserวิธีการที่ทุกคน ควรเป็นส่วนหนึ่งของIUserDeleteBackend(หรืออะไรก็ตามที่คุณต้องการเรียก) รหัสที่ต้องการลบผู้ใช้จะมีอาร์กิวเมนต์IUserDeleteBackendรหัสที่ไม่ต้องการให้ฟังก์ชันการทำงานใช้IUserBackendและจะไม่มีปัญหาใด ๆ กับวิธีการที่ไม่ได้ใช้งาน
Bakuriu

3
การพิจารณาการออกแบบที่สำคัญคือความพร้อมใช้งานของการดำเนินการขึ้นอยู่กับสถานการณ์ของรันไทม์ เป็นเซิร์ฟเวอร์ LDAP ทั้งหมดที่ไม่สนับสนุนการลบหรือไม่ หรือว่าเป็นคุณสมบัติของการกำหนดค่าของเซิร์ฟเวอร์และอาจมีการเปลี่ยนแปลงเมื่อระบบรีสตาร์ท? หากตัวเชื่อมต่อ LDAP ของคุณค้นพบสถานการณ์นี้โดยอัตโนมัติหรือควรกำหนดให้การกำหนดค่าเปลี่ยนเป็นตัวเชื่อมต่อ LDAP ตัวอื่นที่มีความสามารถต่างกันหรือไม่ สิ่งเหล่านี้มีผลกระทบอย่างมากในการแก้ปัญหาที่ทำงานได้
Sebastian Redl

@SebastianRedl ใช่นั่นเป็นสิ่งที่ฉันไม่ได้คำนึงถึง ฉันต้องการทางออกสำหรับ runtime จริงๆ เนื่องจากฉันไม่ต้องการลบล้างคำตอบที่ดีมากฉันจึงเปิดคำถามใหม่ที่มุ่งเน้นไปที่รันไทม์
problemofficer

คำตอบ:


24

การพูดอย่างกว้าง ๆ มีสองวิธีที่คุณสามารถทำได้ที่นี่: การทดสอบและการโยนหรือการจัดวางผ่านความหลากหลาย

ทดสอบและโยน

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

จากนั้นหากsupportsDelete()ส่งคืนการfalseโทรdeleteUser()อาจส่งผลให้เกิดNotImplementedExeptionการโยนหรือวิธีการก็ไม่ได้ทำอะไรเลย

นี่เป็นวิธีที่ได้รับความนิยมในหมู่บางคนเพราะมันง่าย อย่างไรก็ตามหลายคนรวมตัวเองยืนยันว่ามันเป็นการละเมิดหลักการทดแทนของ Liskov (L ใน SOLID) และดังนั้นจึงไม่ใช่ทางออกที่ดี

องค์ประกอบผ่าน Polymorphism

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

ด้วยวิธีนี้LdapUserBackendไม่ได้ใช้งานIDeletableUserและคุณทำการทดสอบโดยใช้การทดสอบเช่น (ใช้ไวยากรณ์ C #):

if (backend is IDeletableUser deletableUser)
{
    deletableUser.deleteUser(id);
}

(ฉันไม่แน่ใจเกี่ยวกับกลไกใน PHP ในการพิจารณาว่าอินสแตนซ์ใช้อินเทอร์เฟซและวิธีที่คุณส่งไปยังอินเทอร์เฟซนั้นหรือไม่ แต่ฉันแน่ใจว่ามีภาษาเทียบเท่ากัน)

ข้อดีของวิธีนี้คือการใช้ polymorphism เพื่อเปิดใช้งานโค้ดของคุณให้สอดคล้องกับหลักการของ SOLID และสง่างามในมุมมองของฉัน

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


4
ข้อพิจารณาในการออกแบบสำหรับ +1 สำหรับ SOLID ดีเสมอที่จะแสดงคำตอบด้วยวิธีการต่าง ๆ ที่จะทำให้โค้ดสะอาดต่อ
แม็กเคเล็บ

2
ใน PHP มันจะเป็นif (backend instanceof IDelatableUser) {...}
Rad80

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

มีข้อยกเว้น (ปุนไม่ได้ตั้งใจ) สำหรับหลักการที่จะไม่โยนประเภท สำหรับ C #, NotImplementedExceptionที่อยู่คือ ข้อยกเว้นนี้มีไว้สำหรับการหยุดทำงานชั่วคราวเช่นรหัสที่ยังไม่ได้พัฒนา แต่จะได้รับการพัฒนา นั่นไม่เหมือนกับการตัดสินใจอย่างชัดเจนว่าชั้นเรียนที่กำหนดจะไม่ทำอะไรด้วยวิธีการที่กำหนดแม้หลังจากการพัฒนาเสร็จสมบูรณ์
Flater

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

5

สถานการณ์ปัจจุบัน

การตั้งค่าปัจจุบันละเมิดหลักการแยกส่วนต่อประสาน (I ใน SOLID)

การอ้างอิง

ตามที่วิกิพีเดียหลักการอินเตอร์เฟซที่แยกจากกัน (ISP) กล่าวว่าลูกค้าที่ไม่ควรถูกบังคับให้ขึ้นอยู่กับวิธีการก็ไม่ได้ใช้ หลักการแยกส่วนต่อประสานถูกกำหนดขึ้นโดย Robert Martin ในช่วงกลางทศวรรษ 1990

กล่าวอีกนัยหนึ่งถ้านี่เป็นอินเทอร์เฟซของคุณ:

public interface IUserBackend
{
    User getUser(int uid);
    User createUser(int uid);
    void deleteUser(int uid);
    void setPassword(int uid, string password);
}

ดังนั้นทุกคลาสที่ใช้อินเทอร์เฟซนี้จะต้องใช้ทุกวิธีที่ระบุไว้ของอินเตอร์เฟส ไม่มีข้อยกเว้น.

ลองนึกภาพถ้ามีวิธีการแบบทั่วไป:

public void HaveUserDeleted(IUserBackend backendService, User user)
{
     backendService.deleteUser(user.Uid);
}

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


โซลูชันที่คุณเสนอ

ฉันได้เห็นวิธีแก้ปัญหาที่ IUserInterface มีวิธี ImplementActions ที่คืนค่าจำนวนเต็มซึ่งเป็นผลมาจากค่าบิต OR ของการกระทำ bitwise ANDed ด้วยการกระทำที่ร้องขอ

สิ่งที่คุณต้องการทำคือ:

public void HaveUserDeleted(IUserBackend backendService, User user)
{
     if(backendService.canDeleteUser())
         backendService.deleteUser(user.Uid);
}

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

นั่นจะช่วยแก้ปัญหาใช่มั้ย ในทางเทคนิคแล้วมันก็เป็นเช่นนั้น แต่ตอนนี้คุณกำลังละเมิดหลักการทดแทน Liskov (L ใน SOLID)

ละทิ้งความซับซ้อนค่อนข้างคำอธิบายที่วิกิพีเดียผมพบว่าเป็นตัวอย่างที่ดีใน StackOverflow สังเกตตัวอย่าง "เลว":

void MakeDuckSwim(IDuck duck)
{
    if (duck is ElectricDuck)
        ((ElectricDuck)duck).TurnOn();

    duck.Swim();
}

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

สิ่งนี้เอาชนะวัตถุประสงค์ของการพัฒนาแนวทางที่เป็นนามธรรม

หมายเหตุ: ตัวอย่างที่นี่แก้ไขได้ง่ายกว่าเคสของคุณ ตัวอย่างก็พอเพียงที่จะมีElectricDuckการเปิดตัวเองในภายในSwim()วิธี เป็ดทั้งสองยังสามารถว่ายน้ำได้ดังนั้นผลการทำงานก็เหมือนกัน

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


ทางออกที่ฉันเสนอ

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

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

วิธีการแก้ปัญหาที่นี่คือการแยกอินเตอร์เฟซ

public interface IGetUserService
{
    User getUser(int uid);
}

public interface ICreateUserService
{
    User createUser(int uid);
}

public interface IDeleteUserService
{
    void deleteUser(int uid);
}

public interface ISetPasswordService
{
    void setPassword(int uid, string password);
}

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

สิ่งนี้ช่วยให้คุณสามารถผสมผสานอินเตอร์เฟสและจับคู่ตามที่คุณต้องการ:

public class UserRetrievalService 
              : IGetUserService, ICreateUserService
{
    //getUser and createUser methods implemented here
}

public class UserDeleteService
              : IDeleteUserService
{
    //deleteUser method implemented here
}

public class DoesEverythingService 
              : IGetUserService, ICreateUserService, IDeleteUserService, ISetPasswordService
{
    //All methods implemented here
}

ทุกคลาสสามารถตัดสินใจได้ว่าพวกเขาต้องการทำอะไรโดยไม่ทำลายสัญญาของอินเทอร์เฟซ

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

public void HaveUserDeleted(IDeleteUserService backendService, User user)
{
     backendService.deleteUser(user.Uid); //guaranteed to work
}

หากใครพยายามส่งวัตถุที่ไม่ได้ใช้IDeleteUserServiceโปรแกรมจะปฏิเสธที่จะรวบรวม นี่คือเหตุผลที่เราชอบความปลอดภัยประเภท

HaveUserDeleted(new DoesEverythingService());    // No problem.
HaveUserDeleted(new UserDeleteService());        // No problem.
HaveUserDeleted(new UserRetrievalService());     // COMPILE ERROR

เชิงอรรถ

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

ตัวอย่างเช่นหากทุกบริการที่สามารถสร้างผู้ใช้สามารถลบผู้ใช้ (และในทางกลับกัน) คุณสามารถใช้วิธีการเหล่านี้เป็นส่วนหนึ่งของอินเทอร์เฟซเดียว:

public interface IManageUserService
{
    User createUser(int uid);
    void deleteUser(int uid);
}

ไม่มีประโยชน์ทางเทคนิคทำเช่นนี้แทนที่จะแยกเป็นชิ้นเล็ก ๆ แต่จะทำให้การพัฒนาง่ายขึ้นเล็กน้อยเพราะต้องใช้การชุบไอน้ำน้อย


+1 สำหรับการแยกอินเทอร์เฟซโดยพฤติกรรมที่พวกเขาสนับสนุนซึ่งเป็นจุดประสงค์ทั้งหมดของอินเทอร์เฟซ
Greg Burghardt

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

@problemofficer: การประเมินผลรันไทม์สำหรับกรณีเหล่านี้ไม่ค่อยเป็นตัวเลือกที่ดีที่สุด แต่มีหลายกรณีที่ต้องทำเช่นนั้น ในกรณีเช่นนี้คุณสามารถสร้างวิธีการที่สามารถเรียกได้ แต่อาจท้ายไม่ได้ทำอะไร (เรียกว่าTryDeleteUserเพื่อสะท้อนให้เห็นว่า); หรือคุณมีวิธีการโยนข้อยกเว้นถ้ามันเป็นสถานการณ์ที่เป็นไปได้ แต่มีปัญหา การใช้วิธีการCanDoThing()และDoThing()วิธีการทำงาน แต่มันจะต้องมีผู้โทรภายนอกของคุณที่จะใช้สองสาย (และได้รับการลงโทษสำหรับความล้มเหลวในการทำเช่นนั้น) ซึ่งใช้งานง่ายน้อยลงและไม่สง่างาม
Flater

0

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

นี่คือสิ่งที่ Java ทำกับEnumSet (ลบด้วยไวยากรณ์น้ำตาล แต่เดี๋ยวก่อนมันเป็น Java)


0

ใน. NET โลกคุณสามารถตกแต่งวิธีการและชั้นเรียนด้วยคุณลักษณะที่กำหนดเอง สิ่งนี้อาจไม่เกี่ยวข้องกับกรณีของคุณ

ฟังดูแล้วว่าปัญหาที่คุณมีอาจเกี่ยวข้องกับการออกแบบในระดับที่สูงกว่า

หากนี่เป็นคุณลักษณะ UI เช่นหน้าแก้ไขผู้ใช้หรือส่วนประกอบดังนั้นความสามารถที่แตกต่างกันจะถูกปกปิดออกมาได้อย่างไร ในกรณีนี้ 'ทดสอบและโยน' จะเป็นวิธีที่ไม่มีประสิทธิภาพสำหรับวัตถุประสงค์นั้น โดยถือว่าก่อนที่จะโหลดทุกหน้าที่คุณเรียกใช้จำลองการโทรไปยังแต่ละฟังก์ชันเพื่อพิจารณาว่าควรซ่อนเครื่องมือหรือองค์ประกอบหรือแสดงต่างกัน อีกวิธีหนึ่งคือคุณมีหน้าเว็บที่บังคับให้ผู้ใช้ค้นพบสิ่งที่มีอยู่โดยใช้ 'ทดสอบและโยน' ด้วยตนเองไม่ว่าจะใช้เส้นทางการเข้ารหัสใดเนื่องจากผู้ใช้ไม่พบว่ามีบางสิ่งไม่พร้อมใช้งานจนกว่าจะมีคำเตือนป๊อปอัปปรากฏขึ้น

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

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

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

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.