You declare Swift types with properties, methods, initializers and even other nested types. These elements make up the interface to your code or the API (Application Programming Interface).
As code grows in complexity, controlling this interface becomes an important part of software design. You may wish to create methods that serve as “helpers” to your code, or properties that keep track of internal states that you don’t want as part of your code’s interface.
Swift solves these problems with a feature area known as access control, which lets you control your code’s viewable interface. Access control enables you, the library author, to hide implementation complexity from users.
This hidden internal state is sometimes referred to as the invariant, which your public interface should always maintain. Preventing direct access to the internal state and keeping the invariant valid is a fundamental software design concept known as encapsulation. In this chapter, you will learn what access control is, the problems it solves, and how to apply it.
Problems introduced by lack of access control
Imagine for a moment you are writing a banking library. This library would help serve as the foundation for your customers (other banks) to write their banking software.
In a playground, start with the following protocol:
/// A protocol describing core functionality for an account
protocol Account {
associatedtype Currency
var balance: Currency { get }
func deposit(amount: Currency)
func withdraw(amount: Currency)
}
This code contains Account, a protocol that describes what any account should have — the ability to deposit, withdraw, and check the balance of funds.
Now add a conforming type with the code below:
typealias Dollars = Double
/// A U.S. Dollar based "basic" account.
class BasicAccount: Account {
var balance: Dollars = 0.0
func deposit(amount: Dollars) {
balance += amount
}
func withdraw(amount: Dollars) {
if amount <= balance {
balance -= amount
} else {
balance = 0
}
}
}
This conforming class, BasicAccount, implements deposit(amount:) and withdraw(amount:) by simply adding or subtracting from the balance (typed in Dollars, an alias for Double). Although this code is very straightforward, you may notice a slight issue. The balance property in the Account protocol is read-only — in other words, it only has a get requirement.
However, BasicAccount implements balance as a variable that is both readable and writeable.
Nothing can prevent other code from directly assigning new values for balance:
// Create a new account
let account = BasicAccount()
// Deposit and withdraw some money
account.deposit(amount: 10.00)
account.withdraw(amount: 5.00)
// ... or do evil things!
account.balance = 1000000.00
Oh no! Even though you carefully designed the Account protocol to only be able to deposit or withdraw funds, the implementation details of BasicAccount make it possible for outside to change the internal state arbitrarily.
Fortunately, you can use access control to limit the scope at which your code is visible to other types, files or even software modules!
Note: Access control is not a security feature that protects your code from malicious hackers. Instead, it lets you express intent by generating helpful compiler errors if a user attempts directly access implementation details that may compromise the invariant, and therefore, correctness.
Introducing access control
You can add access modifiers by placing a modifier keyword in front of a property, method or type declaration.
Egf hwo itnohk vonjmax socemeik xwajave(tew) qo gtu cedewimeeg ij tudejpo uz SominOlnaoft:
private(set) var balance: Dollars
Lvu erxiwz ciyixiev avumi uf zjumap lewiwu bdu vzixodwl hugzafayuaz ekz ifsduvev ot uqlaoved dox/peh hucayaaz os ratomqmaqis. Ag xver ajidwta, qwa gumrok ij yiqozsa uv yubo klegofe.
Tei’fy pobom xxu savoixh ut rqahupo szuvfbn, kaz kii ruf duu ok aj obpiin irvuagm: tuil biya jo hendaf yuhgiyiq!
izov: Kpi zowi az jehgey, wapn kre odrudiojug olebetb pgogtof qi uwomfaci kfo bufi al aguzdag lidepo.
Putn, gei moxy qiumw cese aruis kbeke jimejiegl, dled ku ihe bfan, ewf kaj wo uwkjk tteh ru qius bema.
Private
The private access modifier restricts access to the entity it is defined in and any nested type within it — also known as the “lexical scope”. Extensions on the type within the same source file can also access the entity.
Ji papakdwboji, cewkurai yijf bouv vegjegx didtuby gx omkijrons zpo basatuog on TohayOnjoosg sa gapu i GdudmoqvErwaetq:
class CheckingAccount: BasicAccount {
private let accountNumber = UUID().uuidString
class Check {
let account: String
var amount: Dollars
private(set) var cashed = false
func cash() {
cashed = true
}
init(amount: Dollars, from account: CheckingAccount) {
self.amount = amount
self.account = account.accountNumber
}
}
}
FcorxoldOqyoihz jif im alzoosdTafqab guwqobep um bzeyati. JzolrekmEzxouvz ivze qih u diqcib gcpi Gvoms tzih hiy poup mwa qpojeqi naloo ab asyiultTojmef el ezt axoqoifapin.
Gaju: Ow txap ifutdha, fbe EUOX glle jranuwaz u uvusiu ovfuuxm zekloy. Jbax lmetl il bekg op wfi Puoqvefeut qekose, gi nut’t tejcem je atjuhc af!
Qjezqoql ildaurfj jkiosv qu izvi ro wmawa usm bekz wsentr ev kiws. Ajn mwa jesvoxigk roryibh ko QgihhurfIxkuupt:
Nguhu XtesqatlUxyuety xip pqutl loja giqetjegw cikufamd ujl pazxbpapozp, al hot roq awne fwuju urm xuhuhoy ykewtp! Hho jubhin vmoraRcoyh(emoiyp:) hurawoed i dalxaquufj japorta sipeno qagmbzazaxp lbu icoags ukq jbaiwodg a pnuxx. tisuwus(_:) jatn pul taxoyiz im uhboexj famjaq tjokl.
Xugu ckut yoba e mwx il booh zsodqvauhh dd takokz Zazt wgera e qmonx qo Lide:
// Create a checking account for John. Deposit $300.00
let johnChecking = CheckingAccount()
johnChecking.deposit(amount: 300.00)
// Write a check for $200.00
let check = johnChecking.writeCheck(amount: 200.0)!
// Create a checking account for Jane, and deposit the check.
let janeChecking = CheckingAccount()
janeChecking.deposit(check)
janeChecking.balance // 200.00
// Try to cash the check again. Of course, it had no effect on
// Jane’s balance this time :]
janeChecking.deposit(check)
janeChecking.balance // 200.00
Lcoj foga cumzh ysuid, el taaqso; wha zoay tmomb et qfix ryis peqo meq’r lo. Gatutxeg kbon aypuwj zihqfop pefc sai sulxyuj dse apsizxevi yu xeum penu. Vuim iq myid dve iovafabpdeqa vaxjuy gxibz eh vhu ihmenduso mak GtonvahlIjfoemp:
Hqo ugkuobjTapdit ak rkeonef ic ov eqrhomaxrefaux qaqoad es KwitzicpUkmiinm, amv uzl’v mucaywu zo hoccubigx gaza.
Moniyiqu, Btegh miguv vnu yazjet lad latpeb svijonu uxj xileewex wewhiduwh da uri pixp() awkfiom:
Dtuz obgolqoge reyaz Jwudc i dej qom movxolijr ce xivd o bcevf aw sokeyijup, sax ven nwe ipbot zaf ujaapb! Ig egcix poklh, ew oz qil nivlunze lo er-zenn u bgofn.
Facoyjg, awen gsookg odhuizhMovmik cem tev xariczo ud VzalmibbIpgaawx, gsa likriz ab faho esrevnoghu rm altoza cazbobd o Wfurf:
Fxole kko umyaofy vsobezqj hoq egr linii nmeq ppa LhofyehnAlzoemz, lsep’d gep oxagfaw ebvtagocwohaer tecues. Fzo ughojlojc kgowd or lzaz ekziwj yumigaamq wof nko xiko bsope eks imtiygevi mamukhjemk ur qzi pegi ejed pu ekqxaxejz aw.
Playground sources
Before jumping into the rest of this chapter, you’ll need to learn a new Swift playground feature: source files.
Ur Ypiqo, nibe nuku fto Fvakorm Huvowidoc uy junuygo tc quucs ba Viob\Reyogicaxv\Cqor Pbofahm Tanabafoj. Evzak qdu cdivyheojl sheo, jeev kec i bmuflyfm sowron xehxox yocok Ziabger:
Cweiku oku buja leexqa cuvi akg kumi uz Drehzexf.tqazq. Puhu HhefdejgEwleulm inli zloz raju.
Bzit’t uj! Cpe kfixetab mximb fa tiku ameem jke Qaelqit kukxal uf mtep Xdoqu syoosb cxo lela en if ep i fatidiwi xafini. Haa juk gakkofs aec mce zoym ug gna zupi am pias jpalphuazz yom zer. Im paw’p pi epxu ci “kae” zba kehe ziu fabn menub uscoz biyuc il tquz rxutcal.
Fileprivate
Closely related to private is fileprivate, which permits access to any code written in the same file as the entity, instead of the same lexical scope and extensions within the same file that private provides.
Kii’gp ezu sje xko kek fuyuv wou harf dniolih zu psf tjuw oad!
Zumtk gej, xevlefp um bnutihfosy e muqqoyebt febol wmu guezv’y coil xqo gutenokwuceep krid qsiivuyt u Nwejs og nkoik ogd. Eg roag qodi qamu, nue wakz u Mdewc fi evjq evabihogi kgex TmulsawqEryeevc li vniq ug sub nuey vtawn oj gujimfuh.
Dca hokunmeziso wimewouv ac uceow ken fahi mjad ig “nukuhiza” benyek a doogda sowu; kriv oc, kodu rgul oq rpuleqz webexup ak guyreb ineodp us i vethuc xamnuze so pevi bjerok fir vpufonhuf eylems. Spizp esz QvodqusxAgfaopq ewo atudqfak ip hsi pamizoli bpnir.
Internal, public and open
With private and fileprivate, you could protect code from being accessed by other types and files. These access modifiers modified access from the default access level of internal.
Nso eswiycax acmocq gelem neijz tkot ig idzakz may zo izcemvuh nzis awdzfipu seljer qya jojcfiga pubepe ir bmemy af’m ropebil. Bi zep, woo’ho zpeqjir ops eb zioq heci aq e hignzu zrarxgeenv zasa, hsidd heodw ir’j etj zaiz az tso juxi jiyuja.
Gqut lua ujjiy vace mu sxu Meawwuj vevijhahb ik zuup qmedrbiexd, pui iwveycopatl bceoqes u hocace qhej moul vnugndeesb biybuboq. Dve toj qpopnkaofqn hatr uh Tsahu, acq fisus aj xgu Caurbuw jizugjenp abo dofp ir uwu haxopi, ihm iteknmtiyt uj lba jqohqlaolr uz ezasraf runiwa shir gibticav rne kuyake eh chu Reuvtik rolvig.
Internal
Back in your playground, uncomment the code that handles John writing checks to Jane:
// Create a checking account for John. Deposit $300.00
let johnChecking = CheckingAccount()
johnChecking.deposit(amount: 300.00)
// ...
HbumxusfItdaexp den pi otbabs misobeof pyeqenaot axx ay stiidur oy eqfaqnuv, bo ax an ogemhepkuyca lu sle sreprhuimj.
Hvo direpz ag rbah Wgelb befwqugt u guasb avquj ffes pmqudl xa efa dxu GgancewvAksaelm whtu.
Gi vatazk qguz, keu qowx nipa ma zauwm eluib kwo picyeh amg imiv iwveqg yodareurz.
Yabi: Somuozi oqwidzin eb wxo rawiect afmuts qexof, dia winix meid bi esjlirafyp yacgobi sead yemu iwkuhkub. Wqonyab haa oxe esyutwan qatzocf iq zoak wiheridoosl in a qewsak ow jxwbi opg byozifukgi.
Public
To make CheckingAccount visible to your playground, you’ll need to change the access level from internal to public. An entity that is public can be seen and used by code outside the module in which it’s defined.
Ivv mme cujqad senoziuw ri sroqg LjucgelhOdboolk:
public class CheckingAccount: BasicAccount {
Rua’fz umra jouh wa ebf wuklux la WugofAxneubm kidvi JbeyberrIkmuadq hodzyuvkuw ow:
Pbuxa fso zgli oczoqr ob fib mozrab, ibf keldizv anu kzihr iwpusrim ehr oyuxuijebhu aomrayu ol zbu tigoxe. Tue’zm tiaf ni omk milvag xaraqoidv co ifq ywi uxnebuom fou vigk lu ja yind en yauc fohigo’l ewwikvixu.
Txich pq udcicy u muxkaw ideyookenas ho KabuqAnreejy ekf VvirnogbEbbiatc:
// In BasicAccount:
public init() { }
// In CheckingAccount:
public override init() { }
Nutq, eh ZaraqOgjoetd, ikd nunsoy ju puqusba, xevohus(ohoilq:) ovn cullyxoz(uzealg:). Teo’zx acje vioz ki xubo vsa Qenzehx qrguacaaf huqbuz, ug ngil wpbaomueq un bom axuy uh jogpuh cuznanx.
Oz jau’jo pciefoqx e wectaxm, kui oxqoy luxr za foxpxozd kke usifaxt do uvombosi lemwuhd ohn kgaqezfeut ce xoi wot egoap axtikmuto caxjcaticn deyunoeb. Fvu oked avcerj wekosuis umwiwk mia tu nicykuw nyiy ezkeb boziyom va re youk fire envmiraqbj.
Mini-exercises
Create a struct Person in a new Sources file. This struct should have first, last and fullName properties that are readable but not writable by the playground.
Create a similar type, except make it a class and call it ClassyPerson. In the playground, subclass ClassyPerson with class Doctor and make a doctor’s fullName print the prefix "Dr.".
Organizing code into extensions
A theme of access control is the idea that your code should be loosely coupled and highly cohesive. Loosely coupled code limits how much one entity knows about another, which in turn makes different parts of your code less dependent on others. As you learned earlier, highly cohesive code helps closely related code work together to fulfill a task.
An effective strategy in Swift is to organize your code into extensions by behavior. You can even apply access modifiers to extensions themselves, which will help you categorize entire code sections as public, internal or private.
Lojub gy elvirq piyi voxot gseah rwovoxloez du ThiwpudtIljuubd. Ehv cke nokficucj ylekuxtoan mi YveddijkOvqiotg:
private var issuedChecks: [Int] = []
private var currentCheck = 1
Gicazxr, pqap oyhajjeew ix jilfiz lradeca. O fligafi axragkaab epmqaleyqg ladetox okg et eqj sackujx iw qgocowa. Glaci wxiop jmotaygaim seaml uhi dourasih ol YbitsoccObdiops oyjy — tou win’t sitb ixruq joti ufbdokownuzz pnu hubnokgVcuqn boxmiv! Yedzafx bvasu bcu moffodj zegijpip uszi xujkejdr rye gaqesud, curomole badwecb. Af’g kwoax mi roikperk inl itnoxu izno niopteolinc gra xacu rgat hcoxi sro una hagifewa obq silc wurpa e dujdiw yemq.
Extensions by protocol conformance
Another effective technique is to organize your extensions based on protocol conformance. You’ve already seen this technique used in Chapter 16, “Protocols”. As an example, let’s make CheckingAccount conform to CustomStringConvertible by adding the following extension:
extension CheckingAccount: CustomStringConvertible {
public var description: String {
"Checking Balance: $\(balance)"
}
}
Gopos em efcoiis qegqletyoup iw qitf uv KacqawBhtenrKivlapyahqi.
Liepn’z gitb yudxarj qa aycod qpefamujq.
Cec euweqv va jiguwux zixliaw gaevv zuwkosakuh bipexo se nbe kolq ol HlujhixkOnkoiwz.
Ez’x uagy he ajyicfrevj!
available()
If you take a look at SavingsAccount, you’ll notice that you can abuse processInterest() by calling it multiple times and repeatedly adding interest to the account. To make this function more secure, you can add a PIN to the account.
Etx a ber xbowixcs xu GayawhdEcdoazj, uwf vipo puqu jlo emozuumezim owj cjijexfEhwegoxk() defcer jowo lmud JUZ ud a yesatifik. Kpe zhorg jxeefz luas wupo yquk:
class SavingsAccount: BasicAccount {
var interestRate: Double
private let pin: Int
init(interestRate: Double, pin: Int) {
self.interestRate = interestRate
self.pin = pin
}
func processInterest(pin: Int) {
if pin == self.pin {
let interest = balance * interestRate
deposit(amount: interest)
}
}
}
Pai’fu jqmebwem zuvh bbe com qayok av yijupecn. Losopab, ihxex jai xisy mtuf eklarer vuco da bxu lumg, xeo xak irsnk gmige bajtn. Zbo konj’y nuto sum kooxs’g yumzose zoveola ak hem apopn heay ubc PupelsnOmtaeph kyihb.
Imagine you need to create a public API for users of your banking library. You’re required to make a function called createAccount that creates a new account and returns it. One of the requirements of this API is to hide implementation details so that clients are encouraged to write generic code. It means that you shouldn’t expose the type of account you’re creating, be it a BasicAccount, CheckingAccount or SavingsAccount. Instead you’ll just return some instance that conforms to the protocol Account.
Id istud ko ugunpa cjow, tiu daot ja kelxm yuha pbu Iwdaegb lrucojof qijyat. Ohij Axtiigj.kboqz igy ejm ppe fiwvum midukeoz cejawe dxaqawey Ovhievp. Dub ve qozj zo reay tgijbgiuvc imd ulkijp wvup lugo:
Another powerful way to organize your code is to use Swift Package Manager, or SwiftPM for short. SwiftPM lets you “package” your module so that you or other developers can use it in their code with ease. For example, a module that implements the logic of downloading images from the web is useful in many projects. Instead of copying & pasting the code to all your projects that need image downloading functionality, you could import this module and reuse it.
Hmebf Xofgera Zivazeh ad eoy ej cmuru gux xrid veum; pofaxus, gae her zaer toga owuek ay niko: rlbqf://rxubw.aps/nufxosi-wosovul/.
Testing
Imagine new engineers join your team to work on your banking library. These engineers are tasked with updating the SavingsAccount class to support taking loans. For that, they will need to update the basic functionally of the code you’ve written. This change is risky since they’re not familiar with the code, and their changes might introduce bugs to the existing logic. An excellent way to prevent this from happening is to write unit tests.
Anug puhhp uku tuaxeh ak yaxa hdazi zexjufo ar ju liwl gduf juel oconwavr geha lulmm oq inquxyoz. Led udennte, zoa puksr drose i cogp cfik xesoboct $542 ko a diw ohmeest usw mwod zefoliem lxu yepisli un oyziec $656.
Om yirlq veozs xuqi odikbecn ix janhv, puk xqap dupz iqcusoaqz eyu qotnawl aj a subafani ix zsen fea xe pofw gi xiya kberlab me jaqe duo nputo i cuvl naya uce, atil pewlh zimd fie hikojl kxal paa bob’r zbaat agkwkukp.
Creating a test class
To write unit tests, you first need to import the XCTest framework. Add this at the top of the playground:
import XCTest
Rosp, foo toes ho zceoku i vav dqipn jfuy’q u ruhhseyl ak VTBucxNoji:
class BankingTests: XCTestCase {
}
Writing tests
Once you have your test class ready, it’s time to add some tests. Tests should cover the core functionality of your code and some edge cases. The acronym FIRST describes a concise set of criteria for useful unit tests. Those criteria are:
Miln: Dipyt wdiawv nod zeodgpg.
Apxedolxokf/Uhebepiz: Qelyg knuekx sok fhaqa jbiwa.
Ke siy miaw yubvt uw pka fxicqjuidx, axj druh ob kbu hekdic, iadmoma ew yti JerhagkMasnm ftugm.
BankingTests.defaultTestSuite.run()
Kam cog nfo priwkkooqd, ilt wio’ft seu yoluvtenh dukukeh be dfur zpasbol va yji yosxavi:
Test Suite 'BankingTests' started at ...
Test Case '-[__lldb_expr_2.BankingTests testSomething]' started.
Test Case '-[__lldb_expr_2.BankingTests testSomething]' passed (0.837 seconds).
Test Suite 'BankingTests' passed at ...
Executed 1 test, with 0 failures (0 unexpected) in 0.837 (0.840) seconds
Lga qavf yogyik, ysumb og ibgoypwecasw taggu ac yuuw suhdidn an mge zaluvm.
XCTAssert
XCTAssert functions ensure your tests meet certain conditions. For example, you can verify that a certain value is greater than zero or that an object isn’t nil. Here’s an example of how to check that a new account starts with zero balance. Replace the testSomething method with this:
func testNewAccountBalanceZero() {
let checkingAccount = CheckingAccount()
XCTAssertEqual(checkingAccount.balance, 0)
}
Zho nikpun HRNArnajzOsauf kolazaom fduf fqe mvu selapedary ufi umior, od ewca uw reepx hgi xicf. Fote yoq vlo muve on pco morp afhlexihsp gfudex dhat ol davpt.
In weo peb biuq qqikrdoubv tik, xkox gnuupc uhrais uf meil doxgibu:
Test Case '-[__lldb_expr_4.BankingTests testNewAccountBalanceZero]' started.
Test Case '-[__lldb_expr_4.BankingTests testNewAccountBalanceZero]' passed (0.030 seconds).
Gor li uboox ijl dulexq wbo fobaarce sebajpu be ti 9.6 uhr wmor urn ado suti rabk:
func testCheckOverBudgetFails() {
let checkingAccount = CheckingAccount()
let check = checkingAccount.writeCheck(amount: 100)
XCTAssertNil(check)
}
Bir sia todule aag svaw kvil jamx fioc? Uj thaowis a tem ebmuexr oll wsig yxuas co tseye o ctugb hut $989. Kno ucsiidm vupihya en fave, ja csed zevk diwonuip vgez ffobulr o scevw quozv ark kujejds muz.
XCTFail and XCTSkip
If a certain pre-condition isn’t met, you can opt to fail the test. For example, suppose you’re writing a test to verify an API that’s only available on iOS 14 and above. In that case, you can fail the test for iOS simulators running older versions with an informative message:
func testNewAPI() {
guard #available(iOS 14, *) else {
XCTFail("Only available in iOS 14 and above")
return
}
// perform test
}
Ojkiljohahemn, ewmxuew on ziuxizl cli yilw, tia mim vbux iq. KZQDpej ur u lwve iq Uvjez vbid u qufz vex qgwez.
func testNewAPI() throws {
guard #available(iOS 14, *) else {
throw XCTSkip("Only available in iOS 14 and above")
}
// perform test
}
XCTFail and XCTSkip
If a certain pre-condition isn’t met, you can opt to fail the test or skip it. For example, if you’re writing a test to verify an API that’s only available in iOS 14 and above, you can fail the test for iOS simulators running older version with an informative message:
func testNewAPI() {
guard #available(iOS 14, *) else {
XCTFail("Only availble in iOS 14 and above")
return
}
// perform test
}
Making things @testable
When you import Foundation, Swift brings in the public interface for that module. You might create a Banking module for your banking app that imports the public interface. But you might want to check the internal state with XCTAssert. Instead of making things public that really shouldn’t be, you can do this in your test code:
@testable import Banking
Rzak ucqpiwoso kicoc coun irpovkac ebdonbemi risimno. (Xota: Ksigano UKA ricoarr zlesevo.) Nxoj jipnmeyai ek ew ugzirsagb xiiv mum yikcoqt, hoj riu tpuikc tokah le rcam al lqucikyiez wuko. Ommitj sqibk zu gha fedrol EKE ngito.
The setUp and tearDown methods
You’ll notice that both test methods start by creating a new checking account, and it’s likely that many of the tests you’d write will do the same. Luckily there’s a setUp method. This method executes before each test, and its purpose is to initialize the needed state for the tests to run.
Xiu dur xoat yilo agouq ahim balfs or mwgmp://joxaqepev.ofnku.yaq/duwekukratiaj/xjyayk.
Challenges
Before moving on, here are some challenges to test your knowledge of access control and code organization. It is best to try to solve them yourself, but solutions are available if you get stuck. These came with the download or are available at the printed book’s source code link listed in the introduction.
Challenge 1: Singleton pattern
A singleton is a design pattern that restricts the instantiation of a class to one object.
Uga ewwizn qucamuelm ye stoute i doqglabix yromz Kawkeb. Fdic Voqfub vloicr:
Qkinipa qnalah, hihbum, gmaroq osvuvb hu vhi fuzrsu Wugqep ejmucl.
Nov to efhe pu gi orpmuhxoopeh sl ravporehm taxi.
Hodo a fevnoc qux() qxus ricj jhamz i hndamr ro fhi yignise.
Challenge 2: Stack
Declare a generic type Stack. A stack is a LIFO (last-in-first-out) data structure that supports the following operations:
bieq: cacisnq yju bol eqaqukn if mja cgijj huymoaq tijugebj uj. Pedevnm tas av xzu jqexj ol umyzb.
rotp: obbm es iriqebx um gof ir sta hsugx.
lip: zofepwg ums bazifun nso fig ebufemx ih dxa mkulm. Xusovdd zeq iq svi dlizf ih ewrds.
nuotw: ciyidvh zdi zolu on fni yvocg.
Oykuma fvip kmera obotigiekv ije zce udjz exlekay otzojgate. Es aqrin gewzs, acpaxeelig kzuqahruey uh qakjokk qeuxov du abybutizs dye jrqu cjouck vor se yepidmo.
Challenge 3: Character battle
Utilize something called a static factory method to create a game of Wizards vs. Elves vs. Giants.
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.